# STM32双Bank Flash在OTA升级中的防掉电变砖策略与实现 ## 一、为什么需要双Bank Flash? 传统单Bank Flash升级时,新固件直接覆盖旧固件,一旦写入中途掉电,Flash中既无完整新固件,也无可用旧固件,设备只能变砖。双Bank Flash将Flash划分为两个独立区域(Bank1和Bank2),分别存放当前运行固件和新固件。升级时,新固件写入非活动Bank,完成后通过切换启动地址使系统运行新固件。若写入失败或校验错误,仍可回退到旧Bank,确保设备永不掉线。 ## 二、双Bank Flash原理与硬件支持 STM32系列(如F4、L4、H7等)部分型号支持双Bank模式。以STM32F4为例,Flash容量≥1MB时,可配置为双Bank,每个Bank独立擦写。关键特性: - **Bank切换**:通过设置选项字节或操作FLASH_CR寄存器,系统可从Bank1或Bank2启动。 - **原子切换**:切换操作在重启后生效,利用复位向量表重映射实现。 - **独立擦写**:一个Bank擦写时,另一个Bank可正常执行代码(需注意总线访问)。 硬件连接上,无需额外电路,但需确保电源稳定(建议加掉电检测电路,如通过ADC监测VDD)。 ## 三、总体策略设计 1. **分区规划**: - Bank1(0x08000000):当前运行区(App1) - Bank2(0x08040000):待升级区(App2) - 末尾预留标志区(如0x0807F000)存储状态标志。 2. **状态标志**: - `0xA5A5A5A5`:Bank1有效,Bank2无效 - `0x5A5A5A5A`:Bank2有效,Bank1无效 - `0x00000000`:升级中(无效状态) 3. **升级流程**: - 接收新固件,写入非活动Bank(如当前运行Bank1,则写Bank2)。 - 每写一页校验CRC或哈希。 - 全部写入后,设置标志为“Bank2有效”,然后软复位。 - Bootloader检查标志,跳转到对应Bank。 4. **掉电恢复**: - 若写入中途掉电,标志仍为“Bank1有效”,重启后继续运行旧固件。 - 若标志已更新但新固件校验失败,Bootloader可回滚标志并启动旧Bank。 ## 四、代码实现(基于STM32F4 HAL库) ### 1. Flash配置(启用双Bank) ```c // 在系统初始化时调用,设置双Bank模式 void Flash_EnableDualBank(void) { FLASH_OBProgramInitTypeDef obInit; HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); obInit.OptionType = OPTIONBYTE_BANK; obInit.BANK = FLASH_BANK_1; // 或FLASH_BANK_2,取决于当前配置 obInit.BankConfig = FLASH_BANK_DUAL; // 启用双Bank HAL_FLASH_OBProgram(&obInit); HAL_FLASH_OB_Lock(); HAL_FLASH_Lock(); // 需要重新上电或复位生效 NVIC_SystemReset(); } ``` ### 2. 写入固件到目标Bank ```c #define APP1_ADDR 0x08000000 #define APP2_ADDR 0x08040000 #define FLASH_PAGE_SIZE 2048 // 根据型号调整 uint32_t Write_Firmware(uint8_t *data, uint32_t len, uint8_t bank) { uint32_t addr = (bank == 1) ? APP1_ADDR : APP2_ADDR; uint32_t page_num = (len + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE; HAL_FLASH_Unlock(); for (uint32_t i = 0; i < page_num; i++) { // 擦除页 FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.Banks = (bank == 1) ? FLASH_BANK_1 : FLASH_BANK_2; erase.Page = i; erase.NbPages = 1; uint32_t pageError = 0; if (HAL_FLASHEx_Erase(&erase, &pageError) != HAL_OK) { HAL_FLASH_Lock(); return 1; } // 写入数据(按双字对齐) for (uint32_t offset = 0; offset < FLASH_PAGE_SIZE; offset += 8) { uint64_t data64 = 0; memcpy(&data64, data + i*FLASH_PAGE_SIZE + offset, 8); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i*FLASH_PAGE_SIZE + offset, data64) != HAL_OK) { HAL_FLASH_Lock(); return 2; } } } HAL_FLASH_Lock(); return 0; } ``` ### 3. 设置启动标志并切换Bank ```c #define FLAG_ADDR 0x0807F000 #define FLAG_BANK1_VALID 0xA5A5A5A5 #define FLAG_BANK2_VALID 0x5A5A5A5A void Set_BootFlag(uint32_t flag) { HAL_FLASH_Unlock(); // 擦除标志页 FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.Banks = FLASH_BANK_2; // 标志区位于Bank2末尾 erase.Page = (FLAG_ADDR - 0x08000000) / FLASH_PAGE_SIZE; erase.NbPages = 1; uint32_t pageError; HAL_FLASHEx_Erase(&erase, &pageError); // 写入标志 uint64_t data = flag; HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, FLAG_ADDR, data); HAL_FLASH_Lock(); } void Jump_To_App(uint8_t bank) { uint32_t app_addr = (bank == 1) ? APP1_ADDR : APP2_ADDR; // 检查栈顶地址是否合法 if (*(uint32_t*)app_addr > 0x20000000 && *(uint32_t*)app_addr < 0x20020000) { // 设置主栈指针 __set_MSP(*(uint32_t*)app_addr); // 跳转到复位向量 void (*app_reset)(void) = (void (*)(void))(*(uint32_t*)(app_addr + 4)); app_reset(); } } ``` ### 4. Bootloader主逻辑 ```c void Bootloader_Main(void) { uint32_t flag = *(uint32_t*)FLAG_ADDR; if (flag == FLAG_BANK2_VALID) { // 验证Bank2固件(如CRC) if (Verify_Firmware(APP2_ADDR) == 0) { Jump_To_App(2); } else { // 校验失败,回滚 Set_BootFlag(FLAG_BANK1_VALID); Jump_To_App(1); } } else { // 默认运行Bank1 Jump_To_App(1); } } ``` ## 五、注意事项 - **中断向量表重映射**:App固件需在启动时设置SCB->VTOR为自身Bank起始地址,否则中断异常。 - **Flash擦写时间**:双Bank擦写时,若代码从另一Bank执行,需确保总线不冲突(F4支持并行访问)。 - **掉电检测**:建议在升级前检测电压,低于阈值时暂停写入。 - **标志区可靠性**:标志写入前先擦除,且使用双字写入,防止半字写入掉电。 - **固件校验**:写入时逐页计算CRC,并在跳转前整体校验,避免损坏固件运行。 - **测试回滚**:实际测试中,模拟写入中途断电,验证设备能正常回退。 ## 六、总结 双Bank Flash方案通过空间换可靠性,结合标志位和Bootloader跳转,实现了OTA升级的原子性。即使升级过程中掉电,设备也能自动回退到旧版本,彻底解决变砖问题。该策略在工业、物联网等领域具有极高实用价值,是嵌入式OTA设计的核心进阶技术。