STM32双Bank切换实现OTA零停机:基于Flash双区映射的细节与陷阱
一、为什么需要双Bank?
传统OTA方案通常采用A/B分区:运行在A区,升级写入B区,完成后通过软件跳转或复位切换。但复位期间存在数百毫秒停机,且若切换逻辑出错,设备可能变砖。STM32的双Bank特性(如STM32H743、L4R9、G474)在硬件层面提供两个独立的Flash Bank(Bank1和Bank2),每个Bank可独立擦写,且支持硬件Bank切换——通过修改选项字节或寄存器,让CPU从当前Bank无缝映射到另一个Bank的同一地址,无需复位,实现真正的零停机升级。
二、双Bank映射原理
2.1 物理与逻辑地址
以STM32H743为例,Flash总容量2MB,分为Bank1(0x08000000-0x080FFFFF)和Bank2(0x08100000-0x081FFFFF)。默认情况下,CPU从0x08000000启动,即Bank1。当启用双Bank模式并配置为Bank切换后,硬件会将Bank2映射到0x08000000,而Bank1映射到0x08100000。这样,应用程序无需修改链接地址,只需切换Bank即可运行新固件。
2.2 切换机制
切换通过修改Flash选项字节中的DBANK位和SWAP_BANK位实现。DBANK决定Flash是单Bank还是双Bank模式,SWAP_BANK决定是否交换Bank映射。切换操作需要解锁Flash,写入选项字节,然后触发系统复位(或使用特殊寄存器实现软切换)。但注意:复位会中断执行,因此真正的零停机需要利用STM32的实时Bank切换特性(部分系列支持),通过FLASH_CR寄存器的SWAP_BANK位,在无复位情况下切换映射。
三、配置步骤
3.1 硬件与软件准备
- 硬件:STM32H743 Nucleo板(或任何双Bank系列)
- 软件:STM32CubeIDE + HAL库
- 固件:两个测试固件(v1.0和v2.0),编译时链接地址均为0x08000000,但分别烧录到Bank1和Bank2。
3.2 启用双Bank模式
首次烧录前,需通过选项字节启用双Bank。使用STM32CubeProgrammer或代码设置:
void Flash_Enable_DualBank(void) {
FLASH_OBProgramInitTypeDef ob;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
ob.OptionType = OPTIONBYTE_BANK;
ob.Banks = FLASH_BANK_1; // 配置Bank1选项
ob.BankConfig = FLASH_BANK_BOTH; // 双Bank模式
HAL_FLASH_OB_Program(&ob);
HAL_FLASH_OB_Launch(); // 触发选项字节加载,会复位
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
}
注意:此操作会复位,因此应在首次烧录时执行。
3.3 固件升级流程
假设当前运行在Bank1(地址0x08000000),新固件已写入Bank2(地址0x08100000)。升级流程如下:
- 接收固件并写入Bank2:通过UART/网络接收数据,直接写入Bank2地址。
- 验证固件:计算CRC或哈希,确保数据完整。
-
执行Bank切换:设置
SWAP_BANK位,实现映射交换。
void Jump_To_Bank2(void) {
// 确保Bank2固件有效(检查栈顶地址)
uint32_t stack_addr = *(volatile uint32_t *)0x08100000;
if ((stack_addr & 0xFFF00000) != 0x20000000) return; // 栈顶应在RAM区
// 解锁Flash
HAL_FLASH_Unlock();
// 设置SWAP_BANK位(以H7为例)
FLASH->CR |= FLASH_CR_SWAP_BANK;
// 等待操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 注意:H7需要设置SWAP_BANK_IE?实际无需复位,但需确保没有正在执行的代码在Bank1中。
// 切换后,PC指针仍指向旧地址,但映射已交换,因此需要跳转到新固件入口。
// 实际上,切换后0x08000000指向Bank2,因此直接软复位或跳转即可。
NVIC_SystemReset(); // 为简化,使用复位,但会停机。若需零停机,见下文。
}
3.4 实现真正的零停机切换
零停机要求切换期间CPU不中断。STM32H7的实时切换需要满足:
- 代码必须运行在RAM中(因为切换时Flash映射变化,若代码在Flash中执行会取指失败)。
- 切换函数需放在RAM中执行。
// 将切换函数放到RAM中
__attribute__((section(".ramfunc"))) void Bank_Switch_ZeroDowntime(void) {
// 关闭中断
__disable_irq();
// 设置SWAP_BANK位
FLASH->CR |= FLASH_CR_SWAP_BANK;
while (FLASH->SR & FLASH_SR_BSY);
// 切换后,0x08000000映射到Bank2,但PC仍指向旧地址(现在对应Bank2的相同偏移)。
// 因此,需要跳转到新固件的复位向量。
uint32_t jump_addr = *(volatile uint32_t *)(0x08000004); // 新复位向量
// 设置主栈指针
__set_MSP(*(volatile uint32_t *)0x08000000);
// 跳转
void (*jump)(void) = (void (*)(void))jump_addr;
jump();
// 不会返回
}
注意:此代码需在RAM中执行,且关闭中断,避免切换过程中断。
四、完整代码示例
以下是一个简化但完整的双Bank升级示例,包含写入、验证和切换。
// main.c
#include "stm32h7xx_hal.h"
#include <string.h>
#define BANK2_BASE 0x08100000
#define APP_SIZE 0x100000 // 1MB,根据实际调整
void Flash_Write_Bank2(uint8_t *data, uint32_t len) {
HAL_FLASH_Unlock();
uint32_t addr = BANK2_BASE;
for (uint32_t i = 0; i < len; i += 8) {
uint64_t val = 0;
memcpy(&val, data + i, 8);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, val);
}
HAL_FLASH_Lock();
}
uint32_t Calc_CRC(uint8_t *data, uint32_t len) {
// 使用硬件CRC或软件实现
return 0; // 占位
}
void OTA_Upgrade(void) {
// 假设已通过通信接收固件到RAM缓冲区
extern uint8_t new_fw[];
extern uint32_t new_fw_len;
// 擦除Bank2(需先擦除再写)
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = FLASH_BANK_2;
erase.Sector = 0;
erase.NbSectors = 8; // 根据Bank2扇区数
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t error;
HAL_FLASHEx_Erase(&erase, &error);
HAL_FLASH_Lock();
// 写入新固件
Flash_Write_Bank2(new_fw, new_fw_len);
// 验证CRC
if (Calc_CRC((uint8_t*)BANK2_BASE, new_fw_len) != expected_crc) {
Error_Handler();
}
// 切换Bank(零停机)
Bank_Switch_ZeroDowntime();
}
// 链接脚本中需将Bank_Switch_ZeroDowntime放到RAM
五、关键陷阱与注意事项
5.1 地址重映射与中断向量
- 切换后,新固件的中断向量表必须位于0x08000000(因为映射后Bank2在此地址)。因此,新固件编译时需设置
VECT_TAB_OFFSET=0,且向量表首地址为0x08000000。 - 若使用RTOS或自定义向量表,需在启动时重新设置
SCB->VTOR。
5.2 擦写时序与双Bank并发
- 双Bank模式下,两个Bank可以独立擦写,但不能同时擦写同一Bank的不同扇区。
- 擦除操作会阻塞CPU,若在实时系统中,需考虑调度。建议在空闲时间或使用DMA进行擦写(部分系列支持)。
5.3 切换时的代码执行位置
- 零停机切换要求切换代码在RAM中执行,否则切换瞬间Flash映射变化会导致取指失败。
- 确保切换函数及其调用的所有函数(如HAL库函数)都在RAM中,或使用
__attribute__((ramfunc))。
5.4 选项字节与复位行为
- 修改选项字节后,必须调用
HAL_FLASH_OB_Launch(),这会触发一次系统复位。若不想复位,可使用FLASH_CR的SWAP_BANK位(仅部分系列支持)。 - 某些系列(如F4)不支持实时切换,只能通过选项字节+复位,此时无法零停机。
5.5 固件有效性验证
- 切换前必须验证新固件的栈顶指针和复位向量是否合法,否则跳转会HardFault。
- 建议在Bank2的头部存储固件版本、CRC、长度等信息,便于管理。
5.6 电源与时钟稳定性
- 擦写Flash时,若电源波动可能导致烧写失败。建议使用内部稳压器或确保电源稳定。
- 切换期间关闭所有外设中断,避免干扰。
六、总结
双Bank切换技术为OTA升级提供了硬件级支持,通过合理设计可实现零停机更新。但需注意:并非所有STM32系列都支持实时切换,需查阅参考手册;切换代码必须驻留RAM;固件验证不可省略。掌握这些细节,你的设备将具备更可靠的远程升级能力。