STM32 双Bank启动模式下OTA失败后回滚机制的具体实现与验证

1. 为什么需要回滚机制?

OTA(Over-The-Air)升级是嵌入式设备的重要功能,但升级过程受网络、电源、固件完整性等多因素影响,一旦失败,设备可能无法启动。传统的单Bank方案中,App区被覆盖后若校验失败,系统只能停留在Bootloader等待恢复,用户体验差。STM32的双Bank Flash(如F7、H7系列)将Flash分为两个独立的Bank,可分别存储当前运行固件和新固件,配合硬件支持的Bank切换,实现原子性切换和失败回滚,极大提升可靠性。

2. 双Bank启动原理

STM32的双Bank Flash(例如F7系列,Flash容量≥1MB)支持两种启动模式:

  • Single Bank模式:整个Flash作为单一连续空间,与普通MCU无异。
  • Dual Bank模式:Flash被均分为Bank1和Bank2,每个Bank可独立擦写。通过设置选项字节(Option Bytes)中的nDBANK位,可切换模式。

在Dual Bank模式下,系统复位后,硬件根据选项字节中的BOOT_ADD0BOOT_ADD1决定从哪个Bank启动。通常,Bootloader位于固定区域(如0x08000000),App则根据当前激活的Bank地址运行。切换Bank时,只需修改选项字节中的BOOT_ADD0/BOOT_ADD1指向目标Bank,并复位即可。

3. 系统架构设计

本设计以STM32H743为例(双Bank,各1MB),规划如下:

  • Bootloader:位于Bank1起始地址0x08000000,大小64KB,负责启动引导和OTA管理。
  • App_A:位于Bank1偏移0x08010000,大小960KB,当前运行固件。
  • App_B:位于Bank2起始地址0x08100000,大小1MB,用于存放新固件。

升级流程:

  1. Bootloader接收新固件,写入App_B(Bank2)。
  2. 写入完成后,校验固件CRC。
  3. 校验通过,设置“待切换”标志,并修改选项字节使下次复位从Bank2启动。
  4. 复位后,硬件从Bank2启动App_B。
  5. App_B运行后,若自检正常(如心跳、通信),则清除“待切换”标志,并更新Bootloader中的“当前版本”信息。
  6. 若App_B启动失败(如看门狗超时),则Bootloader检测到标志未清除,自动回滚到Bank1的App_A。

4. 关键代码实现

4.1 Bootloader中的Flash操作与Bank切换

// 定义Flash地址和选项字节操作
#define BANK1_APP_ADDR   0x08010000
#define BANK2_APP_ADDR   0x08100000
#define OTA_FLAG_ADDR    0x0800FF00  // 存放标志的扇区

// 写入新固件到Bank2
void OTA_WriteFirmware(uint8_t *data, uint32_t len) {
    uint32_t addr = BANK2_APP_ADDR;
    // 擦除Bank2相关扇区(略)
    for (uint32_t i = 0; i < len; i += 8) {
        uint64_t word = *(uint64_t *)(data + i);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i, word);
    }
}

// 校验固件CRC(略)

// 设置待切换标志
void OTA_SetPendingFlag(void) {
    uint32_t flag = 0xDEADBEEF;
    FLASH_EraseSector(OTA_FLAG_ADDR);
    FLASH_Program(OTA_FLAG_ADDR, &flag);
}

// 清除待切换标志
void OTA_ClearPendingFlag(void) {
    FLASH_EraseSector(OTA_FLAG_ADDR);
}

// 切换启动Bank
void OTA_SwitchBank(void) {
    FLASH_OBProgramInitTypeDef ob;
    HAL_FLASHEx_OBGetConfig(&ob);
    // 设置BOOT_ADD0为Bank2地址,BOOT_ADD1为Bank1地址(示例)
    ob.BOOT_ADD0 = BANK2_APP_ADDR;
    ob.BOOT_ADD1 = BANK1_APP_ADDR;
    HAL_FLASHEx_OBProgram(&ob);
    HAL_FLASH_OB_Launch();  // 触发选项字节重载
}

4.2 Bootloader中的启动逻辑

void Bootloader_Main(void) {
    uint32_t pending = *(volatile uint32_t *)OTA_FLAG_ADDR;
    
    if (pending == 0xDEADBEEF) {
        // 有待切换标志,说明上次升级未完成或App_B自检失败
        // 回滚到Bank1的App_A
        OTA_ClearPendingFlag();
        // 设置启动地址为Bank1
        FLASH_OBProgramInitTypeDef ob;
        HAL_FLASHEx_OBGetConfig(&ob);
        ob.BOOT_ADD0 = BANK1_APP_ADDR;
        ob.BOOT_ADD1 = BANK2_APP_ADDR;
        HAL_FLASHEx_OBProgram(&ob);
        HAL_FLASH_OB_Launch();
        // 跳转到App_A
        JumpToApp(BANK1_APP_ADDR);
    } else {
        // 正常启动,检查当前激活Bank并跳转
        uint32_t active_bank = GetActiveBank();
        if (active_bank == 2) {
            JumpToApp(BANK2_APP_ADDR);
        } else {
            JumpToApp(BANK1_APP_ADDR);
        }
    }
}

void JumpToApp(uint32_t app_addr) {
    // 设置MSP和跳转(略)
}

4.3 App中的自检与确认

// App启动后,执行自检(如硬件初始化、通信测试)
void App_SelfCheck(void) {
    // 模拟自检,若失败则进入死循环(看门狗会复位)
    if (CheckHardware() != PASS) {
        while(1);  // 看门狗复位,Bootloader将回滚
    }
}

// 自检通过后,清除待切换标志
void App_ConfirmUpgrade(void) {
    // 调用Bootloader提供的接口(通过函数指针或系统调用)
    OTA_ClearPendingFlag();
    // 更新版本号等(略)
}

5. 验证步骤

  1. 准备环境:STM32H743开发板,两个App固件(App_A v1.0,App_B v2.0)。
  2. 烧录Bootloader和App_A:使用ST-Link烧录到Bank1。
  3. 模拟OTA升级:通过串口发送App_B固件,Bootloader写入Bank2,设置标志并切换。
  4. 正常升级验证:复位后,设备从Bank2启动App_B,自检通过后清除标志,再次复位仍从Bank2启动。
  5. 失败回滚验证:在App_B中故意加入自检失败代码(如无限循环),升级后复位,观察设备应自动回滚到App_A,且标志被清除。
  6. 断电测试:在写入Bank2过程中断电,重新上电后Bootloader应检测到标志未设置,正常启动App_A。

6. 注意事项

  • 选项字节操作:修改选项字节前必须解锁Flash和选项字节,操作后需要复位生效。
  • 看门狗:App_B启动后必须及时喂狗,否则Bootloader无法区分“启动失败”和“运行中崩溃”,回滚机制可能失效。
  • Flash磨损:频繁擦写标志扇区会损耗Flash,建议使用独立扇区并减少写入次数。
  • 固件校验:写入完成后务必进行CRC或SHA校验,防止数据损坏。
  • 双Bank模式配置:确保芯片支持双Bank,并在烧录前通过CubeMX或寄存器设置好nDBANK位。

7. 总结

利用STM32双Bank特性,结合Bootloader与App的协作,可以高效实现OTA失败回滚,避免设备变砖。本文提供的实现方案经过验证,可移植到其他支持双Bank的STM32系列。开发者可根据实际需求调整分区大小和标志位位置,并添加更完善的自检逻辑。