STM32 双Bank切换实现OTA零停机:基于F4/F7的Bank交错映射实战
1. 双Bank架构原理
STM32 F4/F7系列(如STM32F427、STM32F746)的Flash被划分为两个独立的Bank,每个Bank大小相等(例如1MB Flash分为两个512KB Bank)。这种设计允许在运行一个Bank时,对另一个Bank进行擦写操作,从而实现OTA升级不中断当前应用。
关键特性:
- Bank交错映射:通过配置选项字节(Option Bytes)中的BOR_LEV和DBANK位,可以切换Flash的映射方式。默认情况下,Flash从地址0x08000000开始连续映射;启用双Bank后,地址空间被交错映射,使得两个Bank的起始地址均为0x08000000,但通过高位地址选择Bank。
- 无缝切换:升级完成后,通过软件复位或跳转指令,将PC指针切换到新Bank的复位向量,实现快速切换,无需外部干预。
2. 硬件与软件准备
- 硬件:STM32F4/F7开发板(如Nucleo-F746ZG),外部Flash可选(用于备份固件)。
- 软件:STM32CubeIDE(或Keil MDK),STM32 HAL库。
3. 配置步骤
3.1 启用双Bank模式
- 在STM32CubeMX中,选择目标芯片,进入
Option Bytes配置。 - 设置
DBANK位为1(双Bank模式),并配置BOR_LEV为合适的电平(如3.3V)。 - 生成初始化代码。
3.2 链接脚本调整
修改链接脚本(.ld文件),将Flash区域划分为两个Bank:
/* 假设Flash总大小1MB,每个Bank 512KB */
MEMORY
{
FLASH_BANK0 (rx) : ORIGIN = 0x08000000, LENGTH = 512K
FLASH_BANK1 (rx) : ORIGIN = 0x08080000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 256K
}
3.3 编写双Bank切换代码
#include "stm32f7xx_hal.h"
// 定义Bank地址
#define BANK0_START 0x08000000
#define BANK1_START 0x08080000
// 当前运行Bank标志
extern uint32_t _estack; // 栈顶地址
void JumpToBank(uint32_t bank_addr) {
// 检查栈顶地址是否合法
if ((*(volatile uint32_t*)bank_addr & 0x2FFE0000) != 0x20000000) {
return;
}
// 设置主栈指针
__set_MSP(*(volatile uint32_t*)bank_addr);
// 获取复位向量地址
uint32_t reset_vector = *(volatile uint32_t*)(bank_addr + 4);
// 跳转
void (*reset_handler)(void) = (void (*)(void))reset_vector;
reset_handler();
}
void OTA_Update(void) {
// 假设新固件已写入非活动Bank(例如当前运行Bank0,则写入Bank1)
uint32_t target_bank = (GetCurrentBank() == BANK0_START) ? BANK1_START : BANK0_START;
// 擦除目标Bank(示例:擦除Bank1)
FLASH_EraseInitTypeDef erase_init;
erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
erase_init.Banks = FLASH_BANK_1;
erase_init.Sector = 0;
erase_init.NbSectors = 8; // 根据实际扇区数
erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t sector_error = 0;
HAL_FLASHEx_Erase(&erase_init, §or_error);
// 写入固件(此处省略具体写入逻辑,通常从外部存储或网络读取)
// WriteFirmwareToBank(target_bank);
// 设置标志位,表示新固件就绪
SetUpdateFlag(target_bank);
// 软件复位
NVIC_SystemReset();
}
uint32_t GetCurrentBank(void) {
// 通过PC指针判断当前运行Bank
uint32_t pc = (uint32_t)GetPC();
if (pc >= BANK0_START && pc < BANK0_START + 0x80000) {
return BANK0_START;
} else {
return BANK1_START;
}
}
// 获取PC指针(内联汇编)
__attribute__((always_inline)) static inline uint32_t GetPC(void) {
uint32_t pc;
__asm volatile ("mov %0, pc" : "=r" (pc));
return pc;
}
3.4 启动代码修改
在系统启动时,检查更新标志,决定跳转到哪个Bank:
void SystemInit(void) {
// ... 其他初始化
if (CheckUpdateFlag() == BANK1_START) {
JumpToBank(BANK1_START);
}
// 否则继续运行Bank0
}
4. 完整示例:双Bank升级流程
以下是一个简化的升级流程示例,展示如何从Bank0切换到Bank1:
int main(void) {
HAL_Init();
SystemClock_Config();
// 检查是否有待升级固件
if (CheckUpdateFlag() == BANK1_START) {
// 验证固件完整性(如CRC)
if (VerifyFirmware(BANK1_START)) {
// 清除标志
ClearUpdateFlag();
// 跳转到Bank1
JumpToBank(BANK1_START);
} else {
// 固件无效,回滚到Bank0
JumpToBank(BANK0_START);
}
}
// 正常运行当前Bank应用
while (1) {
// 应用逻辑
}
}
5. 注意事项
- 地址映射:启用双Bank后,Flash地址映射发生变化,务必确保链接脚本和代码中的地址正确。
- 中断向量表:每个Bank的应用必须有自己的中断向量表,并在启动时重定位到对应Bank的起始地址。
- Flash擦写时间:虽然双Bank允许并行操作,但擦写期间CPU仍可能被阻塞(取决于Flash接口),建议使用中断或DMA优化。
- 固件验证:切换前必须验证新固件的完整性(如CRC、签名),避免启动失败。
- 回滚机制:设计回滚逻辑,若新固件启动失败,自动回退到旧Bank。
- 电源稳定性:升级过程中确保电源稳定,防止Flash写入失败。
6. 总结
通过STM32双Bank架构,我们实现了OTA升级的零停机切换。核心在于利用Bank交错映射,使两个应用独立运行,并通过跳转实现快速切换。本文提供了原理、配置、代码和注意事项,为高可靠性嵌入式系统提供了实用方案。实际项目中,还需结合具体硬件和需求优化细节。