# STM32双Bank Flash在OTA升级中的零停机回滚实践 ## 一、为什么需要双Bank?OTA的痛点 传统单Bank OTA流程:Bootloader擦除App区→写入新固件→跳转。这期间设备处于不可用状态,且一旦写入中途断电或校验失败,设备变砖。双Bank Flash将存储区分为Bank0和Bank1,两个Bank可独立擦写,且支持硬件级Bank切换——即CPU可以从一个Bank的App直接跳转到另一个Bank的App,无需搬移代码。 **核心优势**: - 升级时,新固件写入非活动Bank,旧固件继续运行,实现零停机。 - 切换前通过软件标志确认新固件完整性,失败则回滚到旧Bank。 - 硬件自动映射地址,无需修改链接脚本中的运行地址。 ## 二、硬件原理:STM32双Bank架构解析 以STM32H743为例(2MB Flash,双Bank各1MB),关键寄存器: - `FLASH_CR` 的 `BANK_SW` 位:置1时,系统将Bank0和Bank1的地址映射互换(即Bank0映射到0x08000000,Bank1映射到0x08100000,反之亦然)。 - `FLASH_SR` 的 `BANK_BUSY` 位:指示当前Bank切换是否完成。 **地址映射规则**: - 默认:Bank0 → 0x08000000,Bank1 → 0x08100000。 - 切换后:Bank1 → 0x08000000,Bank0 → 0x08100000。 - 因此,App代码必须链接在0x08000000(即当前活动Bank的起始地址),而Bootloader固定运行在0x08000000(但Bootloader本身不参与切换,它始终位于系统Flash起始区域,通常占用前64KB)。 **关键点**: - 双Bank切换是硬件行为,CPU的PC指针会立即映射到新地址,但需要确保切换时无中断冲突。 - 擦写非活动Bank时,活动Bank的代码执行不受影响(但需注意Flash接口带宽,避免同时读写冲突)。 ## 三、工程架构设计 ### 3.1 内存布局 ```c // Bootloader (固定0x08000000, 64KB) #define BOOT_START 0x08000000 #define BOOT_SIZE 0x10000 // 64KB // App区域:双Bank各占剩余空间 #define BANK0_START 0x08010000 // 1MB - 64KB = 960KB #define BANK1_START 0x08110000 // 对应Bank1的起始 #define APP_MAX_SIZE 0xF0000 // 每个Bank最大固件大小 ``` ### 3.2 软件状态标志 在Flash的最后一个扇区(或系统存储区)保存升级状态: ```c typedef enum { APP_VALID = 0xA5A5A5A5, APP_INVALID = 0x5A5A5A5A, APP_NEW_READY = 0x12345678 // 新固件已写入,待切换 } AppState_t; // 状态存储地址(使用独立扇区,避免与固件冲突) #define STATE_ADDR 0x080FF000 // 假设最后一个扇区 ``` ### 3.3 双Bank切换流程 ``` Bootloader启动 → 检查状态标志 ├─ APP_VALID → 跳转到当前活动Bank(由BANK_SW决定) ├─ APP_NEW_READY → 执行Bank切换(置BANK_SW位)→ 验证新固件CRC → 成功则置APP_VALID,失败则回滚(清除BANK_SW) └─ APP_INVALID → 进入固件下载模式(如UART/USB) ``` ## 四、核心代码实现 ### 4.1 Flash操作驱动(基于HAL库) ```c // 解锁Flash并擦写指定Bank HAL_StatusTypeDef Flash_WriteBank(uint32_t bank_addr, uint8_t *data, uint32_t len) { HAL_StatusTypeDef status; uint32_t addr = bank_addr; uint32_t remaining = len; // 解锁Flash HAL_FLASH_Unlock(); // 擦除整个Bank(或按扇区擦除,这里简化) FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Banks = (bank_addr == BANK0_START) ? FLASH_BANK_1 : FLASH_BANK_2; erase.Sector = 0; erase.NbSectors = 24; // 根据实际扇区数调整 erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; uint32_t sector_error = 0; status = HAL_FLASHEx_Erase(&erase, §or_error); if (status != HAL_OK) return status; // 编程数据(按双字写入) uint64_t temp; for (uint32_t i = 0; i < remaining; i += 8) { memcpy(&temp, data + i, 8); status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, temp); if (status != HAL_OK) break; } HAL_FLASH_Lock(); return status; } ``` ### 4.2 Bank切换与回滚 ```c // 切换活动Bank void Bank_Switch(void) { // 等待当前操作完成 while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY)); // 设置BANK_SW位 FLASH->CR |= FLASH_CR_BANK_SW; // 等待切换完成 while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY)); } // 回滚:恢复原Bank void Bank_Rollback(void) { // 清除BANK_SW位 FLASH->CR &= ~FLASH_CR_BANK_SW; while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY)); } // 验证固件CRC(假设固件末尾存储CRC32) uint32_t Verify_Firmware(uint32_t bank_addr, uint32_t size) { uint32_t crc = 0; // 计算CRC... 这里省略具体实现 // 读取固件末尾的存储CRC并比较 return (crc == stored_crc) ? 1 : 0; } ``` ### 4.3 Bootloader主逻辑 ```c void Bootloader_Main(void) { AppState_t state = Read_State(); if (state == APP_NEW_READY) { // 尝试切换Bank Bank_Switch(); // 验证新固件(现在活动Bank是新固件) if (Verify_Firmware(0x08000000, APP_MAX_SIZE)) { Write_State(APP_VALID); Jump_To_App(); } else { // 回滚 Bank_Rollback(); Write_State(APP_VALID); // 旧固件仍有效 Jump_To_App(); } } else if (state == APP_VALID) { Jump_To_App(); } else { // 进入下载模式,等待接收新固件 Download_Firmware(); } } // 跳转到App(设置向量表偏移) void Jump_To_App(void) { uint32_t app_addr = 0x08000000; // 检查栈顶地址合法性 if (((*(volatile uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) { // 设置主栈指针 __set_MSP(*(volatile uint32_t*)app_addr); // 跳转 void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr + 4)); app_reset(); } } ``` ### 4.4 App端升级触发 ```c // App中收到新固件包,写入非活动Bank void OTA_Update(uint8_t *new_fw, uint32_t len) { // 判断当前活动Bank uint32_t inactive_bank = (FLASH->CR & FLASH_CR_BANK_SW) ? BANK0_START : BANK1_START; // 写入新固件 Flash_WriteBank(inactive_bank, new_fw, len); // 设置状态为待切换 Write_State(APP_NEW_READY); // 软复位,进入Bootloader执行切换 NVIC_SystemReset(); } ``` ## 五、注意事项与工程陷阱 - **中断向量表**:App的向量表必须重定位到0x08000000(活动Bank起始),在App启动代码中设置`SCB->VTOR = 0x08000000`。 - **Flash擦写时间**:双Bank擦写时,活动Bank的代码执行可能因Flash接口竞争而变慢,需优化时序或使用缓存。 - **掉电保护**:在写入状态标志时,建议使用双字写入并加校验,防止半写状态。 - **Bank切换的原子性**:切换过程不可中断,需关闭全局中断或使用临界区。 - **链接脚本**:Bootloader和App的链接脚本需严格区分地址,App的`FLASH`起始地址固定为0x08000000,但实际物理Bank由硬件决定。 - **调试技巧**:使用`BANK_SW`位时,在调试器中查看`FLASH->CR`值确认当前Bank状态。 ## 六、总结 双Bank Flash方案将OTA升级的“停机窗口”压缩到微秒级(仅Bank切换瞬间),并通过硬件映射简化了固件管理。本文提供的工程代码可直接移植到STM32H7/L4+系列,配合CRC校验和状态机,可构建工业级可靠的远程升级系统。实际产品中,还需考虑固件签名、加密传输等安全措施,但双Bank机制本身已为高可用性打下坚实基础。