STM32双Bank Flash在线升级失败后自动回滚的Bootloader设计要点

一、为什么需要双Bank Flash?

传统单Bank升级方案中,新固件直接覆盖旧固件,一旦写入过程中断电或校验失败,设备将无法启动。双Bank Flash(如STM32F7/H7系列)将Flash分为两个独立Bank(Bank0和Bank1),可分别存储当前运行固件和新固件。升级时,Bootloader将新固件写入非活动Bank,完成后通过标志位切换启动Bank。若新固件启动失败,Bootloader可自动回滚到旧Bank,确保设备始终可用。

二、双Bank升级与回滚原理

2.1 双Bank内存映射

以STM32H743为例,Flash共2MB,分为Bank0(0x08000000-0x080FFFFF)和Bank1(0x08100000-0x081FFFFF)。默认从Bank0启动,通过设置选项字节或操作Flash控制寄存器(FLASH_CR)的BANK_SWAP位,可切换启动Bank。

2.2 升级流程

  1. Bootloader启动:检查升级标志(如备份寄存器或Flash特定地址)。
  2. 接收固件:通过UART/CAN/以太网接收新固件,写入非活动Bank。
  3. 校验固件:计算CRC或哈希,与固件头比对。
  4. 切换Bank:设置BANK_SWAP位,并置位升级完成标志。
  5. 跳转App:复位并启动新Bank中的App。

2.3 回滚触发条件

  • App启动失败:App在启动后需在限定时间内(如5秒)向备份寄存器写入“运行正常”标志,否则Bootloader判定为启动失败。
  • 硬件看门狗:若App未及时喂狗,看门狗复位,Bootloader检测到异常标志后回滚。
  • 显式回滚命令:App可主动请求回滚(如检测到严重错误)。

回滚操作:清除BANK_SWAP位,恢复原Bank启动,并清除升级标志。

三、Bootloader代码实现

以下基于STM32H743,使用HAL库,展示核心逻辑。

3.1 内存映射与标志定义

#define APP_BANK0_ADDR  0x08020000  // Bank0 App起始(跳过Bootloader)
#define APP_BANK1_ADDR  0x08120000  // Bank1 App起始
#define UPGRADE_FLAG_ADDR 0x0801FF00 // 升级标志存储地址(Bootloader尾部)
#define RUN_OK_FLAG_ADDR  0x0801FF04 // App运行正常标志地址

// 升级标志值
#define UPGRADE_PENDING  0xA5A5A5A5  // 等待升级完成
#define UPGRADE_DONE     0x5A5A5A5A  // 升级完成,需验证App
#define UPGRADE_FAILED   0xDEADBEEF  // 升级失败

3.2 检查并执行回滚

void CheckAndRollback(void) {
    uint32_t upgrade_flag = *(volatile uint32_t*)UPGRADE_FLAG_ADDR;
    uint32_t run_ok_flag = *(volatile uint32_t*)RUN_OK_FLAG_ADDR;

    if (upgrade_flag == UPGRADE_DONE) {
        // 上次升级完成,但本次启动未收到App运行正常标志
        if (run_ok_flag != 0x12345678) {
            // 回滚:切换回Bank0
            FLASH_OBProgramInitTypeDef ob;
            HAL_FLASHEx_OBGetConfig(&ob);
            ob.Banks = FLASH_BANK_1; // 当前从Bank1启动,需切回Bank0
            ob.OptionType = OPTIONBYTE_BANK;
            ob.Bank = FLASH_BANK_1;
            ob.BankSwap = FLASH_BANK_SWAP_DISABLE; // 禁用Bank交换
            HAL_FLASH_Unlock();
            HAL_FLASH_OB_Unlock();
            HAL_FLASHEx_OBProgram(&ob);
            HAL_FLASH_OB_Launch(); // 触发选项字节重载,复位
            // 复位后将从Bank0启动
        } else {
            // App运行正常,清除升级标志
            WriteFlash(UPGRADE_FLAG_ADDR, 0);
        }
    }
}

3.3 跳转到App

typedef void (*pFunction)(void);

void JumpToApp(uint32_t app_addr) {
    uint32_t app_stack = *(volatile uint32_t*)app_addr;
    pFunction app_entry = (pFunction)*(volatile uint32_t*)(app_addr + 4);

    if ((app_stack & 0x2FFE0000) != 0x20000000) return; // 检查栈顶地址合法性

    HAL_RCC_DeInit();
    SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0;
    __disable_irq();

    // 设置MSP为App的栈顶
    __set_MSP(app_stack);
    app_entry();
}

3.4 主流程

int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 检查是否需要回滚
    CheckAndRollback();

    // 判断当前启动Bank(通过选项字节)
    FLASH_OBProgramInitTypeDef ob;
    HAL_FLASHEx_OBGetConfig(&ob);
    uint32_t app_addr = (ob.BankSwap == FLASH_BANK_SWAP_ENABLE) ? APP_BANK1_ADDR : APP_BANK0_ADDR;

    // 检查App有效性(如校验头部)
    if (IsAppValid(app_addr)) {
        JumpToApp(app_addr);
    } else {
        // 无效则进入Bootloader升级模式
        EnterBootloader();
    }

    while(1);
}

3.5 App端配合(关键)

App在main函数开头必须写入运行正常标志,并启动看门狗。

// App main.c
#define RUN_OK_FLAG_ADDR 0x0801FF04

int main(void) {
    // 写入运行正常标志
    *(volatile uint32_t*)RUN_OK_FLAG_ADDR = 0x12345678;

    // 初始化看门狗,若主循环卡死则复位
    MX_IWDG_Init();

    while(1) {
        // 业务逻辑
        IWDG_Refresh();
    }
}

四、注意事项

  • Flash写入保护:Bootloader和App区域需设置读写保护,防止意外擦写。
  • 固件头设计:建议在固件起始处包含魔数、版本号、长度、CRC等,便于Bootloader校验。
  • 升级标志存储:使用备份寄存器(如RTC备份域)更安全,避免Flash磨损,但需注意备份域复位条件。
  • 看门狗超时:App启动后必须在看门狗超时前喂狗,否则回滚机制会误触发。
  • 双Bank切换时序:切换Bank需操作选项字节,注意解锁和重载流程,避免死锁。
  • 中断向量表:App中需设置VTOR寄存器指向对应Bank的起始地址,否则中断异常。
  • 测试回滚:在开发阶段故意制造升级失败(如写入错误固件),验证回滚逻辑。

五、总结

利用STM32双Bank Flash,结合标志位和看门狗,可设计出高可靠的OTA升级方案。本文提供的代码框架覆盖了核心回滚逻辑,开发者可根据具体芯片型号和需求调整。记住:回滚机制的关键在于“快速检测、可靠切换”,务必在真机上反复测试。