STM32双Bank Flash在OTA中的无缝回滚设计与实现
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式OTA升级中,固件损坏或升级失败可能导致设备变砖。STM32的双Bank Flash架构通过硬件层面的Bank切换,实现了原子性的固件更新与无缝回滚。本文深入解析双Bank原理,并给出基于STM32H7的完整实现代码,涵盖配置、跳转、回滚策略及注意事项,助你构建高可靠的OTA系统。
# STM32双Bank Flash在OTA中的无缝回滚设计与实现
## 1. 为什么需要双Bank?
传统OTA方案通常采用单Bank + 外部备份区,升级时先擦写备份区,再复制到主区。但复制过程存在窗口期,一旦断电或出错,主区固件可能不完整,导致设备无法启动。而STM32的双Bank Flash(如STM32H7、L4+、F7等系列)将Flash分为两个独立的Bank(Bank0和Bank1),每个Bank可独立擦写,且硬件支持从任一Bank启动。升级时直接写入非活动Bank,完成后通过切换启动Bank实现原子性切换,无需复制,极大降低风险。
## 2. 双Bank原理与硬件支持
- **Bank划分**:以STM32H743为例,Flash共2MB,分为Bank0(0x08000000-0x080FFFFF)和Bank1(0x08100000-0x081FFFFF),每个Bank大小1MB。
- **启动配置**:通过选项字节(Option Bytes)中的`BOOT_ADD0`和`BOOT_ADD1`设置启动地址。当`BOOT_SW`位为0时,始终从Bank0启动;为1时,可通过软件切换启动Bank。
- **硬件切换**:写`FLASH_CR`寄存器的`SWAP_BANK`位,触发Bank交换,硬件自动将非活动Bank映射到地址0x08000000,无需搬移代码。
- **关键优势**:切换操作是原子的,即使切换后新固件崩溃,也可通过硬件看门狗或外部触发再次切换回旧Bank,实现无缝回滚。
## 3. 系统架构设计
本设计基于STM32H743,使用两个Bank各存储一份固件,Bootloader位于Bank0起始区域,App1位于Bank0后半部分,App2位于Bank1。实际项目中可灵活划分,但需保证每个Bank的App起始地址对齐(如0x20000对齐)。
- **Bootloader**:负责检查固件有效性,决定启动哪个Bank,并处理升级请求。
- **App1/App2**:功能相同的应用固件,编译时通过链接脚本指定不同的Flash起始地址。
- **升级流程**:运行中的App接收新固件包,写入非活动Bank,校验通过后设置标志位并复位,Bootloader根据标志切换Bank启动新固件。
## 4. 实现步骤
### 4.1 配置Flash双Bank模式
默认情况下,STM32H7可能处于单Bank模式(2MB合并)。需通过选项字节启用双Bank。在CubeMX中或代码中操作:
```c
// 启用双Bank模式(需在解锁Flash后执行)
void Flash_EnableDualBank(void) {
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
if (ob.Banks == FLASH_BANK_BOTH) { // 当前为单Bank
ob.Banks = FLASH_BANK_BOTH;
ob.USERType = FLASH_OB_USER_BANK_SWAP;
ob.USERConfig = FLASH_OB_USER_BANK_SWAP_ENABLE;
HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Launch(); // 触发重新加载选项字节
}
}
```
### 4.2 固件写入非活动Bank
在App中,根据当前运行Bank确定目标Bank地址。例如,若运行在Bank0(基址0x08000000),则目标Bank1基址为0x08100000。写入时需按扇区擦除,注意H7的Flash扇区大小(128KB)。
```c
#define BANK0_BASE 0x08000000
#define BANK1_BASE 0x08100000
#define APP_SIZE 0x80000 // 假设App最大512KB
uint32_t GetTargetBankBase(void) {
// 通过读取VTOR或链接变量判断当前运行位置
if ((uint32_t)&_vectors >= BANK0_BASE && (uint32_t)&_vectors < BANK0_BASE + APP_SIZE)
return BANK1_BASE;
else
return BANK0_BASE;
}
// 写入固件包(分包写入,每包1KB)
void WriteFirmware(uint32_t offset, uint8_t *data, uint32_t len) {
uint32_t target = GetTargetBankBase() + offset;
// 擦除对应扇区(需先计算扇区号)
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = GetSector(target);
erase.NbSectors = 1;
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 val = *(uint64_t*)(data + i);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, target + i, val);
}
}
```
### 4.3 固件校验与切换
写入完成后,计算整个固件的CRC或哈希,与包头中的值比对。校验通过后,设置一个标志位(如备份区或Flash末尾的专用区域),然后复位。Bootloader启动时读取该标志。
```c
// 设置升级标志(存储于备份寄存器或专用Flash页)
void SetUpgradeFlag(uint8_t bank) {
// 示例:使用RTC备份寄存器
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, bank);
}
// 执行切换
void PerformSwap(void) {
HAL_FLASH_Unlock();
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
// 设置SWAP_BANK位
FLASH->CR |= FLASH_CR_SWAP_BANK;
while (FLASH->CR & FLASH_CR_SWAP_BANK); // 等待完成
HAL_FLASH_Lock();
NVIC_SystemReset();
}
```
### 4.4 Bootloader启动逻辑
Bootloader在启动时检查标志位,决定从哪个Bank启动。若新固件运行失败,可通过看门狗复位,Bootloader检测到运行次数超限,自动回滚。
```c
void Bootloader_JumpToApp(void) {
uint8_t bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
uint32_t app_addr = (bank == 1) ? BANK1_BASE : BANK0_BASE;
// 检查App有效性(栈顶地址合法且校验通过)
if (IsAppValid(app_addr)) {
// 设置主栈指针
__set_MSP(*(uint32_t*)app_addr);
// 跳转
void (*app_entry)(void) = (void*)(*(uint32_t*)(app_addr + 4));
app_entry();
} else {
// 回滚到另一个Bank
uint32_t fallback = (bank == 1) ? BANK0_BASE : BANK1_BASE;
if (IsAppValid(fallback)) {
__set_MSP(*(uint32_t*)fallback);
((void(*)(void))(*(uint32_t*)(fallback + 4)))();
}
}
}
```
## 5. 完整示例代码
以下为Bootloader核心逻辑(简化版):
```c
#include "main.h"
#define APP_MAGIC 0xDEADBEEF
typedef struct {
uint32_t magic;
uint32_t crc;
uint32_t size;
} AppHeader;
int IsAppValid(uint32_t addr) {
AppHeader *hdr = (AppHeader*)addr;
if (hdr->magic != APP_MAGIC) return 0;
// 计算CRC(需实现)
return (CalcCRC(addr + sizeof(AppHeader), hdr->size) == hdr->crc);
}
void JumpToApp(uint32_t addr) {
if (IsAppValid(addr)) {
__set_MSP(*(uint32_t*)addr);
void (*entry)(void) = (void*)(*(uint32_t*)(addr + 4));
entry();
}
}
int main(void) {
HAL_Init();
// 检查升级标志
uint8_t new_bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
if (new_bank == 1) {
// 尝试启动新Bank
JumpToApp(BANK1_BASE);
// 若失败,回滚
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0);
JumpToApp(BANK0_BASE);
} else {
JumpToApp(BANK0_BASE);
// 若失败,尝试Bank1
JumpToApp(BANK1_BASE);
}
// 都失败,进入Bootloader升级模式
while(1);
}
```
## 6. 注意事项
- **链接脚本**:App1和App2的起始地址必须不同,且中断向量表需重定位(设置`SCB->VTOR`)。
- **Flash擦写时间**:H7的Flash编程以256位(32字节)为单位,擦除一个扇区约需1秒,升级时需考虑功耗和超时。
- **看门狗**:在切换后启动新App前,喂狗并设置一个“运行成功”标志,若新App未及时清除,则触发回滚。
- **选项字节操作**:修改选项字节可能导致芯片复位,需在升级流程中谨慎处理,避免频繁操作。
- **双Bank模式与读保护**:启用双Bank后,读保护等级可能影响Bank切换,需根据需求配置。
- **测试**:务必在真机上验证断电、复位等异常场景,确保回滚逻辑可靠。
## 7. 总结
双Bank Flash为OTA提供了硬件级的原子切换,配合Bootloader的校验与回滚机制,可显著提升固件升级的可靠性。本文的实现基于STM32H7,但原理适用于所有支持双Bank的STM32系列。开发者可根据实际Flash大小和App体积调整Bank划分,并加入版本管理、断点续传等增强功能。