/notes/mcu/enterprise-bootloader

Firmware//15 分钟

企业级 Bootloader:从三段式到 A/B,再到防变砖底线

把 Golden Image、A/B 双区、外部 Flash 三种主流架构讲清楚,并补齐签名校验、看门狗回滚、防掉电这几条让产品真正能出货的底线。

BootloaderOTASTM32FlashSecure Boot

背景

一个能在实验室跑通的 Bootloader,和一个能出货一百万台的"企业级" Bootloader,差的不是几百行代码,而是鲁棒性安全性这两条底线。

在企业级语境里,我们不说"内存",只说 Flash(闪存)——因为代码断电后必须不丢失。 Bootloader 的核心使命只有两条:永远能启动,永远不能变砖

最常见的入门理解是"三段式"架构(Bootloader + 出厂区 + 运行区 + 参数区),这其实就是业内说的 Golden Image / Recovery 架构。但在真实产品里,根据 Flash 容量和业务需求,还会演化出 A/B 双区、外部 Flash 缓存等多种形态。这篇笔记把它们一次梳理清楚。

分析

一、三段式架构详解(Golden Image / Recovery)

这种架构把 MCU(如 STM32)的内部 Flash 切成四块:

  1. Bootloader 区(固化区)

    • 系统上电执行的第一行代码:硬件初步初始化、检查升级标志、校验固件完整性、执行跳转。
    • 企业级要求:绝对不能在普通 OTA 中被改写。通常用 STM32 的硬件 Write Protection 把扇区锁死——它坏了,设备就成了砖。
  2. Factory App 区(出厂 / 黄金镜像区)

    • 出厂烧录的、经过极严格测试的、功能最基础但绝对稳定的版本。
    • 当升级版本出现致命 Bug(如无限重启),或用户长按"恢复出厂"键时,Bootloader 放弃当前版本、跳转回这里,确保设备永远活着、永远能联网重新拉新固件
  3. Active App 区(运行 / 升级区)

    • 当前正在运行的最新业务代码。日常跑的都是它。
  4. Data / Flag 区(参数区)

    • 存 Bootloader 和 App 之间通信的"暗号"(Magic Word)。
    • 例:App 下载完新固件后写入 0x5A5A,Bootloader 启动时读到该值就知道要执行升级替换。

典型执行流程:

上电 → Bootloader 启动 → 读参数区 → 有升级标志?→ 搬运覆盖 → 有恢复出厂标志?→ 擦 Active 区从 Factory 复制,或直接跳 Factory → 校验目标区 CRC / Hash → 重映射中断向量表(SCB->VTOR)→ 跳转执行

二、其他两种主流架构

1. 双区 A/B 面架构(Dual Bank / A/B Update)

Flash 够大的项目,现在最流行

  • 布局: Bootloader + App Slot A + App Slot B + 数据区
  • 特点: 没有"出厂区"。A 区运行时后台把新固件下载进 B 区,下载完重启,Bootloader 校验 B 区后直接跳 B 运行;下次升级再下到 A,交替进行
  • 优势:
    • 无缝升级:不需要 Bootloader 搬运代码,搬运是断电变砖的最大来源。
    • 升级极快:少一次擦写。
    • 自带回滚(Rollback):B 区启动失败时,重启后 Bootloader 自动跳回旧的 A 区。

2. 外部 Flash 下载架构(Download Partition)

内部 Flash 小到装不下两套代码时用。

  • 布局: 内部 = Bootloader + Active App;外部挂一片便宜的 SPI Flash 做 Download 缓存区。
  • 特点: App 运行时把新固件下到外部 Flash,重启后 Bootloader 把外部数据拷贝并覆盖到内部 Active App。
  • 劣势: 覆盖过程一旦掉电,内部 App 直接损坏。所以 Bootloader 必须实现断点续传 / 防掉电恢复。

三、三种架构怎么选

维度三段式(Recovery)A/B 双区外部 Flash
Flash 占用内部小 + 外部芯片
升级速度中(要搬运)快(直接跳)慢(要覆盖)
变砖风险极低高(依赖防掉电)
回滚能力回 Factory 基础版回上个版本
BOM 成本中(多一片 Flash)

实现

关键代码骨架(以 STM32 为例)

跳转前必须做的事:关中断 → DeInit 外设 → 重映射 VTOR → 设栈指针 → 跳转。任何一步漏掉都会埋玄学雷。

// 跳转到指定 App 地址,注意调用前必须保证目标区代码已校验通过
typedef void (*app_entry_t)(void);

static void jump_to_app(uint32_t app_addr) {
    // 1) 关闭所有中断,避免 App 启动瞬间被遗留中断打断
    __disable_irq();

    // 2) 反初始化 Bootloader 用过的外设(串口、定时器、DMA...)
    HAL_DeInit();

    // 3) 清除可能 pending 的中断,复位 SysTick
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL  = 0;
    for (uint32_t i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    // 4) 重映射中断向量表到 App 起始地址
    SCB->VTOR = app_addr;

    // 5) 取 App 的 MSP(栈顶)并设置
    uint32_t app_msp   = *(volatile uint32_t *)(app_addr);
    uint32_t app_reset = *(volatile uint32_t *)(app_addr + 4);
    __set_MSP(app_msp);

    // 6) 跳转到 App 的 Reset_Handler
    __enable_irq();
    ((app_entry_t)app_reset)();
}

升级标志的"双 Magic + 状态机"写法

为了防止单一标志被误擦写或写到一半掉电,参数区通常存两个 Magic Word + 一个状态码,三者交叉判断。

// 参数区结构(放在独立扇区,App 与 Boot 都能访问)
typedef struct {
    uint32_t magic_head;   // 0xA5A5A5A5
    uint32_t state;        // IDLE / READY_TO_UPGRADE / TRYING / CONFIRMED
    uint32_t target_slot;  // 0 = A, 1 = B
    uint32_t fw_size;
    uint32_t fw_crc32;
    uint32_t boot_retry;   // 试运行重启计数
    uint32_t magic_tail;   // 0x5A5A5A5A
} boot_param_t;

Flash 地址规划示例

# STM32F4 / 1MB Flash,A/B 架构示例(仅作参考)
0x0800_0000 - 0x0800_FFFF  (64K)  Bootloader      [Write-Protect]
0x0801_0000 - 0x0807_EFFF  (~440K) App Slot A
0x0808_0000 - 0x080E_EFFF  (~440K) App Slot B
0x080F_0000 - 0x080F_FFFF  (64K)  Data / Param    [双区冗余]

三、什么是真正的"企业级"?

1. 安全启动(Secure Boot)与签名校验

企业级设备不能随便刷入别人的固件。Bootloader 在跳转前:

  • 先算 CRC32 / SHA-256:防文件传输损坏;
  • 再校验 RSA / ECDSA 数字签名:防黑客篡改。

只有用企业私钥签过名的固件,Bootloader 才允许运行。公钥写死在 Bootloader 里,受 Write Protection 保护。

2. 看门狗与自动回滚(Watchdog & Rollback)

升级后新代码逻辑死锁、一启动就卡死怎么办?

跳转前开启硬件独立看门狗(IWDG),并把参数区状态置为 TRYING。新 App 必须在规定时间内:

  1. 周期性"喂狗";
  2. 业务自检通过后,把状态从 TRYING 改为 CONFIRMED

若新代码卡死导致看门狗复位,Bootloader 再次启动时发现 state == TRYINGboot_retry >= N,就判定升级失败,自动回滚到旧版本或 Factory。

3. 防掉电设计(Power-Loss Protection)

Bootloader 在擦写 Flash 的几秒钟里,用户拔电源绝对不能变砖。状态机要做到:

  • 每次上电都能从参数区识别"上次是拷贝到一半断电了";
  • 续上未完成的拷贝,或者回退到一致状态;
  • 写参数区时用双备份 + 序号,避免参数区自己被写坏。

4. 跳转前的干净环境

Bootloader 跳 App 之前,所有外设都要 DeInit、所有中断都要关闭,否则 App 刚启动就被 Bootloader 遗留的串口 / 定时器中断打断,会触发各种"玄学死机"。这就是上面代码骨架里 HAL_DeInit() + __disable_irq() + 清 NVIC 那几步存在的意义。

三条不可触碰的红线

  1. Bootloader 扇区必须 Write-Protect。
  2. 跳转前必须校验签名 / Hash,CRC 只是基础。
  3. 任何写 Flash 的操作都必须假定下一毫秒会掉电。

结论

  • 架构选型: Flash 够大首选 A/B 双区——无缝升级、自带回滚、风险最低;中等容量用 Recovery / Golden Image 保底;超小容量才考虑外部 Flash 缓存。
  • 核心动作: 规划好 Flash 地址映射(Linker / 分散加载)、处理好中断向量表偏移(SCB->VTOR)、跳转前清干净外设和中断。
  • 企业级的分水岭: 不是功能多,而是签名校验 + 看门狗回滚 + 防掉电 + 干净跳转这四件事是否都做到位。

工程上没有银弹——Bootloader 的全部艺术,就是死死守住"绝对不能变砖"这条底线。