STM32H7双Bank Flash在线升级失败后自动回滚的地址映射陷阱与规避
引言
在工业物联网和汽车电子领域,OTA(Over-The-Air)升级是标配功能。STM32H7系列(如H743、H750)提供双Bank Flash(Bank1和Bank2,各1MB),支持在升级失败时自动回滚到旧固件。然而,双Bank的地址映射机制暗藏陷阱:Bank切换后,CPU的寻址空间会动态重映射,若固件代码未正确处理,轻则回滚失败,重则系统变砖。本文基于实战经验,剖析陷阱并给出规避方案。
双Bank Flash的工作原理
STM32H7的Flash分为两个Bank,每个Bank独立可擦写。通过选项字节(Option Bytes)中的nDBANK位,可配置为单Bank(2MB连续)或双Bank模式。在双Bank模式下,两个Bank的起始地址均为0x08000000,但CPU实际访问哪个Bank由FLASH_CR寄存器的BANK_SW位控制:
-
BANK_SW=0:映射Bank1,地址0x08000000对应物理Bank1,Bank2映射到0x08100000(若使能)。 -
BANK_SW=1:映射Bank2,地址0x08000000对应物理Bank2,Bank1映射到0x08100000。
关键陷阱:切换BANK_SW后,CPU从0x08000000取指时,实际执行的是另一个Bank的代码。若固件在Bank1中运行,升级Bank2后切换Bank,CPU会跳转到Bank2的起始地址,但此时向量表(VTOR)仍指向0x08000000,而该地址已被重映射为Bank2,若Bank2的向量表未正确初始化,将导致HardFault。
自动回滚机制与陷阱
典型的双Bank OTA流程:
- 固件A在Bank1运行,升级固件B写入Bank2。
- 写入完成后,置位
BANK_SW,系统复位。 - 复位后,CPU从
0x08000000执行Bank2的固件B。 - 若固件B运行失败(如校验失败),则清除
BANK_SW,复位回滚到Bank1。
陷阱1:向量表地址未重定位
固件B在编译时,链接脚本通常将向量表放在0x08000000(或偏移地址)。但切换Bank后,0x08000000映射到Bank2,固件B的向量表确实在Bank2的起始位置,这没问题。问题是:固件B运行后,若需要跳转到Bootloader或执行回滚,Bootloader的向量表在Bank1的0x08000000,但此时该地址映射到Bank2,导致跳转失败。
陷阱2:中断服务函数地址错乱
固件B运行期间,若发生中断,CPU从VTOR指向的向量表取中断向量。若VTOR未更新,仍指向0x08000000(此时为Bank2),而固件B的中断服务函数可能链接在Bank2的绝对地址,这没问题。但若固件B调用回滚函数,清除BANK_SW后,VTOR未变,但0x08000000映射回Bank1,此时中断向量表变成Bank1的,而固件B的ISR地址可能不在Bank1,导致中断跳转错误。
陷阱3:链接脚本的地址偏移
若固件B的链接脚本将Flash起始地址设为0x08100000(Bank2的物理地址),则切换Bank后,0x08000000映射到Bank2,但固件B的向量表在0x08100000,CPU复位后从0x08000000取指,执行的是Bank2的起始内容,但该地址是Bank2的物理起始,与链接地址不符,导致取指错误。
规避方案:基于向量表重定位与标志位
核心思路
- 两个Bank的固件使用相同的链接地址
0x08000000,但通过BANK_SW切换映射。 - 在固件启动时,立即读取当前Bank状态,并重定位VTOR到当前Bank的物理地址。
- 使用Flash末尾的专用标志位记录当前运行Bank和升级状态,回滚时依据标志位决定切换方向。
配置步骤
-
使能双Bank模式:通过选项字节设置
nDBANK=0(双Bank),并确保BANK_SW初始为0。 -
链接脚本:两个固件均将
FLASH起始地址设为0x08000000,长度设为1M(每个Bank大小)。 -
启动代码:在
Reset_Handler中,读取FLASH_BANK_SW位,计算当前物理Bank基地址,并设置VTOR。 -
升级流程:将新固件写入非活动Bank,置位标志位,切换
BANK_SW,复位。 -
回滚流程:在固件运行早期进行自检,若失败,清除标志位,切换
BANK_SW,复位。
代码示例
以下为关键代码(基于STM32H743,使用HAL库):
// 启动文件中的Reset_Handler(汇编)
Reset_Handler:
ldr r0, =0x40022000 // FLASH寄存器基地址
ldr r1, [r0, #0x14] // FLASH_CR
tst r1, #(1 << 31) // 检查BANK_SW位(假设位31)
ite eq
ldreq r2, =0x08000000 // 若BANK_SW=0,当前Bank基地址为0x08000000
ldrne r2, =0x08100000 // 若BANK_SW=1,当前Bank基地址为0x08100000
// 设置VTOR(SCB->VTOR地址为0xE000ED08)
ldr r0, =0xE000ED08
str r2, [r0]
// 其余启动代码...
// 固件C代码:获取当前Bank基地址
uint32_t Get_Current_Bank_Base(void) {
if (READ_BIT(FLASH->CR, FLASH_CR_BANK_SW) != 0) {
return 0x08100000; // Bank2映射
} else {
return 0x08000000; // Bank1映射
}
}
// 固件自检与回滚函数
void Check_And_Rollback(void) {
uint32_t bank_base = Get_Current_Bank_Base();
// 校验当前固件完整性(例如CRC)
if (CRC_Check_Firmware(bank_base) == FAIL) {
// 回滚:切换BANK_SW并复位
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
SET_BIT(FLASH->CR, FLASH_CR_BANK_SW); // 切换Bank
FLASH->CR |= FLASH_CR_STRT; // 触发复位(需等待BSY)
while (FLASH->SR & FLASH_SR_BSY);
NVIC_SystemReset();
}
}
// 升级函数:写入新固件到非活动Bank
void OTA_Upgrade(uint8_t *new_fw, uint32_t size) {
uint32_t target_bank = (Get_Current_Bank_Base() == 0x08000000) ? 0x08100000 : 0x08000000;
// 擦除目标Bank,写入数据...
// 写入完成后,设置标志位(例如在Flash末尾)
Write_Upgrade_Flag(target_bank);
// 切换Bank并复位
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
SET_BIT(FLASH->CR, FLASH_CR_BANK_SW);
NVIC_SystemReset();
}
注意事项
- VTOR重定位必须在任何中断使能之前完成,否则中断向量可能指向错误地址。
-
链接脚本中不要使用
0x08100000作为起始地址,除非你明确处理了映射关系。 -
标志位存储位置:建议使用Flash的最后一个扇区(如Bank1的末尾),并确保在切换Bank时该地址不被映射改变(因为两个Bank的物理地址不同,但标志位可放在固定物理地址,如
0x080FF000,该地址在Bank1内,切换Bank后仍可访问,因为Bank1始终映射在0x08100000?不,切换后Bank1映射到0x08100000,所以0x080FF000会变成Bank2的地址,因此标志位应放在两个Bank各自末尾,或使用备份寄存器(RTC)或外部EEPROM。推荐使用备份SRAM(BKPSRAM)或RTC备份寄存器,不受Bank切换影响。 -
复位后延迟:切换Bank后,建议等待Flash就绪(
FLASH_SR_BSY清除)再执行代码。 -
调试技巧:使用
SCB->VTOR的当前值来确认映射状态。
总结
STM32H7双Bank Flash的自动回滚功能强大,但地址映射的灵活性也带来了陷阱。通过理解BANK_SW的映射机制,在启动时正确重定位向量表,并使用独立于Bank映射的标志位,可以构建健壮的OTA升级系统。本文的规避方案已在实际项目中验证,能有效防止升级失败后的变砖风险。记住:地址映射是双刃剑,用得好是利器,用不好是深渊。