# STM32双Bank Flash在OTA中的无缝回滚设计与实现 ## 1. 为什么需要双Bank? 传统OTA方案通常采用单Bank + 外部备份区,升级时先擦写备份区,再复制到主区。但复制过程存在窗口期,一旦断电或出错,主区固件可能不完整,导致设备无法启动。而STM32的双Bank Flash(如STM32H7、L4+、F7等系列)将Flash分为两个独立的Bank(Bank0和Bank1),每个Bank可独立擦写,且硬件支持从任一Bank启动。升级时直接写入非活动Bank,完成后通过切换启动Bank实现原子性切换,无需复制,极大降低风险。 ## 2. 双Bank原理与硬件支持 - **Bank划分**:以STM32H743为例,Flash共2MB,分为Bank0(0x08000000-0x080FFFFF)和Bank1(0x08100000-0x081FFFFF),每个Bank大小1MB。 - **启动配置**:通过选项字节(Option Bytes)中的`BOOT_ADD0`和`BOOT_ADD1`设置启动地址。当`BOOT_SW`位为0时,始终从Bank0启动;为1时,可通过软件切换启动Bank。 - **硬件切换**:写`FLASH_CR`寄存器的`SWAP_BANK`位,触发Bank交换,硬件自动将非活动Bank映射到地址0x08000000,无需搬移代码。 - **关键优势**:切换操作是原子的,即使切换后新固件崩溃,也可通过硬件看门狗或外部触发再次切换回旧Bank,实现无缝回滚。 ## 3. 系统架构设计 本设计基于STM32H743,使用两个Bank各存储一份固件,Bootloader位于Bank0起始区域,App1位于Bank0后半部分,App2位于Bank1。实际项目中可灵活划分,但需保证每个Bank的App起始地址对齐(如0x20000对齐)。 - **Bootloader**:负责检查固件有效性,决定启动哪个Bank,并处理升级请求。 - **App1/App2**:功能相同的应用固件,编译时通过链接脚本指定不同的Flash起始地址。 - **升级流程**:运行中的App接收新固件包,写入非活动Bank,校验通过后设置标志位并复位,Bootloader根据标志切换Bank启动新固件。 ## 4. 实现步骤 ### 4.1 配置Flash双Bank模式 默认情况下,STM32H7可能处于单Bank模式(2MB合并)。需通过选项字节启用双Bank。在CubeMX中或代码中操作: ```c // 启用双Bank模式(需在解锁Flash后执行) void Flash_EnableDualBank(void) { FLASH_OBProgramInitTypeDef ob; HAL_FLASHEx_OBGetConfig(&ob); if (ob.Banks == FLASH_BANK_BOTH) { // 当前为单Bank ob.Banks = FLASH_BANK_BOTH; ob.USERType = FLASH_OB_USER_BANK_SWAP; ob.USERConfig = FLASH_OB_USER_BANK_SWAP_ENABLE; HAL_FLASHEx_OBProgram(&ob); HAL_FLASH_OB_Launch(); // 触发重新加载选项字节 } } ``` ### 4.2 固件写入非活动Bank 在App中,根据当前运行Bank确定目标Bank地址。例如,若运行在Bank0(基址0x08000000),则目标Bank1基址为0x08100000。写入时需按扇区擦除,注意H7的Flash扇区大小(128KB)。 ```c #define BANK0_BASE 0x08000000 #define BANK1_BASE 0x08100000 #define APP_SIZE 0x80000 // 假设App最大512KB uint32_t GetTargetBankBase(void) { // 通过读取VTOR或链接变量判断当前运行位置 if ((uint32_t)&_vectors >= BANK0_BASE && (uint32_t)&_vectors < BANK0_BASE + APP_SIZE) return BANK1_BASE; else return BANK0_BASE; } // 写入固件包(分包写入,每包1KB) void WriteFirmware(uint32_t offset, uint8_t *data, uint32_t len) { uint32_t target = GetTargetBankBase() + offset; // 擦除对应扇区(需先计算扇区号) FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Sector = GetSector(target); erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; uint32_t error; HAL_FLASHEx_Erase(&erase, &error); // 编程 for (uint32_t i = 0; i < len; i += 8) { uint64_t val = *(uint64_t*)(data + i); HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, target + i, val); } } ``` ### 4.3 固件校验与切换 写入完成后,计算整个固件的CRC或哈希,与包头中的值比对。校验通过后,设置一个标志位(如备份区或Flash末尾的专用区域),然后复位。Bootloader启动时读取该标志。 ```c // 设置升级标志(存储于备份寄存器或专用Flash页) void SetUpgradeFlag(uint8_t bank) { // 示例:使用RTC备份寄存器 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, bank); } // 执行切换 void PerformSwap(void) { HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); // 设置SWAP_BANK位 FLASH->CR |= FLASH_CR_SWAP_BANK; while (FLASH->CR & FLASH_CR_SWAP_BANK); // 等待完成 HAL_FLASH_Lock(); NVIC_SystemReset(); } ``` ### 4.4 Bootloader启动逻辑 Bootloader在启动时检查标志位,决定从哪个Bank启动。若新固件运行失败,可通过看门狗复位,Bootloader检测到运行次数超限,自动回滚。 ```c void Bootloader_JumpToApp(void) { uint8_t bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); uint32_t app_addr = (bank == 1) ? BANK1_BASE : BANK0_BASE; // 检查App有效性(栈顶地址合法且校验通过) if (IsAppValid(app_addr)) { // 设置主栈指针 __set_MSP(*(uint32_t*)app_addr); // 跳转 void (*app_entry)(void) = (void*)(*(uint32_t*)(app_addr + 4)); app_entry(); } else { // 回滚到另一个Bank uint32_t fallback = (bank == 1) ? BANK0_BASE : BANK1_BASE; if (IsAppValid(fallback)) { __set_MSP(*(uint32_t*)fallback); ((void(*)(void))(*(uint32_t*)(fallback + 4)))(); } } } ``` ## 5. 完整示例代码 以下为Bootloader核心逻辑(简化版): ```c #include "main.h" #define APP_MAGIC 0xDEADBEEF typedef struct { uint32_t magic; uint32_t crc; uint32_t size; } AppHeader; int IsAppValid(uint32_t addr) { AppHeader *hdr = (AppHeader*)addr; if (hdr->magic != APP_MAGIC) return 0; // 计算CRC(需实现) return (CalcCRC(addr + sizeof(AppHeader), hdr->size) == hdr->crc); } void JumpToApp(uint32_t addr) { if (IsAppValid(addr)) { __set_MSP(*(uint32_t*)addr); void (*entry)(void) = (void*)(*(uint32_t*)(addr + 4)); entry(); } } int main(void) { HAL_Init(); // 检查升级标志 uint8_t new_bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1); if (new_bank == 1) { // 尝试启动新Bank JumpToApp(BANK1_BASE); // 若失败,回滚 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0); JumpToApp(BANK0_BASE); } else { JumpToApp(BANK0_BASE); // 若失败,尝试Bank1 JumpToApp(BANK1_BASE); } // 都失败,进入Bootloader升级模式 while(1); } ``` ## 6. 注意事项 - **链接脚本**:App1和App2的起始地址必须不同,且中断向量表需重定位(设置`SCB->VTOR`)。 - **Flash擦写时间**:H7的Flash编程以256位(32字节)为单位,擦除一个扇区约需1秒,升级时需考虑功耗和超时。 - **看门狗**:在切换后启动新App前,喂狗并设置一个“运行成功”标志,若新App未及时清除,则触发回滚。 - **选项字节操作**:修改选项字节可能导致芯片复位,需在升级流程中谨慎处理,避免频繁操作。 - **双Bank模式与读保护**:启用双Bank后,读保护等级可能影响Bank切换,需根据需求配置。 - **测试**:务必在真机上验证断电、复位等异常场景,确保回滚逻辑可靠。 ## 7. 总结 双Bank Flash为OTA提供了硬件级的原子切换,配合Bootloader的校验与回滚机制,可显著提升固件升级的可靠性。本文的实现基于STM32H7,但原理适用于所有支持双Bank的STM32系列。开发者可根据实际Flash大小和App体积调整Bank划分,并加入版本管理、断点续传等增强功能。