# 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库): ```c // 启动文件中的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 // 固件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升级系统。本文的规避方案已在实际项目中验证,能有效防止升级失败后的变砖风险。记住:地址映射是双刃剑,用得好是利器,用不好是深渊。