STM32双Bank Flash在OTA升级中的防掉电变砖策略与实现

一、为什么需要双Bank Flash?

传统单Bank Flash升级时,新固件直接覆盖旧固件,一旦写入中途掉电,Flash中既无完整新固件,也无可用旧固件,设备只能变砖。双Bank Flash将Flash划分为两个独立区域(Bank1和Bank2),分别存放当前运行固件和新固件。升级时,新固件写入非活动Bank,完成后通过切换启动地址使系统运行新固件。若写入失败或校验错误,仍可回退到旧Bank,确保设备永不掉线。

二、双Bank Flash原理与硬件支持

STM32系列(如F4、L4、H7等)部分型号支持双Bank模式。以STM32F4为例,Flash容量≥1MB时,可配置为双Bank,每个Bank独立擦写。关键特性:

  • Bank切换:通过设置选项字节或操作FLASH_CR寄存器,系统可从Bank1或Bank2启动。
  • 原子切换:切换操作在重启后生效,利用复位向量表重映射实现。
  • 独立擦写:一个Bank擦写时,另一个Bank可正常执行代码(需注意总线访问)。

硬件连接上,无需额外电路,但需确保电源稳定(建议加掉电检测电路,如通过ADC监测VDD)。

三、总体策略设计

  1. 分区规划

    • Bank1(0x08000000):当前运行区(App1)
    • Bank2(0x08040000):待升级区(App2)
    • 末尾预留标志区(如0x0807F000)存储状态标志。
  2. 状态标志

    • 0xA5A5A5A5:Bank1有效,Bank2无效
    • 0x5A5A5A5A:Bank2有效,Bank1无效
    • 0x00000000:升级中(无效状态)
  3. 升级流程

    • 接收新固件,写入非活动Bank(如当前运行Bank1,则写Bank2)。
    • 每写一页校验CRC或哈希。
    • 全部写入后,设置标志为“Bank2有效”,然后软复位。
    • Bootloader检查标志,跳转到对应Bank。
  4. 掉电恢复

    • 若写入中途掉电,标志仍为“Bank1有效”,重启后继续运行旧固件。
    • 若标志已更新但新固件校验失败,Bootloader可回滚标志并启动旧Bank。

四、代码实现(基于STM32F4 HAL库)

1. Flash配置(启用双Bank)

// 在系统初始化时调用,设置双Bank模式
void Flash_EnableDualBank(void) {
    FLASH_OBProgramInitTypeDef obInit;
    HAL_FLASH_Unlock();
    HAL_FLASH_OB_Unlock();
    
    obInit.OptionType = OPTIONBYTE_BANK;
    obInit.BANK = FLASH_BANK_1; // 或FLASH_BANK_2,取决于当前配置
    obInit.BankConfig = FLASH_BANK_DUAL; // 启用双Bank
    HAL_FLASH_OBProgram(&obInit);
    
    HAL_FLASH_OB_Lock();
    HAL_FLASH_Lock();
    // 需要重新上电或复位生效
    NVIC_SystemReset();
}

2. 写入固件到目标Bank

#define APP1_ADDR 0x08000000
#define APP2_ADDR 0x08040000
#define FLASH_PAGE_SIZE 2048 // 根据型号调整

uint32_t Write_Firmware(uint8_t *data, uint32_t len, uint8_t bank) {
    uint32_t addr = (bank == 1) ? APP1_ADDR : APP2_ADDR;
    uint32_t page_num = (len + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;
    
    HAL_FLASH_Unlock();
    for (uint32_t i = 0; i < page_num; i++) {
        // 擦除页
        FLASH_EraseInitTypeDef erase;
        erase.TypeErase = FLASH_TYPEERASE_PAGES;
        erase.Banks = (bank == 1) ? FLASH_BANK_1 : FLASH_BANK_2;
        erase.Page = i;
        erase.NbPages = 1;
        uint32_t pageError = 0;
        if (HAL_FLASHEx_Erase(&erase, &pageError) != HAL_OK) {
            HAL_FLASH_Lock();
            return 1;
        }
        // 写入数据(按双字对齐)
        for (uint32_t offset = 0; offset < FLASH_PAGE_SIZE; offset += 8) {
            uint64_t data64 = 0;
            memcpy(&data64, data + i*FLASH_PAGE_SIZE + offset, 8);
            if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr + i*FLASH_PAGE_SIZE + offset, data64) != HAL_OK) {
                HAL_FLASH_Lock();
                return 2;
            }
        }
    }
    HAL_FLASH_Lock();
    return 0;
}

3. 设置启动标志并切换Bank

#define FLAG_ADDR 0x0807F000
#define FLAG_BANK1_VALID 0xA5A5A5A5
#define FLAG_BANK2_VALID 0x5A5A5A5A

void Set_BootFlag(uint32_t flag) {
    HAL_FLASH_Unlock();
    // 擦除标志页
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_PAGES;
    erase.Banks = FLASH_BANK_2; // 标志区位于Bank2末尾
    erase.Page = (FLAG_ADDR - 0x08000000) / FLASH_PAGE_SIZE;
    erase.NbPages = 1;
    uint32_t pageError;
    HAL_FLASHEx_Erase(&erase, &pageError);
    // 写入标志
    uint64_t data = flag;
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, FLAG_ADDR, data);
    HAL_FLASH_Lock();
}

void Jump_To_App(uint8_t bank) {
    uint32_t app_addr = (bank == 1) ? APP1_ADDR : APP2_ADDR;
    // 检查栈顶地址是否合法
    if (*(uint32_t*)app_addr > 0x20000000 && *(uint32_t*)app_addr < 0x20020000) {
        // 设置主栈指针
        __set_MSP(*(uint32_t*)app_addr);
        // 跳转到复位向量
        void (*app_reset)(void) = (void (*)(void))(*(uint32_t*)(app_addr + 4));
        app_reset();
    }
}

4. Bootloader主逻辑

void Bootloader_Main(void) {
    uint32_t flag = *(uint32_t*)FLAG_ADDR;
    
    if (flag == FLAG_BANK2_VALID) {
        // 验证Bank2固件(如CRC)
        if (Verify_Firmware(APP2_ADDR) == 0) {
            Jump_To_App(2);
        } else {
            // 校验失败,回滚
            Set_BootFlag(FLAG_BANK1_VALID);
            Jump_To_App(1);
        }
    } else {
        // 默认运行Bank1
        Jump_To_App(1);
    }
}

五、注意事项

  • 中断向量表重映射:App固件需在启动时设置SCB->VTOR为自身Bank起始地址,否则中断异常。
  • Flash擦写时间:双Bank擦写时,若代码从另一Bank执行,需确保总线不冲突(F4支持并行访问)。
  • 掉电检测:建议在升级前检测电压,低于阈值时暂停写入。
  • 标志区可靠性:标志写入前先擦除,且使用双字写入,防止半字写入掉电。
  • 固件校验:写入时逐页计算CRC,并在跳转前整体校验,避免损坏固件运行。
  • 测试回滚:实际测试中,模拟写入中途断电,验证设备能正常回退。

六、总结

双Bank Flash方案通过空间换可靠性,结合标志位和Bootloader跳转,实现了OTA升级的原子性。即使升级过程中掉电,设备也能自动回退到旧版本,彻底解决变砖问题。该策略在工业、物联网等领域具有极高实用价值,是嵌入式OTA设计的核心进阶技术。