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 升级流程
- 新固件下载到Bank1(若当前运行App0),写入完成后置状态为
STATE_PENDING。 - 重启进入Bootloader,检查状态:
- 若为
STATE_PENDING,则校验Bank1的CRC/签名。 - 校验通过:将状态改为
STATE_VALID,并跳转到Bank1执行新固件。 - 校验失败:将状态改为
STATE_FAILED,并跳转到Bank0的旧固件(回滚)。
- 若为
- 新固件运行后,可主动报告“升级成功”,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映射:// 映射Bank1到0x08000000 SYSCFG->MEMRMP |= SYSCFG_MEMRMP_MEM_MODE_0; // 根据具体型号,可能需组合位
4.3 状态标志存储
- 在Flash末尾(如0x080FFFF0)分配一个扇区,存储状态结构体。
- 注意擦写次数,建议使用双字备份或磨损均衡。
5. 代码实现示例
5.1 Bootloader核心逻辑
// 状态定义
#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 新固件中的“升级成功”确认
// 在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开发中少走弯路。