# 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 场景。实际项目中还需结合具体硬件和通信协议进行优化,例如增加超时重传、断点续传等。