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)

// 跳转到 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 的启动文件需注意向量表重定位。

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