STM32 双Bank切换实现OTA零停机:基于Flash双区映射的实战踩坑记录
👁 2 阅读 · 2026-08-27 · 嵌入式
在工业物联网设备中,OTA升级往往面临停机风险。本文深入探讨STM32双Bank切换技术,通过Flash双区映射实现无缝升级,避免系统重启。文章从原理剖析到寄存器配置,再到完整代码示例,并分享实战中的关键踩坑点,帮助开发者规避常见陷阱,确保升级可靠性。
# STM32 双Bank切换实现OTA零停机:基于Flash双区映射的实战踩坑记录
## 引言
在嵌入式系统开发中,OTA(Over-The-Air)升级是常见需求,但传统升级方式通常需要设备重启,导致服务中断。对于要求高可用性的工业设备,零停机升级至关重要。STM32系列(如F4、H7等)支持双Bank Flash架构,允许在运行一个Bank的同时擦写另一个Bank,并通过硬件切换实现无缝跳转。本文将分享基于双Bank切换的OTA实现方案,并总结实战中的关键问题。
## 双Bank原理与硬件支持
### 什么是双Bank Flash?
STM32的部分型号(如STM32F427、STM32H743)将内部Flash划分为两个独立的Bank(Bank1和Bank2),每个Bank可独立擦写。当系统从Bank1启动时,可以擦写Bank2;反之亦然。通过设置选项字节(Option Bytes)中的`BOR_LEV`和`nBOOT1`位,或使用软件触发`FLASH_CR`寄存器中的`SW_BANK`位,可以切换启动Bank。
### 零停机实现机制
零停机的关键在于:
- 当前运行在Bank1,升级程序写入Bank2;
- 写入完成后,设置跳转标志,然后软复位;
- Bootloader检测到标志,切换Bank启动新程序;
- 新程序运行后,可擦写旧Bank,准备下一次升级。
整个过程仅在复位瞬间有短暂中断(毫秒级),但避免了长时间停机。
## 硬件与软件配置
### 硬件要求
- STM32F4xx/H7xx系列,具有双Bank Flash;
- 外部Flash(可选)用于存储升级包;
- 串口或网络接口用于接收固件。
### 软件架构
- Bootloader:负责启动和Bank切换;
- App1:运行在Bank1;
- App2:运行在Bank2。
## 关键寄存器与配置步骤
### 1. 使能双Bank模式
在STM32F4中,双Bank模式通过选项字节配置。使用ST-Link或代码设置:
```c
// 解锁Flash选项字节
FLASH_OB_Unlock();
// 设置双Bank模式(以F427为例)
FLASH_OB_Program(OB_DUAL_BANK, OB_DUAL_BANK_ENABLE);
// 重新加载选项字节
FLASH_OB_Launch();
```
注意:修改选项字节会导致系统复位,因此需在Bootloader中执行。
### 2. 编写Bootloader
Bootloader负责检查升级标志和启动Bank。
```c
// Bootloader main
void bootloader_main(void) {
// 检查升级标志(存储在备份寄存器或Flash末尾)
if (check_update_flag()) {
// 切换Bank
if (current_bank() == BANK1) {
set_boot_bank(BANK2);
} else {
set_boot_bank(BANK1);
}
}
// 跳转到App
jump_to_app();
}
// 切换Bank函数
void set_boot_bank(uint8_t bank) {
FLASH_OB_Unlock();
if (bank == BANK2) {
FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK2);
} else {
FLASH_OB_Program(OB_BOOT_BANK, OB_BOOT_BANK1);
}
FLASH_OB_Launch();
}
```
### 3. 编写App升级逻辑
App运行期间,接收新固件并写入非活动Bank。
```c
// App中升级函数
void ota_update(uint8_t *data, uint32_t len) {
uint32_t target_bank = (current_bank() == BANK1) ? BANK2_BASE : BANK1_BASE;
// 擦写目标Bank
flash_erase(target_bank);
// 写入数据
flash_write(target_bank, data, len);
// 设置升级标志
set_update_flag();
// 软复位
NVIC_SystemReset();
}
```
## 完整代码示例
以下是一个简化的Bootloader和App协作示例。
### Bootloader代码(main.c)
```c
#include "stm32f4xx.h"
#define APP1_ADDR 0x08008000 // Bank1 App起始地址
#define APP2_ADDR 0x08088000 // Bank2 App起始地址(假设Bank1为1MB,Bank2为1MB)
void jump_to_app(uint32_t app_addr) {
uint32_t app_stack = *(volatile uint32_t*)app_addr;
void (*app_main)(void) = (void (*)(void))(*(volatile uint32_t*)(app_addr+4));
__set_MSP(app_stack);
app_main();
}
int main(void) {
// 初始化时钟等
// 检查升级标志(使用备份寄存器)
if (RTC->BKP0R & 0x01) {
// 清除标志
RTC->BKP0R &= ~0x01;
// 切换Bank
if (FLASH->OPTR & (1<<31)) { // 当前从Bank2启动
// 设置启动Bank1
FLASH_OB_Unlock();
FLASH->OPTR &= ~(1<<31);
FLASH_OB_Launch();
} else {
FLASH_OB_Unlock();
FLASH->OPTR |= (1<<31);
FLASH_OB_Launch();
}
}
// 判断当前启动Bank
if (FLASH->OPTR & (1<<31)) {
jump_to_app(APP2_ADDR);
} else {
jump_to_app(APP1_ADDR);
}
while(1);
}
```
### App升级代码(ota.c)
```c
#include "stm32f4xx.h"
#define BANK1_BASE 0x08000000
#define BANK2_BASE 0x08080000
#define FLASH_SIZE 0x100000 // 1MB
uint32_t current_bank(void) {
if (FLASH->OPTR & (1<<31)) {
return 2;
} else {
return 1;
}
}
void flash_erase_bank(uint32_t bank_base) {
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
uint32_t addr = bank_base;
while (addr < bank_base + FLASH_SIZE/2) {
if (FLASH_EraseSector(addr, VoltageRange_3) != FLASH_COMPLETE) {
// 错误处理
}
addr += 0x20000; // 扇区大小,根据型号调整
}
FLASH_Lock();
}
void flash_write_bank(uint32_t bank_base, uint8_t *data, uint32_t len) {
FLASH_Unlock();
uint32_t addr = bank_base;
uint32_t idx = 0;
while (idx < len) {
uint32_t word = 0;
for (int i=0; i<4; i++) {
if (idx < len) {
word |= (data[idx] << (8*i));
idx++;
}
}
FLASH_ProgramWord(addr, word);
addr += 4;
}
FLASH_Lock();
}
void ota_start(uint8_t *fw_data, uint32_t fw_len) {
uint32_t target_bank = (current_bank() == 1) ? BANK2_BASE : BANK1_BASE;
flash_erase_bank(target_bank);
flash_write_bank(target_bank, fw_data, fw_len);
// 设置升级标志
RTC->BKP0R |= 0x01;
NVIC_SystemReset();
}
```
## 实战踩坑记录
### 坑1:选项字节修改导致意外复位
修改`FLASH_OB_Launch()`会立即复位系统,如果未保存上下文,可能导致数据丢失。解决方案:在修改前将关键数据存入备份寄存器或Flash末尾。
### 坑2:Bank大小与扇区映射
不同型号的Bank大小和扇区布局不同。例如,STM32F427的Bank1为1MB,Bank2为1MB,但扇区大小不一(前4个扇区16KB,后续64KB)。擦写时必须按扇区对齐,否则会擦除错误区域。
### 坑3:中断向量表重定位
App编译时需设置`VECT_TAB_OFFSET`,并在启动时重定位中断向量表。
```c
// App中
SCB->VTOR = APP1_ADDR; // 或APP2_ADDR
```
否则中断无法正确响应。
### 坑4:Flash写入时禁止中断
Flash编程期间,如果发生中断,可能导致写入失败或死锁。建议在写入前关闭中断,或使用DMA传输。
### 坑5:升级包校验
在写入前必须校验固件完整性(如CRC32),否则写入损坏数据会导致设备变砖。建议在Bootloader中增加校验步骤。
## 总结
双Bank切换技术为STM32提供了高效的OTA零停机方案,但实现时需注意选项字节配置、Bank映射、中断向量重定位等细节。通过合理的Bootloader设计,可以确保升级的可靠性和安全性。希望本文的实战经验能帮助开发者少走弯路。
## 参考资料
- STM32F4xx参考手册(RM0090)
- AN3969: STM32F4 in-application programming
- AN4657: STM32 dual-bank bootloader