STM32双Bank启动模式下OTA失败后回滚机制的设计与验证

1. 为什么需要回滚机制?

嵌入式设备OTA升级时,若新固件在传输、校验或写入过程中发生错误(如断电、CRC不匹配),设备可能无法启动。传统单Bank方案需外部备份或Bootloader辅助,复杂且易失败。STM32的双Bank Flash(如STM32F7、L4、H7系列)提供硬件级支持:Flash分为两个独立Bank,可交错存储两个固件版本,配合启动配置实现原子切换,从而优雅地实现失败回滚。

2. 双Bank启动原理

  • 硬件结构:Flash划分为Bank0和Bank1,每个Bank可独立擦写。通过设置选项字节(Option Bytes)中的nBOOT1BOOT0引脚,或使用SYSCFG_MEMRMP寄存器,可控制从哪个Bank启动。
  • 软件切换:在运行时,通过写FLASH_CRBOR_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_VALIDSTATE_PENDINGSTATE_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=0BOOT0=0,使Bootloader从主Flash启动。
  • 在Bootloader中,通过SYSCFG->MEMRMP寄存器切换Bank映射:
    // 映射Bank1到0x08000000
    SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据具体型号,可能需组合位
    

4.3 状态标志存储

  • 在Flash末尾(如0x080FFFF0)分配一个扇区,存储状态结构体。
  • 注意擦写次数,建议使用双字备份或磨损均衡。

5. 代码实现示例

5.1 Bootloader核心逻辑

// 状态定义
#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 新固件中的“升级成功”确认

// 在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开发中少走弯路。