STM32H7 双 Bank 模式下的 OTA 无缝回滚实战设计

1. 为什么需要双 Bank 与擦写中断?

在嵌入式 OTA 升级中,最怕的是升级过程中断电或写入错误,导致设备变砖。STM32H7 的双 Bank Flash 架构(如 H743/H750)将 Flash 分为两个独立的 Bank(Bank1 和 Bank2),每个 Bank 可独立擦写。配合 Flash 擦写中断(EOP 表示操作完成,WRPERR 表示写保护错误),我们可以实现:

  • 无缝切换:升级完成后,通过切换 Boot 地址立即运行新固件,无需重启。
  • 安全回滚:若新固件校验失败,自动回退到旧 Bank,保证设备可用。
  • 实时监控:利用中断感知擦写状态,避免轮询阻塞,提升系统响应。

2. 双 Bank 模式原理

STM32H7 的 Flash 控制器(FLASH)支持两种 Bank 模式:

  • Single Bank:整个 Flash 作为一个整体,地址连续。
  • Dual Bank:Flash 分为两个 1MB(或 512KB,取决于型号)的 Bank,每个 Bank 有独立的擦写单元。

在双 Bank 模式下,通过选项字节(Option Bytes)中的 DBANK 位配置。当 DBANK=1 时,启用双 Bank。此时,Bank1 的起始地址为 0x08000000,Bank2 为 0x08100000(以 2MB Flash 为例)。

关键点

  • 系统始终从 0x08000000 启动(除非配置了 Bank 切换)。
  • 通过设置 FLASH_OPTCR 寄存器的 SWAP_BANK 位,可以交换 Bank1 和 Bank2 的映射,使得系统从另一个 Bank 启动。

3. Flash 擦写中断机制

STM32H7 的 Flash 操作(编程/擦除)是异步的,完成或出错时会产生中断。相关中断标志位:

  • EOP(End of Operation):操作成功完成。
  • WRPERR(Write Protection Error):写入保护区域。
  • PGAERR(Programming Alignment Error):编程对齐错误。
  • SIZERR(Size Error):大小错误。

在中断服务函数(ISR)中,我们可以检查这些标志,并清除中断,从而避免轮询等待,提高 CPU 利用率。

4. 系统架构设计

我们的 OTA 方案分为三个部分:

  1. Bootloader:位于 Bank1 起始区域,负责固件接收、校验和跳转。
  2. App1:当前运行固件,位于 Bank1 后半部分。
  3. App2:新固件下载区,位于 Bank2。

升级流程:

  • App1 接收新固件写入 Bank2。
  • 写入完成后,校验 CRC。
  • 若成功,设置 SWAP_BANK 位,并软复位,Bootloader 从 Bank2 启动新固件。
  • 若新固件运行失败(如看门狗复位),Bootloader 检测到标志,清除 SWAP_BANK,回滚到 Bank1。

5. 关键代码实现

5.1 配置 Flash 双 Bank 模式

在系统初始化时,检查并配置双 Bank 模式(通常由 Bootloader 完成):

void Flash_EnableDualBank(void) {
    FLASH_OBProgramInitTypeDef obInit;
    HAL_FLASHEx_OBGetConfig(&obInit);
    
    if (obInit.DualBank == 0) {
        obInit.DualBank = 1;
        HAL_FLASHEx_OBProgram(&obInit);
        // 需要复位生效
        HAL_FLASH_OB_Launch();
    }
}

5.2 Flash 擦写中断配置

使能 Flash 中断,并编写中断服务函数:

void Flash_EnableInterrupt(void) {
    __HAL_FLASH_ENABLE_IT(FLASH_IT_EOP | FLASH_IT_WRPERR | FLASH_IT_PGAERR);
    HAL_NVIC_EnableIRQ(FLASH_IRQn);
}

void FLASH_IRQHandler(void) {
    uint32_t flags = __HAL_FLASH_GET_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR);
    
    if (flags & FLASH_FLAG_EOP) {
        // 操作完成,可以继续下一步
        __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP);
        // 设置事件标志,通知主循环
        flash_op_done = 1;
    }
    if (flags & FLASH_FLAG_WRPERR) {
        // 写保护错误,处理错误
        __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_WRPERR);
        flash_error = FLASH_ERROR_WRPERR;
    }
    if (flags & FLASH_FLAG_PGAERR) {
        __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_PGAERR);
        flash_error = FLASH_ERROR_PGAERR;
    }
}

5.3 固件写入 Bank2(带中断等待)

uint8_t Flash_WriteBank2(uint32_t destAddr, uint8_t *data, uint32_t size) {
    uint32_t addr = destAddr;
    uint32_t remaining = size;
    
    // 解锁 Flash
    HAL_FLASH_Unlock();
    
    // 擦除 Bank2(假设 Bank2 起始地址为 0x08100000)
    FLASH_EraseInitTypeDef eraseInit;
    eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
    eraseInit.Banks = FLASH_BANK_2;
    eraseInit.Sector = 0;
    eraseInit.NbSectors = 8; // 根据实际扇区数调整
    eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t sectorError = 0;
    HAL_FLASHEx_Erase(&eraseInit, &sectorError);
    
    // 等待擦除完成(中断方式)
    while (!flash_op_done && !flash_error) {}
    if (flash_error) { HAL_FLASH_Lock(); return 1; }
    flash_op_done = 0;
    
    // 编程数据
    while (remaining > 0) {
        uint32_t word = 0;
        // 组装 32 位数据(注意对齐)
        memcpy(&word, data, 4);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, word);
        while (!flash_op_done && !flash_error) {}
        if (flash_error) { HAL_FLASH_Lock(); return 1; }
        flash_op_done = 0;
        
        addr += 4;
        data += 4;
        remaining -= 4;
    }
    
    HAL_FLASH_Lock();
    return 0;
}

5.4 Bank 切换与回滚

void JumpToBank2(void) {
    // 设置 SWAP_BANK 位
    HAL_FLASH_OB_Unlock();
    __HAL_FLASH_SWAP_BANK();
    HAL_FLASH_OB_Lock();
    
    // 软复位
    NVIC_SystemReset();
}

void RollbackToBank1(void) {
    // 清除 SWAP_BANK 位
    HAL_FLASH_OB_Unlock();
    __HAL_FLASH_CLEAR_SWAP_BANK();
    HAL_FLASH_OB_Lock();
    // 复位后从 Bank1 启动
    NVIC_SystemReset();
}

5.5 Bootloader 中的固件校验与回滚逻辑

void Bootloader_CheckAndRun(void) {
    // 检查回滚标志(存储在备份寄存器或 Flash 特定区域)
    if (CheckRollbackFlag()) {
        RollbackToBank1();
    }
    
    // 检查 Bank2 固件有效性(如 CRC)
    if (IsFirmwareValid(0x08100000)) {
        JumpToBank2();
    } else {
        // 运行 Bank1 固件
        JumpToBank1();
    }
}

6. 注意事项

  • 中断优先级:Flash 中断优先级应设置较高,避免在擦写过程中被其他中断打断导致超时。
  • 擦写时间:STM32H7 擦除一个扇区大约需要 1-2 秒,期间系统不能进入低功耗模式,否则可能失败。
  • 数据对齐:Flash 编程必须按 32 位字对齐,否则产生 PGAERR。
  • 回滚标志:建议使用备份寄存器(如 RTC 备份域)存储回滚标志,避免占用 Flash 空间。
  • 看门狗:在擦写过程中喂狗,防止升级过程中复位。

7. 总结

通过利用 STM32H7 的双 Bank 架构和 Flash 擦写中断,我们实现了一个高效、安全的 OTA 升级方案。中断机制避免了轮询等待,提升了系统响应;双 Bank 切换保证了无缝升级和快速回滚。该设计适用于对可靠性要求极高的工业、汽车等嵌入式应用。