STM32 双 Bank 模式在线升级失败后自动回滚的 Bootloader 实现细节

一、为什么需要双 Bank 回滚?

传统单 Bank 升级流程中,Bootloader 将新固件写入 Flash 后直接跳转。若新固件损坏或运行异常,设备将无法启动,只能依赖外部烧录器恢复,这在远程维护场景下是灾难性的。

STM32 的双 Bank 模式(如 STM32F7、H7 系列)将 Flash 划分为两个独立的 Bank(Bank0 和 Bank1),每个 Bank 可独立擦写。Bootloader 可运行在 Bank0,同时将新固件写入 Bank1,通过硬件位切换启动 Bank,实现原子级切换。若新固件运行失败,Bootloader 可回切到旧 Bank,保证设备可用。

二、双 Bank 回滚的核心原理

2.1 硬件支持

  • Flash 分区:以 STM32H743 为例,Flash 总容量 2MB,Bank0 和 Bank1 各 1MB。每个 Bank 的起始地址分别为 0x08000000 和 0x08100000。
  • 选项字节:通过修改选项字节中的 nDBANK 位,可配置为单 Bank 或双 Bank 模式。双 Bank 模式下,Flash 编程必须按 Bank 独立操作。
  • 启动切换:通过设置 Flash 控制寄存器(FLASH_CR)中的 BOR 位或使用 FLASH_BOR 命令,可触发 Bank 切换。切换后,CPU 从新的 Bank 复位启动。

2.2 软件设计框架

Bootloader 位于 Bank0 起始区域,用户应用可位于 Bank0 后半部分或 Bank1。本文采用典型方案:

  • Bank0:Bootloader + 旧应用(App_A)
  • Bank1:新应用(App_B)

升级流程:

  1. Bootloader 接收新固件,写入 Bank1。
  2. 写入完成后,设置“升级待验证”标志,并触发 Bank 切换,启动 App_B。
  3. App_B 运行后,若自检通过,则清除标志,并设置“升级成功”标志。
  4. 若 App_B 运行异常(如看门狗超时),系统复位,Bootloader 检测到“升级待验证”标志未清除,自动回滚到 Bank0 的 App_A。

2.3 关键标志管理

标志位存放在 Flash 的独立扇区(如 Bank0 的最后一个扇区),避免被应用覆盖。常用结构:

typedef struct {
    uint32_t magic;      // 魔数,如 0xA5A5A5A5
    uint32_t state;      // 0x01: 待验证, 0x02: 升级成功, 0x03: 回滚
    uint32_t version;    // 固件版本号
} upgrade_flag_t;

三、Bootloader 实现步骤

3.1 初始化与 Bank 检测

void bootloader_init(void) {
    // 读取当前启动 Bank
    uint32_t boot_bank = (FLASH->CR & FLASH_CR_BOR) ? 1 : 0;
    // 读取升级标志
    upgrade_flag_t flag;
    read_flag(&flag);

    if (flag.magic == 0xA5A5A5A5) {
        if (flag.state == 0x01) {
            // 上次升级未验证,回滚
            rollback_to_bank0();
        } else if (flag.state == 0x02) {
            // 升级成功,正常启动当前 Bank
            jump_to_app(boot_bank);
        }
    } else {
        // 无标志,默认启动 Bank0
        jump_to_app(0);
    }
}

3.2 固件接收与写入 Bank1

使用 YMODEM 或自定义协议接收固件,写入 Bank1 时注意地址偏移。

void write_firmware_to_bank1(uint8_t *data, uint32_t len, uint32_t offset) {
    uint32_t addr = BANK1_BASE + offset;
    // 解锁 Flash
    HAL_FLASH_Unlock();
    // 擦除 Bank1(按扇区)
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_1;
    erase.Sector = 0;
    erase.NbSectors = 8; // 根据实际扇区数
    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 temp = 0;
        memcpy(&temp, data + i, 8);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, temp);
    }
    HAL_FLASH_Lock();
}

3.3 触发 Bank 切换

void switch_to_bank1(void) {
    // 设置升级待验证标志
    write_flag(0x01);
    // 设置 BOR 位,切换 Bank
    HAL_FLASH_Unlock();
    FLASH->CR |= FLASH_CR_BOR;
    HAL_FLASH_Lock();
    // 复位
    NVIC_SystemReset();
}

3.4 跳转到应用

void jump_to_app(uint32_t bank) {
    uint32_t app_addr = (bank == 0) ? APP_A_BASE : APP_B_BASE;
    // 检查栈顶地址是否合法
    uint32_t msp = *(volatile uint32_t *)app_addr;
    if ((msp & 0xFFF00000) != 0x20000000) return;
    // 设置主栈指针
    __set_MSP(msp);
    // 跳转到复位向量
    void (*reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));
    reset_handler();
}

3.5 回滚操作

void rollback_to_bank0(void) {
    // 清除待验证标志,设置为回滚状态
    write_flag(0x03);
    // 确保 BOR 位为 0,即 Bank0
    HAL_FLASH_Unlock();
    FLASH->CR &= ~FLASH_CR_BOR;
    HAL_FLASH_Lock();
    NVIC_SystemReset();
}

四、应用端自检与确认

App_B 启动后,应执行自检(如 CRC 校验、外设测试),通过后调用确认函数:

void app_confirm_upgrade(void) {
    upgrade_flag_t flag;
    read_flag(&flag);
    flag.state = 0x02;
    write_flag(&flag);
}

同时,App_B 必须开启独立看门狗(IWDG),若自检失败或运行异常,看门狗超时复位,Bootloader 检测到标志仍为 0x01,执行回滚。

五、注意事项

  • Bank 大小对齐:确保应用固件不超过 Bank 容量,且链接脚本中 Flash 起始地址正确。
  • 中断向量表重定向:应用代码中需设置 SCB->VTOR 为对应 Bank 的起始地址。
  • Flash 编程时序:双 Bank 模式下,擦写 Bank1 时不影响 Bank0 运行,但需注意电源稳定性。
  • 标志存储位置:建议使用独立扇区,并考虑磨损均衡,避免频繁擦写损坏。
  • 回滚条件:除了看门狗,还可利用 RTC 或外部通信超时作为回滚触发条件。

六、总结

双 Bank 模式为 STM32 提供了硬件级的安全升级方案。通过合理的标志管理和 Bank 切换逻辑,Bootloader 能自动检测升级失败并回滚,极大提升了远程升级的可靠性。本文的实现细节可直接应用于实际项目,开发者可根据具体芯片型号调整 Flash 地址和扇区配置。