STM32双Bank Flash在线升级:如何避免擦写期间掉电变砖

1. 问题根源:为什么擦写掉电会变砖?

在线升级(OTA)的核心是擦除并重写内部Flash。STM32的Flash编程(尤其是擦除)需要较高电压(如1.8V),且操作期间CPU若掉电,Flash内容可能处于未定义状态——既不是旧固件,也不是新固件,导致设备无法启动。

传统单Bank方案中,Bootloader和App共享同一Flash区域,升级时直接覆盖App区。一旦掉电,Bootloader可能也被破坏(若Bootloader与App相邻),或App区残留半写数据,系统无法跳转。

2. 双Bank Flash:硬件级的安全屏障

部分STM32系列(如STM32F7、H7、L4+)支持双Bank Flash。其核心特性:

  • 物理隔离:Flash分为Bank0和Bank1,可独立擦写。
  • 原子切换:通过设置选项字节(Option Bytes)中的BOR_LEVDBANK位,可让系统在重启时从另一个Bank启动,切换过程由硬件完成,无需软件干预。
  • 并行编程:支持同时擦写一个Bank而运行另一个Bank(需注意总线仲裁)。

关键优势:升级时,App运行在Bank0,新固件写入Bank1。即使Bank1擦写掉电,Bank0的旧固件完好无损,设备仍可正常运行。下次升级时,再尝试从Bank1启动或回滚。

3. 防掉电变砖的软件架构

3.1 总体流程

Bootloader (Bank0起始) --> 检查标志 --> 跳转至Bank0或Bank1的App
App运行 --> 收到升级包 --> 写入非活动Bank --> 设置切换标志 --> 软复位
Bootloader --> 校验新Bank CRC --> 切换Bank --> 启动新App

3.2 关键模块

  • Bootloader:固定位于Bank0起始地址(如0x08000000),负责启动选择、固件校验、跳转。
  • App:可位于Bank0或Bank1,通过编译链接地址区分。
  • 升级标志:存储在备份寄存器(RTC Backup Register)或Flash末尾的专用区域,用于指示升级状态。

4. 配置步骤(以STM32H743为例)

4.1 硬件配置

  1. 使能双Bank模式:在CubeMX中,选择Memory -> Flash,设置Dual Bank ModeEnabled
  2. 配置Flash等待周期:根据主频调整LATENCY,确保Flash访问稳定。
  3. 分配地址
    • Bank0:0x08000000 - 0x080FFFFF(1MB)
    • Bank1:0x08100000 - 0x081FFFFF(1MB)

4.2 软件配置

  1. Bootloader工程

    • 链接脚本:仅包含Bank0区域,起始地址0x08000000,长度可设为0x20000(128KB)。
    • 中断向量表重定位:在启动代码中设置SCB->VTOR = 0x08000000
  2. App工程(以Bank0为例):

    • 链接脚本:起始地址0x08020000,长度0xE0000(896KB)。
    • main函数开头重定位向量表:SCB->VTOR = 0x08020000
  3. 升级流程代码(在App中):

// 写入新固件到非活动Bank(假设当前运行在Bank0,写入Bank1)
void OTA_WriteFirmware(uint32_t *data, uint32_t len) {
    uint32_t dst_addr = 0x08100000; // Bank1起始
    // 擦除Bank1(注意:擦除前需解锁Flash)
    HAL_FLASH_Unlock();
    FLASH_Erase(&flash_erase_cfg); // 配置擦除Bank1
    // 编程数据
    for (uint32_t i = 0; i < len; i += 8) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, dst_addr + i, *(uint64_t*)(data+i));
    }
    HAL_FLASH_Lock();
    // 设置升级标志(备份寄存器)
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0x5A5A); // 标志值
    // 软复位
    NVIC_SystemReset();
}
  1. Bootloader跳转逻辑
void Bootloader_JumpToApp(void) {
    uint32_t app_addr;
    // 检查升级标志
    if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) == 0x5A5A) {
        // 尝试从Bank1启动
        app_addr = 0x08100000;
        // 校验Bank1的CRC(需提前计算并存储)
        if (CRC_Check(app_addr, APP_SIZE) == PASS) {
            // 切换Bank(通过设置选项字节)
            FLASH_OB_Unlock();
            // 设置DBANK位,使系统从Bank1启动
            FLASH_OB_Program(OB_DBANK, 1);
            FLASH_OB_Launch();
            // 复位后硬件自动从Bank1启动
        } else {
            // 校验失败,回滚到Bank0
            app_addr = 0x08020000;
            // 清除标志
            HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0);
        }
    } else {
        app_addr = 0x08020000; // 默认Bank0
    }
    // 跳转
    if (*(volatile uint32_t*)app_addr & 0x2FFE0000) { // 检查栈顶地址合法性
        // 设置主栈指针
        __set_MSP(*(volatile uint32_t*)app_addr);
        // 跳转至复位向量
        void (*jump)(void) = (void*)(*(volatile uint32_t*)(app_addr + 4));
        jump();
    }
}

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

// 升级包接收与写入(伪代码)
void OTA_Process(void) {
    // 1. 接收升级包(通过UART/以太网)
    uint8_t *buffer = malloc(MAX_PACKET_SIZE);
    // 2. 校验包完整性(CRC32)
    if (CRC32_Verify(buffer, packet_len) != PASS) {
        error_handle();
        return;
    }
    // 3. 写入非活动Bank
    uint32_t active_bank = GetActiveBank(); // 读取当前运行Bank
    uint32_t target_bank = (active_bank == BANK0) ? BANK1_ADDR : BANK0_ADDR;
    Flash_EraseBank(target_bank);
    Flash_WriteData(target_bank, buffer, packet_len);
    // 4. 设置升级标志
    SetUpgradeFlag(target_bank);
    // 5. 软复位
    NVIC_SystemReset();
}

// 在Bootloader中,启动前检查
void CheckAndJump(void) {
    uint32_t flag = GetUpgradeFlag();
    if (flag != 0) {
        uint32_t target = (flag == BANK1_FLAG) ? BANK1_ADDR : BANK0_ADDR;
        if (VerifyAppCRC(target)) {
            // 切换Bank(硬件切换)
            SwitchBank(target);
        } else {
            // 回滚到旧固件
            ClearFlag();
        }
    }
    JumpToApp(GetActiveBank());
}

6. 注意事项与最佳实践

  • Flash写保护:在Bootloader中启用对Bank0的写保护,防止App意外擦除Bootloader。可通过选项字节设置WRP
  • 升级标志的可靠性:使用备份寄存器(掉电不丢失)而非RAM,并采用双标志(如0xA5A5和0x5A5A)防止误判。
  • CRC校验:在写入后、切换前,必须对新固件进行CRC或SHA校验,确保完整性。
  • 看门狗:在擦写过程中喂狗,防止因长时间擦除导致系统复位。
  • 电源监测:使用PVD(可编程电压检测器)监测电压,若电压低于阈值,立即停止擦写并进入安全状态。
  • 测试:在开发阶段,模拟掉电场景(如随机断电)进行压力测试,验证恢复机制。

7. 总结

双Bank Flash配合原子切换机制,为OTA升级提供了硬件级的安全保障。通过合理的软件设计——Bootloader引导、标志管理、CRC校验和回滚策略,即使擦写期间掉电,设备也能自动恢复至旧固件,彻底避免变砖。该方案在工业控制、智能家居等对可靠性要求高的场景中极具价值。