STM32H7 双 Bank Flash 在线升级:掉电安全策略与实现
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 系列 MCU 中,双 Bank Flash 支持无缝固件升级,但擦写期间意外掉电可能导致两个 Bank 均不可用,设备变砖。本文深入分析双 Bank 架构的掉电风险,提出基于标志位与备份机制的防掉电策略,并给出完整的实现代码,帮助开发者构建高可靠的在线升级方案。
# 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 标志位读写函数
```c
#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 跳转逻辑
```c
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 升级函数(简化)
```c
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 场景。实际项目中还需结合具体硬件和通信协议进行优化,例如增加超时重传、断点续传等。