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

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

传统单Bank Flash升级流程中,新固件直接覆盖旧固件。若升级过程中断电、通信中断或固件校验失败,设备将无法启动,只能通过JTAG/SWD等外部接口恢复,这在现场环境下几乎不可行。STM32H7的双Bank Flash架构(如STM32H743/H750)将Flash分为两个独立的Bank(Bank1和Bank2),每个Bank可独立擦写和启动。利用这一特性,我们可以实现:

  • 固件冗余:旧固件保留在非活动Bank,新固件写入活动Bank。
  • 硬件自动回滚:若新固件启动失败(如看门狗超时),芯片可自动切换至旧Bank启动,无需软件干预。

2. 原理剖析:双Bank启动与回滚机制

2.1 双Bank Flash布局

STM32H7的Flash容量可达2MB,分为两个1MB的Bank。每个Bank可独立配置为启动区域。通过选项字节(Option Bytes)中的nBOOT1nBOOT0引脚状态,或软件配置FLASH_OPTCR寄存器,可控制芯片从哪个Bank启动。

2.2 硬件自动回滚流程

  1. 升级触发:系统运行在Bank1(旧固件),接收新固件数据。
  2. 写入Bank2:将新固件写入Bank2(非活动Bank),写入完成后置位FLASH_CR中的B2SB位(Bank2 Swap)。
  3. 切换启动:设置FLASH_OPTCRSWAP_BANK位,并触发选项字节重载,芯片重启后将从Bank2启动。
  4. 运行新固件:新固件运行后,需在启动早期(如main函数开头)执行“启动成功”标记(如写特定标志到备份寄存器或Flash)。
  5. 失败回滚:若新固件启动失败(如硬件错误、看门狗复位),芯片在复位后检测到上次启动未成功标记,硬件自动将启动地址切换回Bank1,并清除Swap标志,实现回滚。

关键点:硬件回滚依赖于“启动成功标记”的软件配合。若新固件在标记前崩溃,硬件将自动回滚;若标记后崩溃,则视为新固件已正常启动,不再回滚(避免死循环)。

3. 实现步骤与代码示例

3.1 硬件配置

  • MCU:STM32H743ZI(双Bank 2MB Flash)
  • 开发环境:STM32CubeIDE + HAL库
  • 关键外设:UART(用于接收固件)、IWDG(独立看门狗)

3.2 启动成功标记机制

在备份寄存器(RTC Backup Register)中定义两个标志:BOOT_SUCCESS_FLAGBOOT_ATTEMPTS

#define BOOT_SUCCESS_FLAG_ADDR  0x01  // 备份寄存器索引
#define BOOT_ATTEMPTS_ADDR      0x02
#define BOOT_SUCCESS_MAGIC      0xA5A5  // 成功标记魔数
#define MAX_BOOT_ATTEMPTS       3      // 最大启动尝试次数

void Boot_MarkSuccess(void) {
    HAL_RTCEx_BKUPWrite(&hrtc, BOOT_SUCCESS_FLAG_ADDR, BOOT_SUCCESS_MAGIC);
}

void Boot_ClearSuccess(void) {
    HAL_RTCEx_BKUPWrite(&hrtc, BOOT_SUCCESS_FLAG_ADDR, 0);
}

uint8_t Boot_IsSuccess(void) {
    return (HAL_RTCEx_BKUPRead(&hrtc, BOOT_SUCCESS_FLAG_ADDR) == BOOT_SUCCESS_MAGIC);
}

void Boot_IncrementAttempts(void) {
    uint32_t attempts = HAL_RTCEx_BKUPRead(&hrtc, BOOT_ATTEMPTS_ADDR);
    HAL_RTCEx_BKUPWrite(&hrtc, BOOT_ATTEMPTS_ADDR, attempts + 1);
}

void Boot_ClearAttempts(void) {
    HAL_RTCEx_BKUPWrite(&hrtc, BOOT_ATTEMPTS_ADDR, 0);
}

3.3 固件升级与Bank切换

升级函数负责接收固件、写入非活动Bank,并设置Swap标志。

void Firmware_Upgrade(uint8_t *data, uint32_t len) {
    // 1. 解锁Flash
    HAL_FLASH_Unlock();
    
    // 2. 擦除非活动Bank(假设当前从Bank1启动,则擦除Bank2)
    FLASH_EraseInitTypeDef erase = {0};
    erase.TypeErase = FLASH_TYPEERASE_MASS;  // 注意:此处应针对Bank2,但H7的MASS擦除会擦除所有Bank,需使用BANK擦除
    erase.Banks = FLASH_BANK_2;
    erase.NbPages = 255;  // 1MB / 4KB页
    uint32_t error = 0;
    HAL_FLASHEx_Erase(&erase, &error);
    
    // 3. 写入新固件到Bank2基地址(0x08100000)
    uint32_t addr = 0x08100000;
    for (uint32_t i = 0; i < len; i += 8) {
        uint64_t data64 = *(uint64_t *)(data + i);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, addr, data64);
        addr += 8;
    }
    
    // 4. 设置Swap标志,使下次启动从Bank2开始
    HAL_FLASH_OB_Unlock();
    FLASH_OBProgramInitTypeDef ob = {0};
    ob.OptionType = OPTIONBYTE_USER;
    ob.USERType = OB_USER_B2SB;
    ob.USERConfig = OB_B2SB_ENABLE;  // 使能Bank2启动
    HAL_FLASHEx_OBProgram(&ob);
    HAL_FLASH_OB_Lock();
    
    // 5. 触发系统复位
    HAL_FLASH_Lock();
    NVIC_SystemReset();
}

3.4 启动流程中的回滚逻辑

在main函数开头,执行以下逻辑:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    // 检查当前启动Bank
    uint32_t current_bank = (FLASH->OPTCR & FLASH_OPTCR_SWAP_BANK) ? 2 : 1;
    
    if (current_bank == 2) {
        // 从Bank2启动(新固件)
        if (Boot_IsSuccess()) {
            // 上次启动成功,本次正常,清除尝试计数
            Boot_ClearAttempts();
        } else {
            // 上次启动未成功,增加尝试次数
            Boot_IncrementAttempts();
            if (Boot_GetAttempts() > MAX_BOOT_ATTEMPTS) {
                // 超过最大尝试,回滚到Bank1
                HAL_FLASH_OB_Unlock();
                FLASH_OBProgramInitTypeDef ob = {0};
                ob.OptionType = OPTIONBYTE_USER;
                ob.USERType = OB_USER_B2SB;
                ob.USERConfig = OB_B2SB_DISABLE;  // 禁用Bank2启动,回滚
                HAL_FLASHEx_OBProgram(&ob);
                HAL_FLASH_OB_Lock();
                NVIC_SystemReset();
            }
        }
    } else {
        // 从Bank1启动(旧固件)
        // 正常启动,清除尝试计数
        Boot_ClearAttempts();
    }
    
    // 启动成功标记(在系统初始化完成后调用)
    Boot_MarkSuccess();
    
    // 用户应用代码...
}

3.5 看门狗配合

为了检测新固件是否“卡死”,启用独立看门狗(IWDG),并在主循环中定期喂狗。若新固件崩溃,看门狗复位,从而触发回滚逻辑。

void IWDG_Init(void) {
    // 配置IWDG,超时1秒
    IWDG_HandleTypeDef hiwdg = {0};
    hiwdg.Instance = IWDG;
    hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
    hiwdg.Init.Reload = 4095;  // 约1秒
    HAL_IWDG_Init(&hiwdg);
}

// 在主循环中
HAL_IWDG_Refresh(&hiwdg);

4. 注意事项

  • Bank大小匹配:确保新固件大小不超过Bank容量(1MB)。若固件超过,需使用外部Flash或压缩。
  • 擦除操作:H7的擦除必须按Bank进行,且擦除期间不可中断,否则可能导致Flash损坏。建议在升级前关闭中断。
  • 选项字节重载:设置Swap后,必须触发系统复位(或调用HAL_FLASH_OB_Launch())才能生效。
  • 启动成功标记时机:务必在系统初始化完成、外设就绪后标记成功,避免过早标记导致回滚失效。
  • 备份寄存器供电:备份寄存器在VDD掉电后数据丢失,若需长期保存,需使用外部电池或超级电容。
  • 调试注意事项:在调试时,若使用ST-Link,可能干扰选项字节,建议在调试前备份选项字节。

5. 总结

通过STM32H7的双Bank Flash和硬件Swap机制,我们实现了一个健壮的OTA升级回滚方案。该方案无需外部存储,仅依赖芯片内部资源,即可在升级失败时自动恢复至旧固件,极大提升了产品的可靠性和可维护性。开发者可根据实际需求调整尝试次数、标记时机等参数,以适应不同场景。