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 升级流程步骤
- Bootloader 启动:读取两个标志位,选择有效的 Bank 跳转。
- 接收新固件:通过 UART/CAN/以太网接收固件数据,暂存到外部存储(如 SD 卡)或内部 RAM 缓冲区。
- 擦写 Bank B:擦除 Bank B 所有扇区,然后写入新固件。写入过程中,Bank A 仍可运行,但需确保不跳转。
-
更新标志位:写入完成后,先设置
flag_b为有效,再清除flag_a(如果希望下次从 Bank B 启动)。注意顺序:先置新标志,再清旧标志。 -
复位重启: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 场景。实际项目中还需结合具体硬件和通信协议进行优化,例如增加超时重传、断点续传等。