STM32双Bank Flash在OTA升级中的零停机回滚实践:从原理到工程落地
👁 1 阅读 · 2026-08-27 · 嵌入式
在工业物联网与汽车电子领域,OTA升级的可靠性直接决定设备生死。STM32系列(如H7、L4+)内置的双Bank Flash为原子性固件切换提供了硬件基础,但多数开发者仅将其视为普通存储区。本文深入剖析双Bank架构的硬件机制,结合Bootloader设计、Bank状态管理及回滚策略,给出一个可落地的零停机升级方案。通过完整代码示例,你将掌握如何利用Flash双Bank实现升级失败自动回滚、运行中无缝切换,并规避擦写期间的掉电风险。
# STM32双Bank Flash在OTA升级中的零停机回滚实践
## 一、为什么需要双Bank?OTA的痛点
传统单Bank OTA流程:Bootloader擦除App区→写入新固件→跳转。这期间设备处于不可用状态,且一旦写入中途断电或校验失败,设备变砖。双Bank Flash将存储区分为Bank0和Bank1,两个Bank可独立擦写,且支持硬件级Bank切换——即CPU可以从一个Bank的App直接跳转到另一个Bank的App,无需搬移代码。
**核心优势**:
- 升级时,新固件写入非活动Bank,旧固件继续运行,实现零停机。
- 切换前通过软件标志确认新固件完整性,失败则回滚到旧Bank。
- 硬件自动映射地址,无需修改链接脚本中的运行地址。
## 二、硬件原理:STM32双Bank架构解析
以STM32H743为例(2MB Flash,双Bank各1MB),关键寄存器:
- `FLASH_CR` 的 `BANK_SW` 位:置1时,系统将Bank0和Bank1的地址映射互换(即Bank0映射到0x08000000,Bank1映射到0x08100000,反之亦然)。
- `FLASH_SR` 的 `BANK_BUSY` 位:指示当前Bank切换是否完成。
**地址映射规则**:
- 默认:Bank0 → 0x08000000,Bank1 → 0x08100000。
- 切换后:Bank1 → 0x08000000,Bank0 → 0x08100000。
- 因此,App代码必须链接在0x08000000(即当前活动Bank的起始地址),而Bootloader固定运行在0x08000000(但Bootloader本身不参与切换,它始终位于系统Flash起始区域,通常占用前64KB)。
**关键点**:
- 双Bank切换是硬件行为,CPU的PC指针会立即映射到新地址,但需要确保切换时无中断冲突。
- 擦写非活动Bank时,活动Bank的代码执行不受影响(但需注意Flash接口带宽,避免同时读写冲突)。
## 三、工程架构设计
### 3.1 内存布局
```c
// Bootloader (固定0x08000000, 64KB)
#define BOOT_START 0x08000000
#define BOOT_SIZE 0x10000 // 64KB
// App区域:双Bank各占剩余空间
#define BANK0_START 0x08010000 // 1MB - 64KB = 960KB
#define BANK1_START 0x08110000 // 对应Bank1的起始
#define APP_MAX_SIZE 0xF0000 // 每个Bank最大固件大小
```
### 3.2 软件状态标志
在Flash的最后一个扇区(或系统存储区)保存升级状态:
```c
typedef enum {
APP_VALID = 0xA5A5A5A5,
APP_INVALID = 0x5A5A5A5A,
APP_NEW_READY = 0x12345678 // 新固件已写入,待切换
} AppState_t;
// 状态存储地址(使用独立扇区,避免与固件冲突)
#define STATE_ADDR 0x080FF000 // 假设最后一个扇区
```
### 3.3 双Bank切换流程
```
Bootloader启动 → 检查状态标志
├─ APP_VALID → 跳转到当前活动Bank(由BANK_SW决定)
├─ APP_NEW_READY → 执行Bank切换(置BANK_SW位)→ 验证新固件CRC → 成功则置APP_VALID,失败则回滚(清除BANK_SW)
└─ APP_INVALID → 进入固件下载模式(如UART/USB)
```
## 四、核心代码实现
### 4.1 Flash操作驱动(基于HAL库)
```c
// 解锁Flash并擦写指定Bank
HAL_StatusTypeDef Flash_WriteBank(uint32_t bank_addr, uint8_t *data, uint32_t len) {
HAL_StatusTypeDef status;
uint32_t addr = bank_addr;
uint32_t remaining = len;
// 解锁Flash
HAL_FLASH_Unlock();
// 擦除整个Bank(或按扇区擦除,这里简化)
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = (bank_addr == BANK0_START) ? FLASH_BANK_1 : FLASH_BANK_2;
erase.Sector = 0;
erase.NbSectors = 24; // 根据实际扇区数调整
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t sector_error = 0;
status = HAL_FLASHEx_Erase(&erase, §or_error);
if (status != HAL_OK) return status;
// 编程数据(按双字写入)
uint64_t temp;
for (uint32_t i = 0; i < remaining; i += 8) {
memcpy(&temp, data + i, 8);
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, temp);
if (status != HAL_OK) break;
}
HAL_FLASH_Lock();
return status;
}
```
### 4.2 Bank切换与回滚
```c
// 切换活动Bank
void Bank_Switch(void) {
// 等待当前操作完成
while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
// 设置BANK_SW位
FLASH->CR |= FLASH_CR_BANK_SW;
// 等待切换完成
while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
}
// 回滚:恢复原Bank
void Bank_Rollback(void) {
// 清除BANK_SW位
FLASH->CR &= ~FLASH_CR_BANK_SW;
while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BANK_BUSY));
}
// 验证固件CRC(假设固件末尾存储CRC32)
uint32_t Verify_Firmware(uint32_t bank_addr, uint32_t size) {
uint32_t crc = 0;
// 计算CRC... 这里省略具体实现
// 读取固件末尾的存储CRC并比较
return (crc == stored_crc) ? 1 : 0;
}
```
### 4.3 Bootloader主逻辑
```c
void Bootloader_Main(void) {
AppState_t state = Read_State();
if (state == APP_NEW_READY) {
// 尝试切换Bank
Bank_Switch();
// 验证新固件(现在活动Bank是新固件)
if (Verify_Firmware(0x08000000, APP_MAX_SIZE)) {
Write_State(APP_VALID);
Jump_To_App();
} else {
// 回滚
Bank_Rollback();
Write_State(APP_VALID); // 旧固件仍有效
Jump_To_App();
}
} else if (state == APP_VALID) {
Jump_To_App();
} else {
// 进入下载模式,等待接收新固件
Download_Firmware();
}
}
// 跳转到App(设置向量表偏移)
void Jump_To_App(void) {
uint32_t app_addr = 0x08000000;
// 检查栈顶地址合法性
if (((*(volatile uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) {
// 设置主栈指针
__set_MSP(*(volatile uint32_t*)app_addr);
// 跳转
void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr + 4));
app_reset();
}
}
```
### 4.4 App端升级触发
```c
// App中收到新固件包,写入非活动Bank
void OTA_Update(uint8_t *new_fw, uint32_t len) {
// 判断当前活动Bank
uint32_t inactive_bank = (FLASH->CR & FLASH_CR_BANK_SW) ? BANK0_START : BANK1_START;
// 写入新固件
Flash_WriteBank(inactive_bank, new_fw, len);
// 设置状态为待切换
Write_State(APP_NEW_READY);
// 软复位,进入Bootloader执行切换
NVIC_SystemReset();
}
```
## 五、注意事项与工程陷阱
- **中断向量表**:App的向量表必须重定位到0x08000000(活动Bank起始),在App启动代码中设置`SCB->VTOR = 0x08000000`。
- **Flash擦写时间**:双Bank擦写时,活动Bank的代码执行可能因Flash接口竞争而变慢,需优化时序或使用缓存。
- **掉电保护**:在写入状态标志时,建议使用双字写入并加校验,防止半写状态。
- **Bank切换的原子性**:切换过程不可中断,需关闭全局中断或使用临界区。
- **链接脚本**:Bootloader和App的链接脚本需严格区分地址,App的`FLASH`起始地址固定为0x08000000,但实际物理Bank由硬件决定。
- **调试技巧**:使用`BANK_SW`位时,在调试器中查看`FLASH->CR`值确认当前Bank状态。
## 六、总结
双Bank Flash方案将OTA升级的“停机窗口”压缩到微秒级(仅Bank切换瞬间),并通过硬件映射简化了固件管理。本文提供的工程代码可直接移植到STM32H7/L4+系列,配合CRC校验和状态机,可构建工业级可靠的远程升级系统。实际产品中,还需考虑固件签名、加密传输等安全措施,但双Bank机制本身已为高可用性打下坚实基础。