# 引言 在嵌入式设备远程升级(OTA)中,升级失败(如断电、校验错误、写入异常)可能导致设备无法启动。传统单 Bank 方案需要外部备份或恢复模式,而 STM32 系列(如 F4、L4、H7)支持双 Bank 闪存,允许在运行一个 Bank 的同时擦写另一个 Bank,并在重启时切换。结合 Bootloader 设计,可实现升级失败后自动回滚到旧版本,极大提升可靠性。 # 双 Bank 原理与回滚机制 ## 双 Bank 闪存布局 STM32 的双 Bank 模式将内部 Flash 划分为两个独立的 Bank(如 Bank1 和 Bank2),每个 Bank 可独立擦除和编程。以 STM32F4 为例,若 Flash 总容量为 1MB,则每个 Bank 为 512KB。用户可配置为单 Bank 模式(默认)或双 Bank 模式,通过选项字节(Option Bytes)设置。 - **Bank1**:通常存放 Bootloader 和当前运行的应用(App1)。 - **Bank2**:存放待升级的新应用(App2)或备份旧应用。 ## 回滚策略 设计目标:升级失败时,Bootloader 能自动回滚到上一个正常运行的版本。常见策略: 1. **双副本交替**:Bootloader 始终位于固定区域(如 Bank1 起始地址),App1 和 App2 分别位于两个 Bank 的剩余空间。每次升级将新固件写入非活动 Bank,校验通过后切换启动地址;若校验失败,则继续启动旧 Bank。 2. **版本标记**:在 Flash 的固定区域(如最后几个扇区)存储版本号和状态标志(如升级中、升级成功、升级失败)。Bootloader 根据标志决定启动哪个 Bank。 本文采用策略 1,结合状态标志实现回滚。 # 关键配置步骤 ## 1. 启用双 Bank 模式 通过修改选项字节启用双 Bank。可使用 STM32CubeProgrammer 或在代码中通过 Flash 接口操作。以 STM32F4 为例,设置选项字节的 `DBANK` 位为 `1`。 ```c // 启用双 Bank 模式(需在系统复位后生效) FLASH_OBProgramInitTypeDef ob; HAL_FLASHEx_OBGetConfig(&ob); ob.Banks = FLASH_BANK_1; // 或 FLASH_BANK_BOTH ob.OptionType = OPTIONBYTE_BANK; ob.BankConfig = FLASH_BANK_1 | FLASH_BANK_2; // 双 Bank HAL_FLASHEx_OBProgram(&ob); HAL_FLASH_OB_Launch(); // 触发复位 ``` 注意:修改选项字节会擦除 Flash,需在开发阶段完成,量产时通常已配置好。 ## 2. 内存映射规划 假设 Flash 总容量 1MB,双 Bank 后每个 Bank 512KB。规划如下: - **Bank1**: - 0x08000000:Bootloader(32KB,占用 0x08000000-0x08007FFF) - 0x08008000:App1(480KB,占用 0x08008000-0x0807FFFF) - **Bank2**: - 0x08080000:App2(512KB,占用 0x08080000-0x080FFFFF) 注意:Bootloader 必须位于 Bank1 起始地址,因为复位向量固定指向 0x08000000。 ## 3. 应用代码的链接脚本调整 App1 和 App2 需使用不同的链接脚本,设置各自的起始地址和大小。例如,App1 的 `FLASH_ORIGIN` 为 `0x08008000`,App2 为 `0x08080000`。同时,在应用代码中需配置中断向量表偏移: ```c // 在 main 函数开头设置向量表偏移 SCB->VTOR = FLASH_BASE | APP_START_ADDR; ``` 其中 `APP_START_ADDR` 为当前应用的起始地址(如 App1 为 0x08008000)。 ## 4. Bootloader 设计要点 Bootloader 负责启动流程和升级逻辑。核心流程: 1. 检查升级标志(如外部引脚、通信命令)。 2. 若有升级请求,接收新固件并写入非活动 Bank。 3. 写入完成后,校验 CRC 或哈希。 4. 校验通过,更新状态标志,跳转到新 Bank;否则,回滚到旧 Bank。 5. 若无升级请求,直接跳转到当前有效 Bank。 # 完整代码示例 以下为 Bootloader 的核心代码(基于 STM32 HAL 库),演示双 Bank 切换和回滚逻辑。 ```c // 定义 Bank 地址和大小 #define BOOTLOADER_SIZE (32 * 1024) #define APP1_ADDR (FLASH_BASE + BOOTLOADER_SIZE) // 0x08008000 #define APP2_ADDR (FLASH_BASE + 512 * 1024) // 0x08080000 #define APP_MAX_SIZE (480 * 1024) // 状态标志存储地址(使用最后一个扇区) #define STATUS_ADDR (FLASH_BASE + 1024 * 1024 - 4) // 假设 1MB Flash // 状态值定义 #define STATUS_APP1_VALID 0xA5A5A5A5 #define STATUS_APP2_VALID 0x5A5A5A5A #define STATUS_UPGRADING 0xDEADBEEF // 函数声明 void JumpToApp(uint32_t app_addr); int CheckAppValid(uint32_t app_addr); void WriteStatus(uint32_t status); int main(void) { HAL_Init(); // 初始化时钟、串口等... uint32_t status = *(volatile uint32_t *)STATUS_ADDR; uint32_t active_app = APP1_ADDR; // 检查是否有升级请求(例如通过串口命令) if (CheckUpgradeRequest()) { // 升级流程:写入非活动 Bank uint32_t target_bank = (status == STATUS_APP1_VALID) ? APP2_ADDR : APP1_ADDR; WriteStatus(STATUS_UPGRADING); // 标记升级中 if (ReceiveAndWriteFirmware(target_bank)) { // 校验新固件 if (CheckAppValid(target_bank)) { // 更新状态标志 WriteStatus((target_bank == APP2_ADDR) ? STATUS_APP2_VALID : STATUS_APP1_VALID); active_app = target_bank; } else { // 校验失败,保持旧状态,自动回滚 active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR; } } else { // 写入失败,回滚 active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR; } } else { // 无升级请求,根据状态选择启动 Bank if (status == STATUS_APP2_VALID) { active_app = APP2_ADDR; } else { active_app = APP1_ADDR; // 默认 App1 } } // 跳转到应用 JumpToApp(active_app); while (1); } // 跳转到指定地址的应用 void JumpToApp(uint32_t app_addr) { // 检查栈顶地址是否合法 uint32_t msp = *(volatile uint32_t *)app_addr; if ((msp & 0xFFF00000) != 0x20000000) { // 栈指针无效,进入错误处理 ErrorHandler(); } // 设置主栈指针 __set_MSP(msp); // 设置向量表偏移 SCB->VTOR = app_addr; // 获取复位向量并跳转 void (*reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4)); reset_handler(); } // 检查应用是否有效(通过 CRC 或固定标志) int CheckAppValid(uint32_t app_addr) { // 简单检查:读取应用头部固定偏移处的魔数 uint32_t magic = *(volatile uint32_t *)(app_addr + 0x1FC); // 假设在应用头部写入魔数 return (magic == 0xDEADBEEF); // 实际应使用 CRC } // 写入状态标志 void WriteStatus(uint32_t status) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; uint32_t page_error = 0; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = STATUS_ADDR; erase.NbPages = 1; HAL_FLASHEx_Erase(&erase, &page_error); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, STATUS_ADDR, status); HAL_FLASH_Lock(); } ``` # 注意事项 - **选项字节修改**:启用双 Bank 会改变 Flash 布局,务必在开发阶段完成,并确保 Bootloader 和应用链接脚本匹配。 - **中断向量表**:应用必须设置 `SCB->VTOR`,否则中断会跳转到 Bootloader 的向量表。 - **升级过程中的掉电保护**:建议在升级前备份旧版本状态,升级中写入状态标志,以便重启后识别。 - **校验机制**:使用 CRC32 或 SHA256 对整个固件进行校验,避免仅依赖头部魔数。 - **看门狗**:在升级过程中启用独立看门狗(IWDG),防止卡死。 - **双 Bank 切换**:部分 STM32 支持硬件切换(如通过 SYSCFG 寄存器),但本文采用软件跳转方式,更通用。 - **测试**:务必在真机测试断电、写入错误等场景,确保回滚逻辑可靠。 # 总结 利用 STM32 双 Bank 特性,结合状态标志和校验机制,可以设计出高可靠的 OTA 升级方案。Bootloader 在升级失败时自动回滚到旧版本,避免设备变砖。本文提供了核心设计思路和代码示例,开发者可根据具体芯片型号调整内存布局和细节。