STM32双Bank Flash在OTA中的无缝回滚设计与实现

1. 为什么需要双Bank?

传统OTA方案通常采用单Bank + 外部备份区,升级时先擦写备份区,再复制到主区。但复制过程存在窗口期,一旦断电或出错,主区固件可能不完整,导致设备无法启动。而STM32的双Bank Flash(如STM32H7、L4+、F7等系列)将Flash分为两个独立的Bank(Bank0和Bank1),每个Bank可独立擦写,且硬件支持从任一Bank启动。升级时直接写入非活动Bank,完成后通过切换启动Bank实现原子性切换,无需复制,极大降低风险。

2. 双Bank原理与硬件支持

  • Bank划分:以STM32H743为例,Flash共2MB,分为Bank0(0x08000000-0x080FFFFF)和Bank1(0x08100000-0x081FFFFF),每个Bank大小1MB。
  • 启动配置:通过选项字节(Option Bytes)中的BOOT_ADD0BOOT_ADD1设置启动地址。当BOOT_SW位为0时,始终从Bank0启动;为1时,可通过软件切换启动Bank。
  • 硬件切换:写FLASH_CR寄存器的SWAP_BANK位,触发Bank交换,硬件自动将非活动Bank映射到地址0x08000000,无需搬移代码。
  • 关键优势:切换操作是原子的,即使切换后新固件崩溃,也可通过硬件看门狗或外部触发再次切换回旧Bank,实现无缝回滚。

3. 系统架构设计

本设计基于STM32H743,使用两个Bank各存储一份固件,Bootloader位于Bank0起始区域,App1位于Bank0后半部分,App2位于Bank1。实际项目中可灵活划分,但需保证每个Bank的App起始地址对齐(如0x20000对齐)。

  • Bootloader:负责检查固件有效性,决定启动哪个Bank,并处理升级请求。
  • App1/App2:功能相同的应用固件,编译时通过链接脚本指定不同的Flash起始地址。
  • 升级流程:运行中的App接收新固件包,写入非活动Bank,校验通过后设置标志位并复位,Bootloader根据标志切换Bank启动新固件。

4. 实现步骤

4.1 配置Flash双Bank模式

默认情况下,STM32H7可能处于单Bank模式(2MB合并)。需通过选项字节启用双Bank。在CubeMX中或代码中操作:

// 启用双Bank模式(需在解锁Flash后执行)
void Flash_EnableDualBank(void) {
    FLASH_OBProgramInitTypeDef ob;
    HAL_FLASHEx_OBGetConfig(&ob);
    if (ob.Banks == FLASH_BANK_BOTH) { // 当前为单Bank
        ob.Banks = FLASH_BANK_BOTH;
        ob.USERType = FLASH_OB_USER_BANK_SWAP;
        ob.USERConfig = FLASH_OB_USER_BANK_SWAP_ENABLE;
        HAL_FLASHEx_OBProgram(&ob);
        HAL_FLASH_OB_Launch(); // 触发重新加载选项字节
    }
}

4.2 固件写入非活动Bank

在App中,根据当前运行Bank确定目标Bank地址。例如,若运行在Bank0(基址0x08000000),则目标Bank1基址为0x08100000。写入时需按扇区擦除,注意H7的Flash扇区大小(128KB)。

#define BANK0_BASE 0x08000000
#define BANK1_BASE 0x08100000
#define APP_SIZE   0x80000  // 假设App最大512KB

uint32_t GetTargetBankBase(void) {
    // 通过读取VTOR或链接变量判断当前运行位置
    if ((uint32_t)&_vectors >= BANK0_BASE && (uint32_t)&_vectors < BANK0_BASE + APP_SIZE)
        return BANK1_BASE;
    else
        return BANK0_BASE;
}

// 写入固件包(分包写入,每包1KB)
void WriteFirmware(uint32_t offset, uint8_t *data, uint32_t len) {
    uint32_t target = GetTargetBankBase() + offset;
    // 擦除对应扇区(需先计算扇区号)
    FLASH_EraseInitTypeDef erase;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = GetSector(target);
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    uint32_t error;
    HAL_FLASHEx_Erase(&erase, &error);
    // 编程
    for (uint32_t i = 0; i < len; i += 8) {
        uint64_t val = *(uint64_t*)(data + i);
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, target + i, val);
    }
}

4.3 固件校验与切换

写入完成后,计算整个固件的CRC或哈希,与包头中的值比对。校验通过后,设置一个标志位(如备份区或Flash末尾的专用区域),然后复位。Bootloader启动时读取该标志。

// 设置升级标志(存储于备份寄存器或专用Flash页)
void SetUpgradeFlag(uint8_t bank) {
    // 示例:使用RTC备份寄存器
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, bank);
}

// 执行切换
void PerformSwap(void) {
    HAL_FLASH_Unlock();
    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
    // 设置SWAP_BANK位
    FLASH->CR |= FLASH_CR_SWAP_BANK;
    while (FLASH->CR & FLASH_CR_SWAP_BANK); // 等待完成
    HAL_FLASH_Lock();
    NVIC_SystemReset();
}

4.4 Bootloader启动逻辑

Bootloader在启动时检查标志位,决定从哪个Bank启动。若新固件运行失败,可通过看门狗复位,Bootloader检测到运行次数超限,自动回滚。

void Bootloader_JumpToApp(void) {
    uint8_t bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    uint32_t app_addr = (bank == 1) ? BANK1_BASE : BANK0_BASE;
    
    // 检查App有效性(栈顶地址合法且校验通过)
    if (IsAppValid(app_addr)) {
        // 设置主栈指针
        __set_MSP(*(uint32_t*)app_addr);
        // 跳转
        void (*app_entry)(void) = (void*)(*(uint32_t*)(app_addr + 4));
        app_entry();
    } else {
        // 回滚到另一个Bank
        uint32_t fallback = (bank == 1) ? BANK0_BASE : BANK1_BASE;
        if (IsAppValid(fallback)) {
            __set_MSP(*(uint32_t*)fallback);
            ((void(*)(void))(*(uint32_t*)(fallback + 4)))();
        }
    }
}

5. 完整示例代码

以下为Bootloader核心逻辑(简化版):

#include "main.h"

#define APP_MAGIC 0xDEADBEEF

typedef struct {
    uint32_t magic;
    uint32_t crc;
    uint32_t size;
} AppHeader;

int IsAppValid(uint32_t addr) {
    AppHeader *hdr = (AppHeader*)addr;
    if (hdr->magic != APP_MAGIC) return 0;
    // 计算CRC(需实现)
    return (CalcCRC(addr + sizeof(AppHeader), hdr->size) == hdr->crc);
}

void JumpToApp(uint32_t addr) {
    if (IsAppValid(addr)) {
        __set_MSP(*(uint32_t*)addr);
        void (*entry)(void) = (void*)(*(uint32_t*)(addr + 4));
        entry();
    }
}

int main(void) {
    HAL_Init();
    // 检查升级标志
    uint8_t new_bank = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
    if (new_bank == 1) {
        // 尝试启动新Bank
        JumpToApp(BANK1_BASE);
        // 若失败,回滚
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0);
        JumpToApp(BANK0_BASE);
    } else {
        JumpToApp(BANK0_BASE);
        // 若失败,尝试Bank1
        JumpToApp(BANK1_BASE);
    }
    // 都失败,进入Bootloader升级模式
    while(1);
}

6. 注意事项

  • 链接脚本:App1和App2的起始地址必须不同,且中断向量表需重定位(设置SCB->VTOR)。
  • Flash擦写时间:H7的Flash编程以256位(32字节)为单位,擦除一个扇区约需1秒,升级时需考虑功耗和超时。
  • 看门狗:在切换后启动新App前,喂狗并设置一个“运行成功”标志,若新App未及时清除,则触发回滚。
  • 选项字节操作:修改选项字节可能导致芯片复位,需在升级流程中谨慎处理,避免频繁操作。
  • 双Bank模式与读保护:启用双Bank后,读保护等级可能影响Bank切换,需根据需求配置。
  • 测试:务必在真机上验证断电、复位等异常场景,确保回滚逻辑可靠。

7. 总结

双Bank Flash为OTA提供了硬件级的原子切换,配合Bootloader的校验与回滚机制,可显著提升固件升级的可靠性。本文的实现基于STM32H7,但原理适用于所有支持双Bank的STM32系列。开发者可根据实际Flash大小和App体积调整Bank划分,并加入版本管理、断点续传等增强功能。