STM32 双 Bank 模式在线升级失败后自动回滚的 Bootloader 实现细节
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式设备远程升级中,升级失败可能导致设备变砖。STM32 的双 Bank 模式提供了硬件级的安全回滚机制。本文将深入剖析双 Bank 模式下 Bootloader 的设计原理,包括 Bank 切换、跳转逻辑、升级标志管理以及失败检测,并给出完整的工程代码示例,帮助开发者实现高可靠性的在线升级方案。
# STM32 双 Bank 模式在线升级失败后自动回滚的 Bootloader 实现细节
## 一、为什么需要双 Bank 回滚?
传统单 Bank 升级流程中,Bootloader 将新固件写入 Flash 后直接跳转。若新固件损坏或运行异常,设备将无法启动,只能依赖外部烧录器恢复,这在远程维护场景下是灾难性的。
STM32 的双 Bank 模式(如 STM32F7、H7 系列)将 Flash 划分为两个独立的 Bank(Bank0 和 Bank1),每个 Bank 可独立擦写。Bootloader 可运行在 Bank0,同时将新固件写入 Bank1,通过硬件位切换启动 Bank,实现原子级切换。若新固件运行失败,Bootloader 可回切到旧 Bank,保证设备可用。
## 二、双 Bank 回滚的核心原理
### 2.1 硬件支持
- **Flash 分区**:以 STM32H743 为例,Flash 总容量 2MB,Bank0 和 Bank1 各 1MB。每个 Bank 的起始地址分别为 0x08000000 和 0x08100000。
- **选项字节**:通过修改选项字节中的 `nDBANK` 位,可配置为单 Bank 或双 Bank 模式。双 Bank 模式下,Flash 编程必须按 Bank 独立操作。
- **启动切换**:通过设置 Flash 控制寄存器(FLASH_CR)中的 `BOR` 位或使用 `FLASH_BOR` 命令,可触发 Bank 切换。切换后,CPU 从新的 Bank 复位启动。
### 2.2 软件设计框架
Bootloader 位于 Bank0 起始区域,用户应用可位于 Bank0 后半部分或 Bank1。本文采用典型方案:
- **Bank0**:Bootloader + 旧应用(App_A)
- **Bank1**:新应用(App_B)
升级流程:
1. Bootloader 接收新固件,写入 Bank1。
2. 写入完成后,设置“升级待验证”标志,并触发 Bank 切换,启动 App_B。
3. App_B 运行后,若自检通过,则清除标志,并设置“升级成功”标志。
4. 若 App_B 运行异常(如看门狗超时),系统复位,Bootloader 检测到“升级待验证”标志未清除,自动回滚到 Bank0 的 App_A。
### 2.3 关键标志管理
标志位存放在 Flash 的独立扇区(如 Bank0 的最后一个扇区),避免被应用覆盖。常用结构:
```c
typedef struct {
uint32_t magic; // 魔数,如 0xA5A5A5A5
uint32_t state; // 0x01: 待验证, 0x02: 升级成功, 0x03: 回滚
uint32_t version; // 固件版本号
} upgrade_flag_t;
```
## 三、Bootloader 实现步骤
### 3.1 初始化与 Bank 检测
```c
void bootloader_init(void) {
// 读取当前启动 Bank
uint32_t boot_bank = (FLASH->CR & FLASH_CR_BOR) ? 1 : 0;
// 读取升级标志
upgrade_flag_t flag;
read_flag(&flag);
if (flag.magic == 0xA5A5A5A5) {
if (flag.state == 0x01) {
// 上次升级未验证,回滚
rollback_to_bank0();
} else if (flag.state == 0x02) {
// 升级成功,正常启动当前 Bank
jump_to_app(boot_bank);
}
} else {
// 无标志,默认启动 Bank0
jump_to_app(0);
}
}
```
### 3.2 固件接收与写入 Bank1
使用 YMODEM 或自定义协议接收固件,写入 Bank1 时注意地址偏移。
```c
void write_firmware_to_bank1(uint8_t *data, uint32_t len, uint32_t offset) {
uint32_t addr = BANK1_BASE + offset;
// 解锁 Flash
HAL_FLASH_Unlock();
// 擦除 Bank1(按扇区)
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = FLASH_BANK_1;
erase.Sector = 0;
erase.NbSectors = 8; // 根据实际扇区数
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t error;
HAL_FLASHEx_Erase(&erase, &error);
// 编程数据
for (uint32_t i = 0; i < len; i += 8) {
uint64_t temp = 0;
memcpy(&temp, data + i, 8);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, temp);
}
HAL_FLASH_Lock();
}
```
### 3.3 触发 Bank 切换
```c
void switch_to_bank1(void) {
// 设置升级待验证标志
write_flag(0x01);
// 设置 BOR 位,切换 Bank
HAL_FLASH_Unlock();
FLASH->CR |= FLASH_CR_BOR;
HAL_FLASH_Lock();
// 复位
NVIC_SystemReset();
}
```
### 3.4 跳转到应用
```c
void jump_to_app(uint32_t bank) {
uint32_t app_addr = (bank == 0) ? APP_A_BASE : APP_B_BASE;
// 检查栈顶地址是否合法
uint32_t msp = *(volatile uint32_t *)app_addr;
if ((msp & 0xFFF00000) != 0x20000000) return;
// 设置主栈指针
__set_MSP(msp);
// 跳转到复位向量
void (*reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));
reset_handler();
}
```
### 3.5 回滚操作
```c
void rollback_to_bank0(void) {
// 清除待验证标志,设置为回滚状态
write_flag(0x03);
// 确保 BOR 位为 0,即 Bank0
HAL_FLASH_Unlock();
FLASH->CR &= ~FLASH_CR_BOR;
HAL_FLASH_Lock();
NVIC_SystemReset();
}
```
## 四、应用端自检与确认
App_B 启动后,应执行自检(如 CRC 校验、外设测试),通过后调用确认函数:
```c
void app_confirm_upgrade(void) {
upgrade_flag_t flag;
read_flag(&flag);
flag.state = 0x02;
write_flag(&flag);
}
```
同时,App_B 必须开启独立看门狗(IWDG),若自检失败或运行异常,看门狗超时复位,Bootloader 检测到标志仍为 0x01,执行回滚。
## 五、注意事项
- **Bank 大小对齐**:确保应用固件不超过 Bank 容量,且链接脚本中 Flash 起始地址正确。
- **中断向量表重定向**:应用代码中需设置 `SCB->VTOR` 为对应 Bank 的起始地址。
- **Flash 编程时序**:双 Bank 模式下,擦写 Bank1 时不影响 Bank0 运行,但需注意电源稳定性。
- **标志存储位置**:建议使用独立扇区,并考虑磨损均衡,避免频繁擦写损坏。
- **回滚条件**:除了看门狗,还可利用 RTC 或外部通信超时作为回滚触发条件。
## 六、总结
双 Bank 模式为 STM32 提供了硬件级的安全升级方案。通过合理的标志管理和 Bank 切换逻辑,Bootloader 能自动检测升级失败并回滚,极大提升了远程升级的可靠性。本文的实现细节可直接应用于实际项目,开发者可根据具体芯片型号调整 Flash 地址和扇区配置。