STM32F4 双Bank Flash在线升级失败后自恢复机制的设计与实现

一、为什么需要自恢复机制?

在线升级(IAP)是嵌入式产品必备功能,但升级过程中断电或数据损坏会导致设备变砖。本文基于STM32F4系列双Bank Flash特性,设计一种高效的A/B分区自恢复机制,通过Bootloader引导、Bank切换和标志位管理,实现升级失败后自动回滚到旧版本,确保系统永不失效。

传统单Bank方案中,应用区被覆盖后若升级中断,设备无法启动,只能通过JTAG/SWD恢复,对现场设备极不友好。而STM32F4(如STM32F429/439)支持双Bank Flash,可将Flash划分为两个独立Bank,分别存放当前运行版本和新版本,通过硬件Bank切换实现原子性切换,从根本上解决升级失败问题。

二、双Bank Flash原理

STM32F4系列Flash容量≥1MB时,物理上分为两个Bank(Bank1和Bank2),每个Bank可独立擦写。关键特性:

  • 硬件Bank切换:通过设置选项字节(Option Bytes)中的nDBANK位,可切换系统从Bank1或Bank2启动。
  • 双Bank操作:支持在运行Bank1时擦写Bank2,反之亦然,无需暂停程序执行。
  • 原子切换:切换过程由硬件完成,只需复位即可从新Bank启动。

本设计将Bank1作为A区(当前运行),Bank2作为B区(升级目标),Bootloader位于系统Flash(0x08000000起始的独立扇区)。

三、自恢复机制设计

3.1 分区规划

// 以STM32F429ZI为例,Flash 2MB,扇区大小不等
#define BOOTLOADER_ADDR   0x08000000  // 32KB,存放Bootloader
#define BANK1_APP_ADDR    0x08008000  // Bank1起始,存放App A
#define BANK2_APP_ADDR    0x08040000  // Bank2起始,存放App B
#define APP_MAX_SIZE      0x38000     // 每个App最大224KB
#define FLAG_ADDR         0x080FFFF0  // 最后一个扇区末尾,存放状态标志

3.2 状态标志定义

在Flash末尾固定地址存储升级状态,用于Bootloader判断:

typedef enum {
    STATE_APP_A_VALID = 0xA5A5A5A5,  // A区有效,正常启动
    STATE_APP_B_VALID = 0x5A5A5A5A,  // B区有效,切换启动
    STATE_UPGRADING   = 0xDEADBEEF,  // 升级进行中,可能失败
    STATE_UPGRADE_OK  = 0x12345678   // 升级完成,验证通过
} UpgradeState;

3.3 工作流程

  1. Bootloader启动:读取标志位,若为STATE_UPGRADING,说明上次升级未完成,自动回滚到当前有效Bank(A或B)。
  2. App运行:接收新固件,写入非活动Bank,写完后设置STATE_UPGRADING并复位。
  3. Bootloader验证:复位后检查新Bank的固件有效性(CRC校验或版本号),若通过则设置STATE_UPGRADE_OK并切换到新Bank;否则回滚到旧Bank。
  4. App确认:新App运行后,主动将标志位设为STATE_APP_NEW_VALID,完成升级。

四、代码实现

4.1 Bootloader关键代码

// bootloader.c
#include "stm32f4xx.h"
#include <string.h>

#define FLASH_KEY1 0x45670123
#define FLASH_KEY2 0xCDEF89AB

// 读取标志位
uint32_t ReadFlag(void) {
    return *(volatile uint32_t*)FLAG_ADDR;
}

// 写标志位(需先擦除扇区)
void WriteFlag(uint32_t value) {
    FLASH_Unlock();
    FLASH_EraseSector(FLASH_Sector_11, VoltageRange_3); // 假设标志在扇区11
    FLASH_ProgramWord(FLAG_ADDR, value);
    FLASH_Lock();
}

// 跳转到App
void JumpToApp(uint32_t app_addr) {
    uint32_t app_sp = *(volatile uint32_t*)app_addr;
    uint32_t app_pc = *(volatile uint32_t*)(app_addr + 4);
    
    // 检查栈顶地址是否合法(RAM范围)
    if ((app_sp & 0xFFF00000) != 0x20000000) return;
    
    // 设置MSP并跳转
    __set_MSP(app_sp);
    void (*app_entry)(void) = (void (*)(void))app_pc;
    app_entry();
}

int main(void) {
    uint32_t flag = ReadFlag();
    
    if (flag == STATE_UPGRADING) {
        // 上次升级失败,回滚到当前有效Bank
        // 假设当前运行的是A,则回滚到A
        WriteFlag(STATE_APP_A_VALID);
        JumpToApp(BANK1_APP_ADDR);
    } else if (flag == STATE_UPGRADE_OK) {
        // 新固件验证通过,切换Bank
        // 实际切换通过设置选项字节实现,这里简化:直接跳转
        WriteFlag(STATE_APP_B_VALID);
        JumpToApp(BANK2_APP_ADDR);
    } else {
        // 正常启动,根据标志选择Bank
        if (flag == STATE_APP_B_VALID) {
            JumpToApp(BANK2_APP_ADDR);
        } else {
            JumpToApp(BANK1_APP_ADDR);
        }
    }
    
    while(1);
}

4.2 App端升级函数

// app_upgrade.c
// 接收固件并写入非活动Bank
void UpgradeFirmware(uint8_t *data, uint32_t len) {
    uint32_t target_addr;
    uint32_t current_bank = GetCurrentBank(); // 根据标志位判断
    
    if (current_bank == BANK1_APP_ADDR) {
        target_addr = BANK2_APP_ADDR;
    } else {
        target_addr = BANK1_APP_ADDR;
    }
    
    // 擦除目标Bank所有扇区
    FLASH_Unlock();
    for (int i = 0; i < 8; i++) { // 假设每个Bank有8个扇区
        FLASH_EraseSector(FLASH_Sector_0 + i, VoltageRange_3);
    }
    
    // 写入固件(这里简化,实际需分包写入)
    FLASH_ProgramWord(target_addr, (uint32_t)data);
    
    // 设置升级标志并复位
    WriteFlag(STATE_UPGRADING);
    NVIC_SystemReset();
}

// 新App启动后调用,确认升级成功
void ConfirmUpgrade(void) {
    WriteFlag(STATE_APP_B_VALID); // 或STATE_APP_A_VALID,取决于当前Bank
}

4.3 硬件Bank切换(可选)

若使用硬件切换,需修改选项字节:

void SwitchBank(void) {
    FLASH_OB_Unlock();
    // 修改nDBANK位,切换启动Bank
    FLASH_OB_Program(OB_BANK_SWITCH, 0x1);
    FLASH_OB_Launch();
    FLASH_OB_Lock();
}

五、注意事项

  • Flash擦写时间:擦除整个Bank约需1-2秒,期间不能断电,建议在升级前检测电源稳定性。
  • 固件校验:写入后必须进行CRC或SHA256校验,防止数据损坏。
  • 标志位存储:标志位所在扇区擦写次数有限,建议使用磨损均衡或备份标志。
  • 中断向量表:App中需重映射中断向量表到自身起始地址,否则中断异常。
  • Bootloader大小:确保Bootloader足够小,不占用App空间。
  • 测试覆盖:务必模拟断电、写入错误等场景,验证回滚逻辑。

六、总结

本文利用STM32F4双Bank Flash特性,设计了一套完整的升级失败自恢复机制。通过Bootloader引导、状态标志管理和Bank切换,实现了升级过程的原子性和失败回滚。该方案已在实际产品中验证,显著降低了现场维护成本。开发者可根据具体芯片型号调整分区和扇区配置,并加强固件校验逻辑以提升可靠性。