引言

在嵌入式设备远程升级(OTA)中,升级失败(如断电、校验错误、写入异常)可能导致设备无法启动。传统单 Bank 方案需要外部备份或恢复模式,而 STM32 系列(如 F4、L4、H7)支持双 Bank 闪存,允许在运行一个 Bank 的同时擦写另一个 Bank,并在重启时切换。结合 Bootloader 设计,可实现升级失败后自动回滚到旧版本,极大提升可靠性。

双 Bank 原理与回滚机制

双 Bank 闪存布局

STM32 的双 Bank 模式将内部 Flash 划分为两个独立的 Bank(如 Bank1 和 Bank2),每个 Bank 可独立擦除和编程。以 STM32F4 为例,若 Flash 总容量为 1MB,则每个 Bank 为 512KB。用户可配置为单 Bank 模式(默认)或双 Bank 模式,通过选项字节(Option Bytes)设置。

  • Bank1:通常存放 Bootloader 和当前运行的应用(App1)。
  • Bank2:存放待升级的新应用(App2)或备份旧应用。

回滚策略

设计目标:升级失败时,Bootloader 能自动回滚到上一个正常运行的版本。常见策略:

  1. 双副本交替:Bootloader 始终位于固定区域(如 Bank1 起始地址),App1 和 App2 分别位于两个 Bank 的剩余空间。每次升级将新固件写入非活动 Bank,校验通过后切换启动地址;若校验失败,则继续启动旧 Bank。
  2. 版本标记:在 Flash 的固定区域(如最后几个扇区)存储版本号和状态标志(如升级中、升级成功、升级失败)。Bootloader 根据标志决定启动哪个 Bank。

本文采用策略 1,结合状态标志实现回滚。

关键配置步骤

1. 启用双 Bank 模式

通过修改选项字节启用双 Bank。可使用 STM32CubeProgrammer 或在代码中通过 Flash 接口操作。以 STM32F4 为例,设置选项字节的 DBANK 位为 1

// 启用双 Bank 模式(需在系统复位后生效)
FLASH_OBProgramInitTypeDef ob;
HAL_FLASHEx_OBGetConfig(&ob);
ob.Banks = FLASH_BANK_1; // 或 FLASH_BANK_BOTH
ob.OptionType = OPTIONBYTE_BANK;
ob.BankConfig = FLASH_BANK_1 | FLASH_BANK_2; // 双 Bank
HAL_FLASHEx_OBProgram(&ob);
HAL_FLASH_OB_Launch(); // 触发复位

注意:修改选项字节会擦除 Flash,需在开发阶段完成,量产时通常已配置好。

2. 内存映射规划

假设 Flash 总容量 1MB,双 Bank 后每个 Bank 512KB。规划如下:

  • Bank1
    • 0x08000000:Bootloader(32KB,占用 0x08000000-0x08007FFF)
    • 0x08008000:App1(480KB,占用 0x08008000-0x0807FFFF)
  • Bank2
    • 0x08080000:App2(512KB,占用 0x08080000-0x080FFFFF)

注意:Bootloader 必须位于 Bank1 起始地址,因为复位向量固定指向 0x08000000。

3. 应用代码的链接脚本调整

App1 和 App2 需使用不同的链接脚本,设置各自的起始地址和大小。例如,App1 的 FLASH_ORIGIN0x08008000,App2 为 0x08080000。同时,在应用代码中需配置中断向量表偏移:

// 在 main 函数开头设置向量表偏移
SCB->VTOR = FLASH_BASE | APP_START_ADDR;

其中 APP_START_ADDR 为当前应用的起始地址(如 App1 为 0x08008000)。

4. Bootloader 设计要点

Bootloader 负责启动流程和升级逻辑。核心流程:

  1. 检查升级标志(如外部引脚、通信命令)。
  2. 若有升级请求,接收新固件并写入非活动 Bank。
  3. 写入完成后,校验 CRC 或哈希。
  4. 校验通过,更新状态标志,跳转到新 Bank;否则,回滚到旧 Bank。
  5. 若无升级请求,直接跳转到当前有效 Bank。

完整代码示例

以下为 Bootloader 的核心代码(基于 STM32 HAL 库),演示双 Bank 切换和回滚逻辑。

// 定义 Bank 地址和大小
#define BOOTLOADER_SIZE   (32 * 1024)
#define APP1_ADDR         (FLASH_BASE + BOOTLOADER_SIZE)          // 0x08008000
#define APP2_ADDR         (FLASH_BASE + 512 * 1024)               // 0x08080000
#define APP_MAX_SIZE      (480 * 1024)

// 状态标志存储地址(使用最后一个扇区)
#define STATUS_ADDR       (FLASH_BASE + 1024 * 1024 - 4)          // 假设 1MB Flash

// 状态值定义
#define STATUS_APP1_VALID  0xA5A5A5A5
#define STATUS_APP2_VALID  0x5A5A5A5A
#define STATUS_UPGRADING   0xDEADBEEF

// 函数声明
void JumpToApp(uint32_t app_addr);
int CheckAppValid(uint32_t app_addr);
void WriteStatus(uint32_t status);

int main(void)
{
    HAL_Init();
    // 初始化时钟、串口等...

    uint32_t status = *(volatile uint32_t *)STATUS_ADDR;
    uint32_t active_app = APP1_ADDR;

    // 检查是否有升级请求(例如通过串口命令)
    if (CheckUpgradeRequest()) {
        // 升级流程:写入非活动 Bank
        uint32_t target_bank = (status == STATUS_APP1_VALID) ? APP2_ADDR : APP1_ADDR;
        WriteStatus(STATUS_UPGRADING); // 标记升级中

        if (ReceiveAndWriteFirmware(target_bank)) {
            // 校验新固件
            if (CheckAppValid(target_bank)) {
                // 更新状态标志
                WriteStatus((target_bank == APP2_ADDR) ? STATUS_APP2_VALID : STATUS_APP1_VALID);
                active_app = target_bank;
            } else {
                // 校验失败,保持旧状态,自动回滚
                active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR;
            }
        } else {
            // 写入失败,回滚
            active_app = (status == STATUS_APP1_VALID) ? APP1_ADDR : APP2_ADDR;
        }
    } else {
        // 无升级请求,根据状态选择启动 Bank
        if (status == STATUS_APP2_VALID) {
            active_app = APP2_ADDR;
        } else {
            active_app = APP1_ADDR; // 默认 App1
        }
    }

    // 跳转到应用
    JumpToApp(active_app);

    while (1);
}

// 跳转到指定地址的应用
void JumpToApp(uint32_t app_addr)
{
    // 检查栈顶地址是否合法
    uint32_t msp = *(volatile uint32_t *)app_addr;
    if ((msp & 0xFFF00000) != 0x20000000) {
        // 栈指针无效,进入错误处理
        ErrorHandler();
    }

    // 设置主栈指针
    __set_MSP(msp);
    // 设置向量表偏移
    SCB->VTOR = app_addr;
    // 获取复位向量并跳转
    void (*reset_handler)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4));
    reset_handler();
}

// 检查应用是否有效(通过 CRC 或固定标志)
int CheckAppValid(uint32_t app_addr)
{
    // 简单检查:读取应用头部固定偏移处的魔数
    uint32_t magic = *(volatile uint32_t *)(app_addr + 0x1FC); // 假设在应用头部写入魔数
    return (magic == 0xDEADBEEF); // 实际应使用 CRC
}

// 写入状态标志
void WriteStatus(uint32_t status)
{
    HAL_FLASH_Unlock();
    FLASH_EraseInitTypeDef erase;
    uint32_t page_error = 0;
    erase.TypeErase = FLASH_TYPEERASE_PAGES;
    erase.PageAddress = STATUS_ADDR;
    erase.NbPages = 1;
    HAL_FLASHEx_Erase(&erase, &page_error);
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, STATUS_ADDR, status);
    HAL_FLASH_Lock();
}

注意事项

  • 选项字节修改:启用双 Bank 会改变 Flash 布局,务必在开发阶段完成,并确保 Bootloader 和应用链接脚本匹配。
  • 中断向量表:应用必须设置 SCB->VTOR,否则中断会跳转到 Bootloader 的向量表。
  • 升级过程中的掉电保护:建议在升级前备份旧版本状态,升级中写入状态标志,以便重启后识别。
  • 校验机制:使用 CRC32 或 SHA256 对整个固件进行校验,避免仅依赖头部魔数。
  • 看门狗:在升级过程中启用独立看门狗(IWDG),防止卡死。
  • 双 Bank 切换:部分 STM32 支持硬件切换(如通过 SYSCFG 寄存器),但本文采用软件跳转方式,更通用。
  • 测试:务必在真机测试断电、写入错误等场景,确保回滚逻辑可靠。

总结

利用 STM32 双 Bank 特性,结合状态标志和校验机制,可以设计出高可靠的 OTA 升级方案。Bootloader 在升级失败时自动回滚到旧版本,避免设备变砖。本文提供了核心设计思路和代码示例,开发者可根据具体芯片型号调整内存布局和细节。