STM32双Bank切换实现OTA无缝回滚:边界条件与失败恢复策略

一、双Bank架构原理与优势

双Bank(Dual Bank)是STM32(如F4、L4、H7系列)支持的一种Flash组织方式,将Flash划分为两个独立的Bank(如Bank1和Bank2),每个Bank可独立擦写。OTA升级时,新固件写入非活动Bank,完成后通过切换启动地址(BOOT引脚或选项字节)使系统从新Bank启动。

核心优势:

  • 原子性切换:通过硬件选项字节(如BOOT_SW)实现瞬时切换,避免部分写入导致的半更新状态。
  • 无缝回滚:若新固件启动失败,可自动回退到旧Bank,无需外部干预。
  • 掉电安全:升级过程中断电,旧Bank仍完整,系统可正常启动。

二、关键边界条件分析

2.1 Bank状态有效性校验

切换前必须验证目标Bank的固件完整性,否则可能跳转到无效代码。校验包括:

  • CRC/哈希校验:对整个固件区域计算校验值,与固件头中的期望值比对。
  • 向量表检查:验证目标Bank起始地址的栈指针(SP)和复位向量(Reset Handler)是否在有效RAM/Flash范围。
// 检查Bank固件有效性
int check_bank_valid(uint32_t bank_base) {
    uint32_t sp = *(volatile uint32_t*)bank_base;
    uint32_t reset_handler = *(volatile uint32_t*)(bank_base + 4);
    // 栈指针必须在RAM范围内(示例:0x20000000~0x20020000)
    if (sp < 0x20000000 || sp > 0x20020000) return 0;
    // 复位向量必须在Flash范围内(示例:0x08000000~0x08100000)
    if (reset_handler < 0x08000000 || reset_handler > 0x08100000) return 0;
    return 1;
}

2.2 切换时序与中断安全

切换Bank时,若中断正在执行且向量表未重映射,可能导致跳转后中断异常。必须:

  • 关闭全局中断__disable_irq()
  • 确保无DMA传输(DMA可能访问旧Bank地址)
  • 重映射向量表(通过SCB->VTOR设置新Bank的向量表地址)

2.3 启动标志与回滚触发条件

系统需在每次启动时记录当前Bank状态,并设置“升级成功”标志。若新Bank启动后未在超时时间内设置成功标志,则触发回滚。

// 启动时检查升级标志
void check_ota_status(void) {
    if (ota_flag->upgrade_pending) {
        if (ota_flag->boot_count > MAX_BOOT_TRIES) {
            // 启动失败,回滚到旧Bank
            rollback_to_old_bank();
        } else {
            ota_flag->boot_count++;
        }
    }
}

三、配置步骤(以STM32F4为例)

3.1 开启双Bank模式

通过选项字节设置Flash为双Bank模式(如DBANK=1),并配置BOOT_SW位允许软件切换。

// 使用HAL库设置选项字节
HAL_FLASH_Unlock();
FLASH_OBProgramInitTypeDef ob;
ob.OptionType = OPTIONBYTE_BYTE;
ob.BYTE.BYTE0 = FLASH_OPTIONBYTE_BYTE0_DBANK | FLASH_OPTIONBYTE_BYTE0_BOOT_SW;
HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_Lock();

3.2 固件写入与切换

  1. 擦除目标Bank(如Bank2)
  2. 写入新固件(通过UART/SPI等)
  3. 校验固件完整性
  4. 设置升级标志(存于备份寄存器或Flash末尾)
  5. 执行切换:
void switch_bank(void) {
    __disable_irq();
    // 设置BOOT_SW位,使下次复位从Bank2启动
    HAL_FLASH_Unlock();
    FLASH_OBProgramInitTypeDef ob;
    ob.OptionType = OPTIONBYTE_BYTE;
    ob.BYTE.BYTE0 = FLASH_OPTIONBYTE_BYTE0_BOOT_SW; // 切换启动Bank
    HAL_FLASHEx_OBProgram(&ob);
    HAL_FLASH_Lock();
    // 软件复位
    NVIC_SystemReset();
}

3.3 回滚实现

若新固件启动失败(如看门狗超时),则清除BOOT_SW位,复位回旧Bank。

void rollback_to_old_bank(void) {
    // 清除BOOT_SW位
    HAL_FLASH_Unlock();
    FLASH_OBProgramInitTypeDef ob;
    ob.OptionType = OPTIONBYTE_BYTE;
    ob.BYTE.BYTE0 = 0; // 清除BOOT_SW
    HAL_FLASHEx_OBProgram(&ob);
    HAL_FLASH_Lock();
    NVIC_SystemReset();
}

四、完整代码示例(伪代码框架)

#define BANK1_BASE 0x08000000
#define BANK2_BASE 0x08040000 // 假设每个Bank 256KB

typedef struct {
    uint32_t magic;
    uint32_t boot_count;
    uint32_t upgrade_pending;
} OTAFlag;

OTAFlag *ota_flag = (OTAFlag*)0x40024000; // 备份寄存器区域

void ota_process(void) {
    // 1. 接收固件到RAM缓冲区
    // 2. 写入Bank2
    if (write_firmware_to_bank2() == SUCCESS) {
        // 3. 校验
        if (check_bank_valid(BANK2_BASE) && verify_crc(BANK2_BASE)) {
            // 4. 设置升级标志
            ota_flag->magic = 0xA5A5;
            ota_flag->boot_count = 0;
            ota_flag->upgrade_pending = 1;
            // 5. 切换
            switch_bank();
        }
    }
}

void main(void) {
    // 初始化
    check_ota_status();
    // 正常应用逻辑
}

五、注意事项与工程建议

  • 看门狗必须开启:若新固件死机,看门狗复位后触发回滚逻辑。
  • 备份寄存器保存标志:避免Flash频繁擦写,且掉电不丢失。
  • 双Bank大小限制:确保固件不超过单个Bank容量,否则需改用其他方案。
  • 测试边界条件:模拟升级过程中断电、新固件CRC错误、启动超时等场景。
  • 日志记录:在回滚前记录失败原因(如通过串口或Flash日志)。

六、总结

双Bank切换是OTA高可靠性的基石,但边界条件处理决定成败。通过严格校验、安全时序和明确的回滚策略,可构建近乎零风险的升级系统。开发者应结合具体芯片手册,验证选项字节行为,并充分测试异常路径。