STM32H7 双 Bank 模式下在线固件升级(Bootloader)的地址映射陷阱与规避
👁 2 阅读 · 2026-08-27 · 嵌入式
STM32H7 系列凭借双 Bank Flash 架构,可实现无缝在线固件升级(OTA),但 Bank 切换带来的地址映射变化常让开发者踩坑。本文深入剖析双 Bank 模式下的地址重映射机制,揭示常见陷阱(如中断向量表错位、跳转失效、擦写冲突),并提供基于 HAL 库的完整 Bootloader 与 App 代码示例,助你稳健实现固件升级。
# STM32H7 双 Bank 模式 Bootloader:地址映射陷阱与规避
## 引言
在嵌入式 OTA 升级中,STM32H7 的双 Bank Flash 设计允许在运行当前固件的同时擦写另一 Bank,实现“无缝”升级。然而,双 Bank 模式下的地址映射并非直观的线性关系,若忽视其重映射机制,轻则升级失败,重则系统变砖。本文面向有经验的开发者,聚焦地址映射陷阱,提供可落地的规避方案。
## 双 Bank 模式与地址映射基础
### 1. Flash 架构概览
STM32H7 系列(如 H743/H750)内部 Flash 通常分为两个 Bank:Bank1 和 Bank2,每个 Bank 大小可为 1MB(H743)或 128KB(H750,但可配置)。默认情况下,两个 Bank 的地址空间连续:
- Bank1:0x08000000 - 0x080FFFFF(以 1MB 为例)
- Bank2:0x08100000 - 0x081FFFFF
### 2. 双 Bank 模式与地址重映射
当启用双 Bank 模式(通过选项字节设置 `DBANK=1`),Flash 控制器会将两个 Bank 视为一个逻辑整体,但支持“Bank 切换”功能。关键点在于:**Bank 切换后,物理 Bank 的地址映射会互换**。
- 默认映射:Bank1 映射到低地址(0x08000000),Bank2 映射到高地址(0x08100000)。
- 切换后:Bank1 映射到高地址,Bank2 映射到低地址。
这意味着,如果 Bootloader 位于 Bank1,App 位于 Bank2,当 Bootloader 跳转到 App 时,必须确保 CPU 从正确的物理地址取指。
### 3. 常见陷阱
- **陷阱1:中断向量表错位**。App 的中断向量表必须位于其实际运行地址(即映射后的低地址),否则中断无法响应。
- **陷阱2:跳转地址计算错误**。直接使用物理地址跳转,但未考虑重映射后的逻辑地址。
- **陷阱3:擦写冲突**。在运行 Bank1 时擦写 Bank2,但若地址映射未正确配置,可能误擦当前运行代码。
- **陷阱4:Flash 选项字节配置不当**。DBANK 位设置错误导致启动异常。
## 规避策略与实现
### 1. 设计思路
采用经典方案:Bootloader 固定位于 Bank1(低地址),App 位于 Bank2(高地址)。Bootloader 启动后,检查升级标志,若需要升级则擦写 Bank2,然后执行 Bank 切换,使 Bank2 映射到低地址,最后跳转到低地址的 App。
### 2. 配置步骤
1. **设置选项字节**:启用双 Bank 模式(DBANK=1)。可使用 STM32CubeProgrammer 或代码中操作 FLASH_OB_Program。
2. **编写 Bootloader**:位于 Bank1,负责接收固件、擦写 Bank2、切换 Bank 并跳转。
3. **编写 App**:编译时链接地址设为 Bank2 的物理地址(如 0x08100000),但运行时需在 Bank 切换后位于低地址。
4. **实现 Bank 切换**:调用 HAL 库函数 `HAL_FLASHEx_OB_Launch()` 或直接操作寄存器。
### 3. 关键代码示例
#### Bootloader 端(Bank1)
```c
// 跳转到 App 函数(在 Bank 切换后调用)
void JumpToApp(uint32_t app_addr) {
// 检查栈顶地址合法性
if (((__IO uint32_t*)app_addr)[0] < 0x20000000 ||
((__IO uint32_t*)app_addr)[0] > 0x20020000) {
return;
}
// 设置主栈指针
__set_MSP(*(__IO uint32_t*)app_addr);
// 获取复位向量
void (*app_reset_handler)(void) = (void (*)(void))(*(__IO uint32_t*)(app_addr + 4));
// 跳转
app_reset_handler();
}
// 执行 Bank 切换
void SwitchBank(void) {
// 设置 FLASH_CR 的 BERS1 位和 STRT 位(根据参考手册)
FLASH->CR1 |= FLASH_CR_BERS1; // 选择 Bank1 擦除(实际是切换)
FLASH->CR1 |= FLASH_CR_STRT;
while (FLASH->SR1 & FLASH_SR_QW); // 等待完成
// 或者使用 HAL 库(更安全)
// HAL_FLASHEx_OB_Launch(); // 注意:此函数会复位,需在复位后处理
}
// 主流程示例
int main(void) {
// 初始化时钟、串口等
// 检查升级标志(如外部 Flash 中的标志)
if (upgrade_requested) {
// 擦写 Bank2(物理地址 0x08100000)
EraseBank2();
// 写入新固件到 Bank2
WriteFirmwareToBank2();
// 切换 Bank,使 Bank2 映射到低地址
SwitchBank();
// 注意:切换后,CPU 仍从当前地址运行,但 Flash 映射已变。
// 需要复位或跳转。此处直接跳转到低地址(0x08000000)
JumpToApp(0x08000000); // 此时 0x08000000 对应物理 Bank2
} else {
// 直接跳转到当前映射的低地址 App(若未切换,则跳转到 Bank1 的 App)
JumpToApp(0x08000000);
}
}
```
#### App 端(Bank2)
App 编译时链接地址设为 0x08100000,但运行时需在 Bank 切换后位于 0x08000000。因此,App 的启动文件需注意向量表重定位。
```c
// App 的 main 函数开头(或在 SystemInit 中)
void SystemInit(void) {
// 设置向量表偏移,指向当前运行地址(0x08000000)
SCB->VTOR = 0x08000000; // 因为 Bank 切换后,App 实际在低地址
// 注意:若未切换,则应为 0x08100000,但 Bootloader 会保证切换后跳转
}
```
### 4. 注意事项
- **中断向量表**:务必在 App 启动早期重设 VTOR,否则任何中断都会导致 HardFault。
- **Flash 擦写保护**:确保 Bootloader 和 App 的 Flash 区域没有设置写保护(RDP 等级)。
- **Bank 切换的复位问题**:`HAL_FLASHEx_OB_Launch()` 会触发系统复位,因此切换后需重新启动,Bootloader 需在启动时判断当前映射状态,避免重复切换。
- **编译链接地址**:App 的链接脚本中,FLASH 起始地址应设为 0x08100000,但运行时需通过 VTOR 重定向。
- **调试技巧**:在切换前后打印 Flash 映射寄存器(FLASH_OPTSR)的值,确认 DBANK 位和 BFB2 位状态。
## 进阶:避免切换的替代方案
若不想处理 Bank 切换的复杂性,可考虑以下方案:
- **方案A:固定双 Bank 不切换**。Bootloader 在 Bank1,App 在 Bank2,但 App 始终从 0x08100000 运行,升级时擦写 Bank1(但 Bootloader 在 Bank1,不可行)。因此需将 Bootloader 放在独立区域,如系统 Flash。
- **方案B:使用外部 Flash**。将固件存储在外部 SPI Flash,Bootloader 从外部加载到 RAM 执行,但受限于 RAM 大小。
- **方案C:使用 STM32 的“无缝”升级库**(如 X-CUBE-FREERTOS 中的 OTA 组件),但底层仍需处理映射。
## 总结
STM32H7 双 Bank 模式是强大的 OTA 工具,但地址映射陷阱不容小觑。理解 Bank 切换的物理与逻辑地址关系,正确配置选项字节,并在 App 中重设向量表,是成功升级的关键。本文提供的代码示例和注意事项,可帮助开发者避开常见坑点,实现稳定可靠的固件升级。建议在实际项目中,先在开发板上验证切换流程,再部署到产品中。