STM32 双 Bank 模式下在线升级失败后自动回滚的 Bootloader 设计要点
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式设备远程升级(OTA)中,升级失败可能导致设备变砖。利用 STM32 的双 Bank 特性,设计一个支持自动回滚的 Bootloader,可显著提升升级可靠性。本文从原理出发,讲解双 Bank 切换机制、回滚策略、关键配置步骤,并给出完整的代码示例与注意事项,帮助开发者快速实现安全升级方案。
# 引言
在嵌入式设备远程升级(OTA)中,升级失败(如断电、校验错误、写入异常)可能导致设备无法启动。传统单 Bank 方案需要外部备份或恢复模式,而 STM32 系列(如 F4、L4、H7)支持双 Bank 闪存,允许在运行一个 Bank 的同时擦写另一个 Bank,并在重启时切换。结合 Bootloader 设计,可实现升级失败后自动回滚到旧版本,极大提升可靠性。
# 双 Bank 原理与回滚机制
## 双 Bank 闪存布局
STM32 的双 Bank 模式将内部 Flash 划分为两个独立的 Bank(如 Bank1 和 Bank2),每个 Bank 可独立擦除和编程。以 STM32F4 为例,若 Flash 总容量为 1MB,则每个 Bank 为 512KB。用户可配置为单 Bank 模式(默认)或双 Bank 模式,通过选项字节(Option Bytes)设置。
- **Bank1**:通常存放 Bootloader 和当前运行的应用(App1)。
- **Bank2**:存放待升级的新应用(App2)或备份旧应用。
## 回滚策略
设计目标:升级失败时,Bootloader 能自动回滚到上一个正常运行的版本。常见策略:
1. **双副本交替**:Bootloader 始终位于固定区域(如 Bank1 起始地址),App1 和 App2 分别位于两个 Bank 的剩余空间。每次升级将新固件写入非活动 Bank,校验通过后切换启动地址;若校验失败,则继续启动旧 Bank。
2. **版本标记**:在 Flash 的固定区域(如最后几个扇区)存储版本号和状态标志(如升级中、升级成功、升级失败)。Bootloader 根据标志决定启动哪个 Bank。
本文采用策略 1,结合状态标志实现回滚。
# 关键配置步骤
## 1. 启用双 Bank 模式
通过修改选项字节启用双 Bank。可使用 STM32CubeProgrammer 或在代码中通过 Flash 接口操作。以 STM32F4 为例,设置选项字节的 `DBANK` 位为 `1`。
```c
// 启用双 Bank 模式(需在系统复位后生效)
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
ob.Banks = FLASH_BANK_1; // 或 FLASH_BANK_BOTH
ob.OptionType = OPTIONBYTE_BANK;
ob.BankConfig = FLASH_BANK_1 | FLASH_BANK_2; // 双 Bank
HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Launch(); // 触发复位
```
注意:修改选项字节会擦除 Flash,需在开发阶段完成,量产时通常已配置好。
## 2. 内存映射规划
假设 Flash 总容量 1MB,双 Bank 后每个 Bank 512KB。规划如下:
- **Bank1**:
- 0x08000000:Bootloader(32KB,占用 0x08000000-0x08007FFF)
- 0x08008000:App1(480KB,占用 0x08008000-0x0807FFFF)
- **Bank2**:
- 0x08080000:App2(512KB,占用 0x08080000-0x080FFFFF)
注意:Bootloader 必须位于 Bank1 起始地址,因为复位向量固定指向 0x08000000。
## 3. 应用代码的链接脚本调整
App1 和 App2 需使用不同的链接脚本,设置各自的起始地址和大小。例如,App1 的 `FLASH_ORIGIN` 为 `0x08008000`,App2 为 `0x08080000`。同时,在应用代码中需配置中断向量表偏移:
```c
// 在 main 函数开头设置向量表偏移
SCB->VTOR = FLASH_BASE | APP_START_ADDR;
```
其中 `APP_START_ADDR` 为当前应用的起始地址(如 App1 为 0x08008000)。
## 4. Bootloader 设计要点
Bootloader 负责启动流程和升级逻辑。核心流程:
1. 检查升级标志(如外部引脚、通信命令)。
2. 若有升级请求,接收新固件并写入非活动 Bank。
3. 写入完成后,校验 CRC 或哈希。
4. 校验通过,更新状态标志,跳转到新 Bank;否则,回滚到旧 Bank。
5. 若无升级请求,直接跳转到当前有效 Bank。
# 完整代码示例
以下为 Bootloader 的核心代码(基于 STM32 HAL 库),演示双 Bank 切换和回滚逻辑。
```c
// 定义 Bank 地址和大小
#define BOOTLOADER_SIZE (32 * 1024)
#define APP1_ADDR (FLASH_BASE + BOOTLOADER_SIZE) // 0x08008000
#define APP2_ADDR (FLASH_BASE + 512 * 1024) // 0x08080000
#define APP_MAX_SIZE (480 * 1024)
// 状态标志存储地址(使用最后一个扇区)
#define STATUS_ADDR (FLASH_BASE + 1024 * 1024 - 4) // 假设 1MB Flash
// 状态值定义
#define STATUS_APP1_VALID 0xA5A5A5A5
#define STATUS_APP2_VALID 0x5A5A5A5A
#define STATUS_UPGRADING 0xDEADBEEF
// 函数声明
void JumpToApp(uint32_t app_addr);
int CheckAppValid(uint32_t app_addr);
void WriteStatus(uint32_t status);
int main(void)
{
HAL_Init();
// 初始化时钟、串口等...
uint32_t status = *(volatile uint32_t *)STATUS_ADDR;
uint32_t active_app = APP1_ADDR;
// 检查是否有升级请求(例如通过串口命令)
if (CheckUpgradeRequest()) {
// 升级流程:写入非活动 Bank
uint32_t target_bank = (status == STATUS_APP1_VALID) ? APP2_ADDR : APP1_ADDR;
WriteStatus(STATUS_UPGRADING); // 标记升级中
if (ReceiveAndWriteFirmware(target_bank)) {
// 校验新固件
if (CheckAppValid(target_bank)) {
// 更新状态标志
WriteStatus((target_bank == APP2_ADDR) ? STATUS_APP2_VALID : STATUS_APP1_VALID);
active_app = target_bank;
} else {
// 校验失败,保持旧状态,自动回滚
active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR;
}
} else {
// 写入失败,回滚
active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR;
}
} else {
// 无升级请求,根据状态选择启动 Bank
if (status == STATUS_APP2_VALID) {
active_app = APP2_ADDR;
} else {
active_app = APP1_ADDR; // 默认 App1
}
}
// 跳转到应用
JumpToApp(active_app);
while (1);
}
// 跳转到指定地址的应用
void JumpToApp(uint32_t app_addr)
{
// 检查栈顶地址是否合法
uint32_t msp = *(volatile uint32_t *)app_addr;
if ((msp & 0xFFF00000) != 0x20000000) {
// 栈指针无效,进入错误处理
ErrorHandler();
}
// 设置主栈指针
__set_MSP(msp);
// 设置向量表偏移
SCB->VTOR = app_addr;
// 获取复位向量并跳转
void (*reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));
reset_handler();
}
// 检查应用是否有效(通过 CRC 或固定标志)
int CheckAppValid(uint32_t app_addr)
{
// 简单检查:读取应用头部固定偏移处的魔数
uint32_t magic = *(volatile uint32_t *)(app_addr + 0x1FC); // 假设在应用头部写入魔数
return (magic == 0xDEADBEEF); // 实际应使用 CRC
}
// 写入状态标志
void WriteStatus(uint32_t status)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase;
uint32_t page_error = 0;
erase.TypeErase = FLASH_TYPEERASE_PAGES;
erase.PageAddress = STATUS_ADDR;
erase.NbPages = 1;
HAL_FLASHEx_Erase(&erase, &page_error);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, STATUS_ADDR, status);
HAL_FLASH_Lock();
}
```
# 注意事项
- **选项字节修改**:启用双 Bank 会改变 Flash 布局,务必在开发阶段完成,并确保 Bootloader 和应用链接脚本匹配。
- **中断向量表**:应用必须设置 `SCB->VTOR`,否则中断会跳转到 Bootloader 的向量表。
- **升级过程中的掉电保护**:建议在升级前备份旧版本状态,升级中写入状态标志,以便重启后识别。
- **校验机制**:使用 CRC32 或 SHA256 对整个固件进行校验,避免仅依赖头部魔数。
- **看门狗**:在升级过程中启用独立看门狗(IWDG),防止卡死。
- **双 Bank 切换**:部分 STM32 支持硬件切换(如通过 SYSCFG 寄存器),但本文采用软件跳转方式,更通用。
- **测试**:务必在真机测试断电、写入错误等场景,确保回滚逻辑可靠。
# 总结
利用 STM32 双 Bank 特性,结合状态标志和校验机制,可以设计出高可靠的 OTA 升级方案。Bootloader 在升级失败时自动回滚到旧版本,避免设备变砖。本文提供了核心设计思路和代码示例,开发者可根据具体芯片型号调整内存布局和细节。