# STM32双Bank启动模式下OTA失败后回滚机制的设计与验证 ## 1. 为什么需要回滚机制? 嵌入式设备OTA升级时,若新固件在传输、校验或写入过程中发生错误(如断电、CRC不匹配),设备可能无法启动。传统单Bank方案需外部备份或Bootloader辅助,复杂且易失败。STM32的**双Bank Flash**(如STM32F7、L4、H7系列)提供硬件级支持:Flash分为两个独立Bank,可交错存储两个固件版本,配合启动配置实现原子切换,从而优雅地实现失败回滚。 ## 2. 双Bank启动原理 - **硬件结构**:Flash划分为Bank0和Bank1,每个Bank可独立擦写。通过设置选项字节(Option Bytes)中的`nBOOT1`和`BOOT0`引脚,或使用`SYSCFG_MEMRMP`寄存器,可控制从哪个Bank启动。 - **软件切换**:在运行时,通过写`FLASH_CR`的`BOR_LEV`或使用`FLASH_OB_Program`修改启动配置,但更常用的是**在Bootloader中根据标志位跳转**。 - **关键点**:双Bank模式下,两个Bank的地址映射不同(如Bank0从0x08000000,Bank1从0x08040000),但CPU可通过重映射(Memory Remap)将任一Bank映射到0x08000000。 ## 3. 回滚机制设计 ### 3.1 总体架构 - **Bootloader**(位于Bank0起始区域,固定不变):负责启动引导、固件校验、回滚决策。 - **App0**(旧版本,位于Bank0剩余区域)和**App1**(新版本,位于Bank1)交替存放。 - **状态标志**:在Flash末尾或独立扇区存储升级状态(如`STATE_VALID`、`STATE_PENDING`、`STATE_FAILED`)。 ### 3.2 升级流程 1. 新固件下载到Bank1(若当前运行App0),写入完成后置状态为`STATE_PENDING`。 2. 重启进入Bootloader,检查状态: - 若为`STATE_PENDING`,则校验Bank1的CRC/签名。 - 校验通过:将状态改为`STATE_VALID`,并跳转到Bank1执行新固件。 - 校验失败:将状态改为`STATE_FAILED`,并跳转到Bank0的旧固件(回滚)。 3. 新固件运行后,可主动报告“升级成功”,Bootloader再清除状态标志,完成升级。 ### 3.3 回滚触发条件 - 新固件校验失败(CRC、签名)。 - 新固件启动后,在预设时间内未收到“心跳”或“升级成功”确认(需看门狗配合)。 ## 4. 配置步骤(以STM32F767为例) ### 4.1 内存布局 - 设置链接脚本,将Bootloader放在Bank0起始(0x08000000,大小32KB),App0放在Bank0剩余(0x08008000),App1放在Bank1(0x08040000)。 - 两个App的链接脚本中,`FLASH_ORIGIN`分别设为对应地址。 ### 4.2 选项字节配置 - 使用STM32CubeProgrammer或代码设置`nBOOT1=0`,`BOOT0=0`,使Bootloader从主Flash启动。 - 在Bootloader中,通过`SYSCFG->MEMRMP`寄存器切换Bank映射: ```c // 映射Bank1到0x08000000 SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据具体型号,可能需组合位 ``` ### 4.3 状态标志存储 - 在Flash末尾(如0x080FFFF0)分配一个扇区,存储状态结构体。 - 注意擦写次数,建议使用双字备份或磨损均衡。 ## 5. 代码实现示例 ### 5.1 Bootloader核心逻辑 ```c // 状态定义 #define STATE_EMPTY 0xFFFFFFFF #define STATE_PENDING 0xA5A5A5A5 #define STATE_VALID 0x5A5A5A5A #define STATE_FAILED 0x12345678 // 跳转函数 void jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t*)app_addr; uint32_t app_pc = *(volatile uint32_t*)(app_addr + 4); // 设置主栈指针 __set_MSP(app_sp); // 跳转 void (*app_entry)(void) = (void (*)(void))app_pc; app_entry(); } int main() { // 读取状态 uint32_t state = *(volatile uint32_t*)STATE_FLASH_ADDR; if (state == STATE_PENDING) { // 校验Bank1固件(示例:CRC32) if (crc32_check(BANK1_APP_ADDR, APP_MAX_SIZE) == 0) { // 校验通过,置为VALID flash_write_word(STATE_FLASH_ADDR, STATE_VALID); // 映射Bank1并跳转 SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据型号调整 jump_to_app(BANK1_APP_ADDR); } else { // 校验失败,置为FAILED,回滚到Bank0 flash_write_word(STATE_FLASH_ADDR, STATE_FAILED); jump_to_app(BANK0_APP_ADDR); } } else if (state == STATE_VALID) { // 正常启动Bank1 SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; jump_to_app(BANK1_APP_ADDR); } else { // 默认启动Bank0 jump_to_app(BANK0_APP_ADDR); } while(1); } ``` ### 5.2 新固件中的“升级成功”确认 ```c // 在App1初始化完成后,调用此函数通知Bootloader升级成功 void ota_confirm_success(void) { // 擦除状态标志,表示升级完成 flash_erase_sector(STATE_FLASH_SECTOR); flash_write_word(STATE_FLASH_ADDR, STATE_EMPTY); } ``` ### 5.3 看门狗配合 - 在Bootloader跳转前启动独立看门狗(IWDG),超时时间设为10秒。 - 新固件启动后,必须在超时前喂狗,否则复位回Bootloader,Bootloader检测到状态仍为`STATE_PENDING`且校验失败,则回滚。 ## 6. 验证方法 - **模拟传输错误**:在升级过程中人为断电,重启后观察是否回滚到旧版本。 - **模拟校验失败**:在写入Bank1后,篡改一个字节,重启后应回滚。 - **测试看门狗**:新固件故意不喂狗,观察复位后回滚。 - **使用逻辑分析仪**:监控GPIO输出,确认Bootloader跳转路径。 ## 7. 注意事项 - **Bank大小**:确保两个Bank容量足够,且App不超过Bank大小。 - **中断向量表**:App编译时需将中断向量表偏移到对应Bank地址(如`SCB->VTOR = BANK1_APP_ADDR`)。 - **Flash擦写**:在App中擦写状态标志时,注意不要擦除自身代码区域。 - **双Bank映射**:不同型号的映射位不同,务必参考参考手册。 - **状态标志可靠性**:建议使用双字备份,写入时先擦后写,并校验。 ## 8. 总结 利用STM32双Bank硬件特性,结合简单的状态机,可以构建一个健壮的OTA回滚机制。本文的设计不仅避免了设备变砖,还提供了灵活的升级确认流程。实际项目中,可根据需求增加签名验证、多版本回退等增强功能。希望本文能帮助你在嵌入式OTA开发中少走弯路。