# 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 中重设向量表,是成功升级的关键。本文提供的代码示例和注意事项,可帮助开发者避开常见坑点,实现稳定可靠的固件升级。建议在实际项目中,先在开发板上验证切换流程,再部署到产品中。