STM32H7 双 Bank Flash 在线升级:掉电安全策略与实现

1. 双 Bank Flash 架构与掉电风险

STM32H7 系列(如 STM32H743)内置双 Bank Flash,每个 Bank 容量可达 1MB,支持在运行一个 Bank 的同时擦写另一个 Bank,从而实现无缝升级。然而,这种架构存在一个致命风险:如果在擦写 Bank B 的过程中掉电,而当前程序运行在 Bank A,那么重启后 Bootloader 可能无法判断哪个 Bank 有效,甚至两个 Bank 都处于损坏状态

具体来说,掉电可能发生在以下阶段:

  • 擦除 Bank B 的扇区时,Bank B 的数据已部分丢失,但 Bank A 仍完好。
  • 写入 Bank B 的新固件过程中,Bank B 的数据不完整,但 Bank A 仍可启动。
  • 更危险的是,如果升级流程涉及 Bank A 和 Bank B 的交叉操作(例如先擦除 Bank A,再写 Bank B),掉电可能导致两个 Bank 均不可用。

2. 防掉电设计原则

核心思想:确保在任何时刻,至少有一个 Bank 是可启动的。具体策略包括:

  • 双标志位:在 Flash 的独立区域(如 Option Bytes 或用户 Flash 末尾)存储两个标志,分别表示 Bank A 和 Bank B 的有效性。
  • 备份机制:升级前,将当前运行固件备份到另一个 Bank(如果空间允许),或者确保新固件写入完成前不破坏旧固件。
  • 原子操作:标志位的更新必须原子完成(如写入后立即读回验证)。

3. 实现方案:基于标志位的安全升级流程

3.1 内存布局规划

假设使用 Bank A(地址 0x08000000)运行当前固件,Bank B(地址 0x08080000)用于存放新固件。在 Flash 末尾(如 0x080FFFF0)预留 16 字节存储标志位:

  • flag_a:0x5A5A5A5A 表示 Bank A 有效,0x00000000 表示无效。
  • flag_b:0xA5A5A5A5 表示 Bank B 有效,0x00000000 表示无效。

3.2 升级流程步骤

  1. Bootloader 启动:读取两个标志位,选择有效的 Bank 跳转。
  2. 接收新固件:通过 UART/CAN/以太网接收固件数据,暂存到外部存储(如 SD 卡)或内部 RAM 缓冲区。
  3. 擦写 Bank B:擦除 Bank B 所有扇区,然后写入新固件。写入过程中,Bank A 仍可运行,但需确保不跳转。
  4. 更新标志位:写入完成后,先设置 flag_b 为有效,再清除 flag_a(如果希望下次从 Bank B 启动)。注意顺序:先置新标志,再清旧标志。
  5. 复位重启:Bootloader 检测到 flag_b 有效,跳转到 Bank B。

3.3 掉电恢复机制

如果在步骤 3 中掉电,flag_b 仍为无效,Bootloader 会继续从 Bank A 启动,升级失败但设备正常。如果在步骤 4 中掉电,可能出现 flag_b 已置有效但 flag_a 未清除,此时 Bootloader 优先选择 Bank B(若有效),若 Bank B 固件损坏,则回退到 Bank A。

4. 完整代码示例

以下代码基于 STM32H743,使用 HAL 库。

4.1 标志位读写函数

#define FLAG_ADDR  0x080FFFF0
#define FLAG_A_VALID  0x5A5A5A5A
#define FLAG_B_VALID  0xA5A5A5A5
#define FLAG_INVALID  0x00000000

// 写入标志位(注意:Flash 写前需擦除,但这里使用独立扇区)
void write_flag(uint32_t addr, uint32_t value) {
    HAL_FLASH_Unlock();
    FLASH_EraseInitTypeDef erase = {0};
    uint32_t error = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = FLASH_SECTOR_11; // 根据实际调整
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASHEx_Erase(&erase, &error);
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, value);
    HAL_FLASH_Lock();
}

uint32_t read_flag(uint32_t addr) {
    return *(__IO uint32_t*)addr;
}

// 原子更新标志:先写新标志,再清旧标志
void update_flags(uint32_t new_flag_addr, uint32_t new_val, uint32_t old_flag_addr) {
    write_flag(new_flag_addr, new_val);
    write_flag(old_flag_addr, FLAG_INVALID);
}

4.2 Bootloader 跳转逻辑

void jump_to_app(void) {
    uint32_t flag_a = read_flag(FLAG_ADDR);
    uint32_t flag_b = read_flag(FLAG_ADDR + 4);
    uint32_t app_addr;

    if (flag_b == FLAG_B_VALID) {
        app_addr = 0x08080000; // Bank B
    } else if (flag_a == FLAG_A_VALID) {
        app_addr = 0x08000000; // Bank A
    } else {
        // 双无效,进入错误处理(如等待重新升级)
        return;
    }

    // 检查栈指针合法性
    if (*(uint32_t*)app_addr & 0xFFF00000 != 0x20000000) return;

    // 设置主栈指针并跳转
    __set_MSP(*(uint32_t*)app_addr);
    void (*jump)(void) = (void*)(*(uint32_t*)(app_addr + 4));
    jump();
}

4.3 升级函数(简化)

void perform_upgrade(uint8_t *new_fw, uint32_t size) {
    // 1. 擦除 Bank B
    erase_bank_b();
    // 2. 写入新固件(逐扇区写入)
    for (uint32_t i = 0; i < size; i += 8) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, 0x08080000 + i, *(uint64_t*)(new_fw + i));
    }
    // 3. 更新标志:置 Bank B 有效,清 Bank A
    update_flags(FLAG_ADDR + 4, FLAG_B_VALID, FLAG_ADDR);
    // 4. 复位
    NVIC_SystemReset();
}

5. 注意事项

  • Flash 擦写时间:STM32H7 擦除一个 128KB 扇区约需 1-2 秒,期间掉电风险最高。建议在擦写前关闭中断,并启用独立看门狗(IWDG)防止程序跑飞。
  • 标志位存储位置:建议使用 Option Bytes 或专用数据扇区,避免与代码扇区冲突。若使用用户 Flash,需确保该扇区不被擦除。
  • 固件校验:写入后应进行 CRC 或 SHA256 校验,确保固件完整后再更新标志位。
  • 回退机制:如果新固件运行后自检失败,应能自动回退到旧 Bank。可在应用层设置“运行成功”标志,Bootloader 据此决定是否回退。
  • 双 Bank 启动配置:确保 Bootloader 和两个应用都正确设置 VTOR 寄存器,以指向对应 Bank 的中断向量表。

6. 总结

通过双标志位和严格的更新顺序,可以确保 STM32H7 在双 Bank 升级过程中即使掉电,也至少有一个 Bank 可启动。本文提供的方案简单可靠,适用于大多数 IAP 场景。实际项目中还需结合具体硬件和通信协议进行优化,例如增加超时重传、断点续传等。