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流程:

  1. 固件A在Bank1运行,升级固件B写入Bank2。
  2. 写入完成后,置位BANK_SW,系统复位。
  3. 复位后,CPU从0x08000000执行Bank2的固件B。
  4. 若固件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和升级状态,回滚时依据标志位决定切换方向。

配置步骤

  1. 使能双Bank模式:通过选项字节设置nDBANK=0(双Bank),并确保BANK_SW初始为0。
  2. 链接脚本:两个固件均将FLASH起始地址设为0x08000000,长度设为1M(每个Bank大小)。
  3. 启动代码:在Reset_Handler中,读取FLASH_BANK_SW位,计算当前物理Bank基地址,并设置VTOR。
  4. 升级流程:将新固件写入非活动Bank,置位标志位,切换BANK_SW,复位。
  5. 回滚流程:在固件运行早期进行自检,若失败,清除标志位,切换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升级系统。本文的规避方案已在实际项目中验证,能有效防止升级失败后的变砖风险。记住:地址映射是双刃剑,用得好是利器,用不好是深渊。