STM32H7 双 Bank Flash 在线升级失败后自动回滚的硬件看门狗联动策略

1. 为什么需要双 Bank + 看门狗联动?

在 STM32H7 系列中,Flash 被划分为两个独立的 Bank(Bank0 和 Bank1),每个 Bank 可独立擦写。利用这一特性,我们可以将当前运行固件放在一个 Bank,将新固件下载到另一个 Bank,然后通过切换启动地址完成升级。然而,升级过程并非绝对可靠:

  • 固件传输中断(如网络异常)导致写入不完整。
  • 固件校验失败(CRC 或哈希不匹配)。
  • 新固件本身存在缺陷,启动后立即崩溃。

若没有回滚机制,系统可能永久变砖。硬件看门狗(IWDG)能在程序跑飞或死循环时复位 MCU,但单纯复位无法解决固件损坏问题。因此,我们需要将 IWDG 与双 Bank 状态标志结合,在复位后检测上次启动是否成功,若失败则自动回滚到旧版本。

2. 原理:双 Bank 切换与 IWDG 的协同工作

2.1 双 Bank 启动机制

STM32H7 的 Flash 选项字节(Option Bytes)中的 nBOOT1nBOOT0 位控制启动 Bank。更灵活的方式是使用 FLASH_OTP 或备份寄存器(如 RTC 备份寄存器)存储当前活动 Bank 的标识。我们通常采用以下策略:

  • Bank0:存放当前稳定运行的固件(旧版本)。
  • Bank1:存放新下载的固件(待验证)。

升级流程:

  1. 将新固件写入 Bank1。
  2. 校验 Bank1 固件完整性(如 CRC32)。
  3. 设置“待切换”标志,并复位。
  4. 启动时检查标志,若存在则尝试从 Bank1 启动。

2.2 IWDG 的角色

IWDG 独立于主时钟,即使系统死机也能复位。我们利用它来检测新固件是否“健康”:

  • 新固件启动后,必须在规定时间内(如 1 秒)喂狗,否则 IWDG 复位。
  • 复位后,启动代码检查“启动失败计数”,若超过阈值(如 3 次),则判定新固件不可用,自动回滚到 Bank0。

2.3 状态标志存储

状态标志可存放在备份寄存器(如 RTC->BKPxR)或 Flash 的专用区域。备份寄存器在复位后不丢失,且无需擦写 Flash,推荐使用。

3. 配置步骤

3.1 硬件准备

  • STM32H743 开发板(或其他 H7 系列)。
  • 外部或内部复位源(IWDG 使用 LSI 时钟,无需外部晶振)。

3.2 软件配置

步骤 1:使能 IWDG

在初始化代码中配置 IWDG,超时时间设为 1 秒(根据实际需求调整)。

// 初始化 IWDG,LSI 约 32kHz,预分频 256,重载值 125  => 1s 超时
void IWDG_Init(void) {
    // 解锁写保护
    IWDG->KR = 0x5555;
    // 设置预分频:256
    IWDG->PR = 0x06;
    // 设置重载值:125 (约 1s)
    IWDG->RLR = 125;
    // 等待更新完成
    while (IWDG->SR & IWDG_SR_PVU);
    while (IWDG->SR & IWDG_SR_RVU);
    // 启动看门狗
    IWDG->KR = 0xCCCC;
}

// 喂狗函数
void IWDG_Feed(void) {
    IWDG->KR = 0xAAAA;
}

步骤 2:备份寄存器读写

使用 RTC 备份寄存器存储状态标志。注意需使能 PWR 和 RTC 时钟。

// 使能备份寄存器访问
void BKP_Enable(void) {
    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_RCC_RTC_ENABLE();
    HAL_PWR_EnableBkUpAccess();
}

// 写入标志
void BKP_Write(uint32_t reg, uint32_t val) {
    RTC->BKPxR[reg] = val;
}

// 读取标志
uint32_t BKP_Read(uint32_t reg) {
    return RTC->BKPxR[reg];
}

步骤 3:启动流程设计

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

#define BOOT_MAGIC_OK      0xA5A5A5A5  // 新固件启动成功标志
#define BOOT_MAGIC_FAIL    0xDEADBEEF  // 新固件启动失败标志
#define MAX_RETRY          3           // 最大尝试次数

int main(void) {
    // 初始化系统时钟等...
    BKP_Enable();
    
    // 读取当前启动计数
    uint32_t retry_count = BKP_Read(0);
    
    // 检查是否有待切换标志
    if (BKP_Read(1) == 0x12345678) {  // 待切换标志
        // 尝试从 Bank1 启动(新固件)
        if (retry_count < MAX_RETRY) {
            retry_count++;
            BKP_Write(0, retry_count);
            // 设置启动地址为 Bank1
            // 注意:实际切换需修改 VTOR 和向量表,或使用系统 Bootloader
            // 此处简化:直接跳转
            void (*jump)(void) = (void (*)(void))0x08100000; // Bank1 基地址
            jump();
        } else {
            // 超过重试次数,回滚到 Bank0
            BKP_Write(0, 0);
            BKP_Write(1, 0); // 清除待切换标志
            // 继续执行 Bank0 固件(当前代码)
        }
    }
    
    // 正常启动流程
    IWDG_Init();
    
    // 如果是新固件首次启动,设置成功标志
    if (BKP_Read(1) == 0x12345678) {
        // 新固件运行正常,清除待切换标志,并设置成功标志
        BKP_Write(1, 0);
        BKP_Write(2, BOOT_MAGIC_OK);
    }
    
    // 主循环
    while (1) {
        IWDG_Feed(); // 喂狗
        // 应用逻辑...
    }
}

步骤 4:升级函数示例

在 Bank0 固件中,执行升级操作:

void OTA_Upgrade(void) {
    // 1. 擦除 Bank1
    FLASH_Erase_Bank(FLASH_BANK_1);
    
    // 2. 写入新固件到 Bank1(通过串口/网络等)
    // ... 写入过程 ...
    
    // 3. 校验固件(如 CRC32)
    if (CRC_Check(BANK1_ADDR, firmware_size) != 0) {
        // 校验失败,中止升级
        return;
    }
    
    // 4. 设置待切换标志
    BKP_Write(1, 0x12345678);
    BKP_Write(0, 0); // 重置重试计数
    
    // 5. 复位系统
    NVIC_SystemReset();
}

4. 完整代码示例(简化)

以下是一个整合的启动代码片段,展示了核心逻辑:

void SystemInit(void) {
    // ... 系统时钟初始化 ...
    BKP_Enable();
    
    uint32_t retry = BKP_Read(0);
    uint32_t pending = BKP_Read(1);
    
    if (pending == 0x12345678) {
        if (retry < MAX_RETRY) {
            retry++;
            BKP_Write(0, retry);
            // 跳转到 Bank1
            JumpToBank1();
        } else {
            // 回滚
            BKP_Write(0, 0);
            BKP_Write(1, 0);
            // 留在 Bank0
        }
    }
    
    IWDG_Init();
    
    // 新固件成功启动后,清除标志
    if (pending == 0x12345678 && retry <= MAX_RETRY) {
        BKP_Write(1, 0);
        BKP_Write(2, BOOT_MAGIC_OK);
    }
}

void JumpToBank1(void) {
    // 设置主栈指针
    uint32_t msp = *(volatile uint32_t*)0x08100000;
    __set_MSP(msp);
    // 跳转到复位向量
    void (*reset)(void) = (void (*)(void))(*(volatile uint32_t*)0x08100004);
    reset();
}

5. 注意事项

  • IWDG 超时时间:需大于新固件初始化所需时间,否则可能误判失败。建议设置为 1-2 秒,并在新固件启动后尽早喂狗。
  • 备份寄存器初始化:首次使用前,确保备份寄存器已清零,否则可能误触发回滚。
  • Flash 擦写保护:操作 Flash 时需解锁,并注意中断优先级,避免擦写过程中被中断导致错误。
  • 向量表重定位:从 Bank1 启动时,必须将 VTOR 设置为 Bank1 的基地址,否则中断向量错误。
  • 固件校验:写入后务必进行 CRC 或哈希校验,防止不完整固件启动。
  • 回滚次数限制:设置最大重试次数(如 3 次),避免无限循环。
  • 调试注意事项:使用调试器时,IWDG 可能因暂停而复位,建议在调试时禁用 IWDG 或延长超时。

6. 总结

通过将 STM32H7 的双 Bank Flash 与硬件看门狗联动,我们构建了一个健壮的 OTA 升级回滚机制。核心思想是:利用备份寄存器记录状态,利用 IWDG 检测新固件是否正常运行,并在多次失败后自动回滚到旧版本。这种方法无需外部存储,成本低且可靠性高,适用于工业控制、物联网设备等对稳定性要求极高的场景。开发者可根据实际需求调整超时和重试策略,并确保在升级过程中严格遵循 Flash 操作规范。