# STM32 双Bank切换实现OTA零停机:基于Flash双区映射的实战踩坑记录 ## 引言 在嵌入式系统开发中,OTA(Over-The-Air)升级是常见需求,但传统升级方式通常需要设备重启,导致服务中断。对于要求高可用性的工业设备,零停机升级至关重要。STM32系列(如F4、H7等)支持双Bank Flash架构,允许在运行一个Bank的同时擦写另一个Bank,并通过硬件切换实现无缝跳转。本文将分享基于双Bank切换的OTA实现方案,并总结实战中的关键问题。 ## 双Bank原理与硬件支持 ### 什么是双Bank Flash? STM32的部分型号(如STM32F427、STM32H743)将内部Flash划分为两个独立的Bank(Bank1和Bank2),每个Bank可独立擦写。当系统从Bank1启动时,可以擦写Bank2;反之亦然。通过设置选项字节(Option Bytes)中的`BOR_LEV`和`nBOOT1`位,或使用软件触发`FLASH_CR`寄存器中的`SW_BANK`位,可以切换启动Bank。 ### 零停机实现机制 零停机的关键在于: - 当前运行在Bank1,升级程序写入Bank2; - 写入完成后,设置跳转标志,然后软复位; - Bootloader检测到标志,切换Bank启动新程序; - 新程序运行后,可擦写旧Bank,准备下一次升级。 整个过程仅在复位瞬间有短暂中断(毫秒级),但避免了长时间停机。 ## 硬件与软件配置 ### 硬件要求 - STM32F4xx/H7xx系列,具有双Bank Flash; - 外部Flash(可选)用于存储升级包; - 串口或网络接口用于接收固件。 ### 软件架构 - Bootloader:负责启动和Bank切换; - App1:运行在Bank1; - App2:运行在Bank2。 ## 关键寄存器与配置步骤 ### 1. 使能双Bank模式 在STM32F4中,双Bank模式通过选项字节配置。使用ST-Link或代码设置: ```c // 解锁Flash选项字节 FLASH_OB_Unlock(); // 设置双Bank模式(以F427为例) FLASH_OB_Program(OB_DUAL_BANK, OB_DUAL_BANK_ENABLE); // 重新加载选项字节 FLASH_OB_Launch(); ``` 注意:修改选项字节会导致系统复位,因此需在Bootloader中执行。 ### 2. 编写Bootloader Bootloader负责检查升级标志和启动Bank。 ```c // Bootloader main void bootloader_main(void) { // 检查升级标志(存储在备份寄存器或Flash末尾) if (check_update_flag()) { // 切换Bank if (current_bank() == BANK1) { set_boot_bank(BANK2); } else { set_boot_bank(BANK1); } } // 跳转到App jump_to_app(); } // 切换Bank函数 void set_boot_bank(uint8_t bank) { FLASH_OB_Unlock(); if (bank == BANK2) { FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK2); } else { FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK1); } FLASH_OB_Launch(); } ``` ### 3. 编写App升级逻辑 App运行期间,接收新固件并写入非活动Bank。 ```c // App中升级函数 void ota_update(uint8_t *data, uint32_t len) { uint32_t target_bank = (current_bank() == BANK1) ? BANK2_BASE : BANK1_BASE; // 擦写目标Bank flash_erase(target_bank); // 写入数据 flash_write(target_bank, data, len); // 设置升级标志 set_update_flag(); // 软复位 NVIC_SystemReset(); } ``` ## 完整代码示例 以下是一个简化的Bootloader和App协作示例。 ### Bootloader代码(main.c) ```c #include "stm32f4xx.h" #define APP1_ADDR 0x08008000 // Bank1 App起始地址 #define APP2_ADDR 0x08088000 // Bank2 App起始地址(假设Bank1为1MB,Bank2为1MB) void jump_to_app(uint32_t app_addr) { uint32_t app_stack = *(volatile uint32_t*)app_addr; void (*app_main)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr+4)); __set_MSP(app_stack); app_main(); } int main(void) { // 初始化时钟等 // 检查升级标志(使用备份寄存器) if (RTC->BKP0R & 0x01) { // 清除标志 RTC->BKP0R &= ~0x01; // 切换Bank if (FLASH->OPTR & (1<<31)) { // 当前从Bank2启动 // 设置启动Bank1 FLASH_OB_Unlock(); FLASH->OPTR &= ~(1<<31); FLASH_OB_Launch(); } else { FLASH_OB_Unlock(); FLASH->OPTR |= (1<<31); FLASH_OB_Launch(); } } // 判断当前启动Bank if (FLASH->OPTR & (1<<31)) { jump_to_app(APP2_ADDR); } else { jump_to_app(APP1_ADDR); } while(1); } ``` ### App升级代码(ota.c) ```c #include "stm32f4xx.h" #define BANK1_BASE 0x08000000 #define BANK2_BASE 0x08080000 #define FLASH_SIZE 0x100000 // 1MB uint32_t current_bank(void) { if (FLASH->OPTR & (1<<31)) { return 2; } else { return 1; } } void flash_erase_bank(uint32_t bank_base) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); uint32_t addr = bank_base; while (addr < bank_base + FLASH_SIZE/2) { if (FLASH_EraseSector(addr, VoltageRange_3) != FLASH_COMPLETE) { // 错误处理 } addr += 0x20000; // 扇区大小,根据型号调整 } FLASH_Lock(); } void flash_write_bank(uint32_t bank_base, uint8_t *data, uint32_t len) { FLASH_Unlock(); uint32_t addr = bank_base; uint32_t idx = 0; while (idx < len) { uint32_t word = 0; for (int i=0; i<4; i++) { if (idx < len) { word |= (data[idx] << (8*i)); idx++; } } FLASH_ProgramWord(addr, word); addr += 4; } FLASH_Lock(); } void ota_start(uint8_t *fw_data, uint32_t fw_len) { uint32_t target_bank = (current_bank() == 1) ? BANK2_BASE : BANK1_BASE; flash_erase_bank(target_bank); flash_write_bank(target_bank, fw_data, fw_len); // 设置升级标志 RTC->BKP0R |= 0x01; NVIC_SystemReset(); } ``` ## 实战踩坑记录 ### 坑1:选项字节修改导致意外复位 修改`FLASH_OB_Launch()`会立即复位系统,如果未保存上下文,可能导致数据丢失。解决方案:在修改前将关键数据存入备份寄存器或Flash末尾。 ### 坑2:Bank大小与扇区映射 不同型号的Bank大小和扇区布局不同。例如,STM32F427的Bank1为1MB,Bank2为1MB,但扇区大小不一(前4个扇区16KB,后续64KB)。擦写时必须按扇区对齐,否则会擦除错误区域。 ### 坑3:中断向量表重定位 App编译时需设置`VECT_TAB_OFFSET`,并在启动时重定位中断向量表。 ```c // App中 SCB->VTOR = APP1_ADDR; // 或APP2_ADDR ``` 否则中断无法正确响应。 ### 坑4:Flash写入时禁止中断 Flash编程期间,如果发生中断,可能导致写入失败或死锁。建议在写入前关闭中断,或使用DMA传输。 ### 坑5:升级包校验 在写入前必须校验固件完整性(如CRC32),否则写入损坏数据会导致设备变砖。建议在Bootloader中增加校验步骤。 ## 总结 双Bank切换技术为STM32提供了高效的OTA零停机方案,但实现时需注意选项字节配置、Bank映射、中断向量重定位等细节。通过合理的Bootloader设计,可以确保升级的可靠性和安全性。希望本文的实战经验能帮助开发者少走弯路。 ## 参考资料 - STM32F4xx参考手册(RM0090) - AN3969: STM32F4 in-application programming - AN4657: STM32 dual-bank bootloader