STM32H7双Bank Flash在线升级失败后自动回滚的硬件级保障机制设计

1. 为什么需要硬件级回滚保障?

传统单Bank Flash升级流程:擦除旧固件 → 写入新固件 → 跳转执行。若在写入过程中断电或校验失败,设备将停留在半成品状态,无法启动。虽然可通过Bootloader二次引导,但Bootloader本身也可能被破坏,且恢复流程复杂。STM32H7的双Bank Flash从硬件层面解决了此问题:两个独立的Bank可分别存储新旧固件,通过选项字节(Option Bytes)原子切换启动地址,确保系统永远有一个可用的固件版本。

2. STM32H7双Bank Flash原理

2.1 硬件架构

  • STM32H7(如H743/H750)内置2MB Flash,分为Bank1和Bank2,各1MB(或根据型号不同,如H723为双Bank各512KB)。
  • 每个Bank可独立擦写,且支持同时读取一个Bank并写入另一个Bank(RWW,Read-While-Write)。
  • 系统启动时,通过选项字节BOOT_ADD0BOOT_ADD1决定从哪个Bank启动。默认从Bank1启动(地址0x08000000)。

2.2 切换机制

  • 通过修改选项字节中的BOOT_ADD0(对应Bank1)或BOOT_ADD1(对应Bank2)的地址值,并触发系统复位,即可切换启动Bank。
  • 关键点:选项字节的修改需要解锁Flash接口,且写入后必须复位才生效。但复位前,当前Bank仍在运行,因此切换是原子的——要么旧固件继续运行,要么新固件启动,不存在中间状态。

3. 双Bank升级与回滚设计

3.1 整体流程

  1. 固件A(旧版) 运行在Bank1(地址0x08000000)。
  2. 下载固件B(新版)到Bank2(地址0x08080000,以1MB Bank为例)。
  3. 对固件B进行CRC校验,若通过,则设置选项字节BOOT_ADD1指向Bank2,并复位。
  4. 系统从Bank2启动,运行固件B。
  5. 固件B运行后,执行自检(如外设初始化、通信握手)。若自检成功,则标记“升级成功”,并可选地将固件B复制到Bank1(或直接以Bank2为运行区)。若自检失败,则软件触发回滚:修改选项字节BOOT_ADD0指向Bank1,复位后回到固件A。

3.2 回滚触发条件

  • 升级后启动超时(如看门狗复位)。
  • 应用层主动上报错误(如关键外设初始化失败)。
  • 用户手动强制回滚(通过外部命令)。

4. 配置步骤(基于STM32CubeMX + HAL库)

4.1 内存布局规划

  • 在链接脚本(.ld)中,为Bank1和Bank2分别定义独立的固件起始地址和大小。例如:
    /* Bank1: 0x08000000, 长度1MB */
    /* Bank2: 0x08080000, 长度1MB */
    
  • 编译时,通过宏或不同工程生成两个固件:firmware_A.hex(Bank1)和firmware_B.hex(Bank2)。

4.2 选项字节操作函数

#include "stm32h7xx_hal.h"

// 解锁Flash接口
void Flash_Unlock(void) {
    HAL_FLASH_Unlock();
    // 解锁选项字节区域
    HAL_FLASH_OB_Unlock();
}

// 设置启动Bank
void Flash_SetBootBank(uint8_t bank) {
    FLASH_OBProgramInitTypeDef ob;
    ob.OptionType = OPTIONBYTE_USER;
    ob.USERType = OB_USER_BOOT_ADD0 | OB_USER_BOOT_ADD1;
    if (bank == 1) {
        ob.USERConfig = OB_BOOT_ADD0(0x08000000) | OB_BOOT_ADD1(0x08080000);
    } else {
        ob.USERConfig = OB_BOOT_ADD0(0x08080000) | OB_BOOT_ADD1(0x08000000);
    }
    HAL_FLASH_OBProgram(&ob);
    // 触发复位使选项字节生效
    HAL_NVIC_SystemReset();
}

4.3 固件写入Bank2

void Write_Firmware_To_Bank2(uint32_t srcAddr, uint32_t size) {
    uint32_t dstAddr = 0x08080000;
    uint32_t word;
    for (uint32_t i = 0; i < size; i += 8) {
        // 从外部存储(如SD卡或网络)读取数据,这里简化
        word = *(uint32_t*)(srcAddr + i);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, dstAddr + i, word);
    }
}

4.4 升级成功标记与回滚检测

// 在Bank2固件中,启动后执行自检
void Check_And_Confirm_Update(void) {
    if (SelfTest_Pass()) {
        // 写标记到备份寄存器或Flash特定区域
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0xA5A5); // 示例
        // 可选:将Bank2复制到Bank1,实现永久切换
    } else {
        // 回滚:设置启动Bank为Bank1
        Flash_SetBootBank(1);
    }
}

5. 完整代码示例(简化版)

// main.c 片段
int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    // 检查是否首次启动新固件
    if (Is_New_Firmware_First_Boot()) {
        if (SelfTest_Pass()) {
            Confirm_Update(); // 标记成功
        } else {
            Rollback_To_Old(); // 回滚
        }
    }
    
    // 正常运行应用
    while (1) {
        // 应用逻辑
    }
}

void Rollback_To_Old(void) {
    Flash_Unlock();
    Flash_SetBootBank(1); // 强制从Bank1启动
}

6. 注意事项

  • 选项字节写入次数:Flash选项字节有擦写寿命(通常1万次),频繁切换会耗尽,建议仅在升级时操作,且避免循环切换。
  • 看门狗配合:在升级过程中,务必喂狗,防止升级时间过长导致复位,但回滚时需利用看门狗强制复位。
  • 双Bank地址映射:不同型号的H7,Bank大小可能不同(如H723为512KB),需查阅数据手册确认地址。
  • 固件大小限制:每个Bank的固件不能超过Bank容量,否则写入会越界。
  • 中断向量表重定位:Bank2固件启动后,需在SystemInitmain开头设置SCB->VTOR为0x08080000,否则中断异常。
  • RWW特性:在写入Bank2时,可以继续从Bank1执行代码,但需确保代码不在Bank2区域,否则总线冲突。

7. 总结

STM32H7的双Bank Flash机制为OTA升级提供了硬件级的安全网。通过合理设计启动切换和回滚策略,即使升级失败,设备也能自动恢复到上一个稳定版本,极大提升了系统的鲁棒性。本文给出的代码和步骤可直接应用于实际项目,但需根据具体型号和需求调整。记住:硬件保障是基础,软件逻辑是灵魂,二者结合才能打造真正可靠的嵌入式系统。