STM32双Bank启动模式下OTA失败后回滚机制的设计与验证
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式OTA升级中,固件传输或写入中断可能导致设备变砖。本文基于STM32的双Bank启动特性,设计一种高效的失败回滚机制:利用硬件双Bank交错存储与软件标志位,实现升级失败后自动回退至旧版本,并详细讲解原理、配置步骤、代码实现及验证方法,帮助开发者构建高可靠性的远程升级方案。
# STM32双Bank启动模式下OTA失败后回滚机制的设计与验证
## 1. 为什么需要回滚机制?
嵌入式设备OTA升级时,若新固件在传输、校验或写入过程中发生错误(如断电、CRC不匹配),设备可能无法启动。传统单Bank方案需外部备份或Bootloader辅助,复杂且易失败。STM32的**双Bank Flash**(如STM32F7、L4、H7系列)提供硬件级支持:Flash分为两个独立Bank,可交错存储两个固件版本,配合启动配置实现原子切换,从而优雅地实现失败回滚。
## 2. 双Bank启动原理
- **硬件结构**:Flash划分为Bank0和Bank1,每个Bank可独立擦写。通过设置选项字节(Option Bytes)中的`nBOOT1`和`BOOT0`引脚,或使用`SYSCFG_MEMRMP`寄存器,可控制从哪个Bank启动。
- **软件切换**:在运行时,通过写`FLASH_CR`的`BOR_LEV`或使用`FLASH_OB_Program`修改启动配置,但更常用的是**在Bootloader中根据标志位跳转**。
- **关键点**:双Bank模式下,两个Bank的地址映射不同(如Bank0从0x08000000,Bank1从0x08040000),但CPU可通过重映射(Memory Remap)将任一Bank映射到0x08000000。
## 3. 回滚机制设计
### 3.1 总体架构
- **Bootloader**(位于Bank0起始区域,固定不变):负责启动引导、固件校验、回滚决策。
- **App0**(旧版本,位于Bank0剩余区域)和**App1**(新版本,位于Bank1)交替存放。
- **状态标志**:在Flash末尾或独立扇区存储升级状态(如`STATE_VALID`、`STATE_PENDING`、`STATE_FAILED`)。
### 3.2 升级流程
1. 新固件下载到Bank1(若当前运行App0),写入完成后置状态为`STATE_PENDING`。
2. 重启进入Bootloader,检查状态:
- 若为`STATE_PENDING`,则校验Bank1的CRC/签名。
- 校验通过:将状态改为`STATE_VALID`,并跳转到Bank1执行新固件。
- 校验失败:将状态改为`STATE_FAILED`,并跳转到Bank0的旧固件(回滚)。
3. 新固件运行后,可主动报告“升级成功”,Bootloader再清除状态标志,完成升级。
### 3.3 回滚触发条件
- 新固件校验失败(CRC、签名)。
- 新固件启动后,在预设时间内未收到“心跳”或“升级成功”确认(需看门狗配合)。
## 4. 配置步骤(以STM32F767为例)
### 4.1 内存布局
- 设置链接脚本,将Bootloader放在Bank0起始(0x08000000,大小32KB),App0放在Bank0剩余(0x08008000),App1放在Bank1(0x08040000)。
- 两个App的链接脚本中,`FLASH_ORIGIN`分别设为对应地址。
### 4.2 选项字节配置
- 使用STM32CubeProgrammer或代码设置`nBOOT1=0`,`BOOT0=0`,使Bootloader从主Flash启动。
- 在Bootloader中,通过`SYSCFG->MEMRMP`寄存器切换Bank映射:
```c
// 映射Bank1到0x08000000
SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据具体型号,可能需组合位
```
### 4.3 状态标志存储
- 在Flash末尾(如0x080FFFF0)分配一个扇区,存储状态结构体。
- 注意擦写次数,建议使用双字备份或磨损均衡。
## 5. 代码实现示例
### 5.1 Bootloader核心逻辑
```c
// 状态定义
#define STATE_EMPTY 0xFFFFFFFF
#define STATE_PENDING 0xA5A5A5A5
#define STATE_VALID 0x5A5A5A5A
#define STATE_FAILED 0x12345678
// 跳转函数
void jump_to_app(uint32_t app_addr) {
uint32_t app_sp = *(volatile uint32_t*)app_addr;
uint32_t app_pc = *(volatile uint32_t*)(app_addr + 4);
// 设置主栈指针
__set_MSP(app_sp);
// 跳转
void (*app_entry)(void) = (void (*)(void))app_pc;
app_entry();
}
int main() {
// 读取状态
uint32_t state = *(volatile uint32_t*)STATE_FLASH_ADDR;
if (state == STATE_PENDING) {
// 校验Bank1固件(示例:CRC32)
if (crc32_check(BANK1_APP_ADDR, APP_MAX_SIZE) == 0) {
// 校验通过,置为VALID
flash_write_word(STATE_FLASH_ADDR, STATE_VALID);
// 映射Bank1并跳转
SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据型号调整
jump_to_app(BANK1_APP_ADDR);
} else {
// 校验失败,置为FAILED,回滚到Bank0
flash_write_word(STATE_FLASH_ADDR, STATE_FAILED);
jump_to_app(BANK0_APP_ADDR);
}
} else if (state == STATE_VALID) {
// 正常启动Bank1
SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0;
jump_to_app(BANK1_APP_ADDR);
} else {
// 默认启动Bank0
jump_to_app(BANK0_APP_ADDR);
}
while(1);
}
```
### 5.2 新固件中的“升级成功”确认
```c
// 在App1初始化完成后,调用此函数通知Bootloader升级成功
void ota_confirm_success(void) {
// 擦除状态标志,表示升级完成
flash_erase_sector(STATE_FLASH_SECTOR);
flash_write_word(STATE_FLASH_ADDR, STATE_EMPTY);
}
```
### 5.3 看门狗配合
- 在Bootloader跳转前启动独立看门狗(IWDG),超时时间设为10秒。
- 新固件启动后,必须在超时前喂狗,否则复位回Bootloader,Bootloader检测到状态仍为`STATE_PENDING`且校验失败,则回滚。
## 6. 验证方法
- **模拟传输错误**:在升级过程中人为断电,重启后观察是否回滚到旧版本。
- **模拟校验失败**:在写入Bank1后,篡改一个字节,重启后应回滚。
- **测试看门狗**:新固件故意不喂狗,观察复位后回滚。
- **使用逻辑分析仪**:监控GPIO输出,确认Bootloader跳转路径。
## 7. 注意事项
- **Bank大小**:确保两个Bank容量足够,且App不超过Bank大小。
- **中断向量表**:App编译时需将中断向量表偏移到对应Bank地址(如`SCB->VTOR = BANK1_APP_ADDR`)。
- **Flash擦写**:在App中擦写状态标志时,注意不要擦除自身代码区域。
- **双Bank映射**:不同型号的映射位不同,务必参考参考手册。
- **状态标志可靠性**:建议使用双字备份,写入时先擦后写,并校验。
## 8. 总结
利用STM32双Bank硬件特性,结合简单的状态机,可以构建一个健壮的OTA回滚机制。本文的设计不仅避免了设备变砖,还提供了灵活的升级确认流程。实际项目中,可根据需求增加签名验证、多版本回退等增强功能。希望本文能帮助你在嵌入式OTA开发中少走弯路。