2. 升级架构设计:Bootloader + App 双区架构、单区覆盖架构、A/B 系统架构

升级架构这东西,说白了就是决定「新固件往哪儿放、怎么替换老固件」。我见过不少团队一上来就写升级逻辑,结果做到一半发现 Flash 不够用,或者升级失败变砖了才回头改架构。嗯,咱们先把地基打牢。

目前主流的嵌入式升级架构就三种:Bootloader + App 双区、单区覆盖、A/B 系统。各有各的脾气,选错了后面全是坑。

一句话总结: 双区架构最通用,单区覆盖最省空间,A/B 系统最安全但最贵。
三种升级架构核心对比 Bootloader + App 双区 Bootloader App 分区 → 下载到 App 分区,重启后校验 单区覆盖架构 唯一 App 分区(原地覆盖) → 下载 + 擦除 + 写入,一气呵成 A/B 系统架构 Slot A Slot B → 后台下载到备用 Slot,切换启动 关键指标对比 指标 双区架构 单区覆盖 A/B 系统 Flash 占用 App 大小 + Bootloader 仅 App 大小 2 × App + Bootloader 安全等级 中(掉电可能变砖) 低(掉电必砖) 高(掉电可恢复) 恢复能力 需进入 Bootloader 重刷 无恢复能力 自动回滚到旧版本

2.1 Bootloader + App 双区架构

这是我最常用的方案,也是绝大多数 MCU 项目的默认选择。芯片上电先跑 Bootloader,Bootloader 检查有没有新固件,有就更新,没有就直接跳转到 App 分区执行。

分区结构很简单:

  • Bootloader 区:只做一件事——引导和升级。代码量很小,一般 8KB~32KB 就够。
  • App 分区:存放真正的业务逻辑。升级时新固件直接写入这个分区。
  • 参数/标志区:存放升级标志、版本号、校验值。我习惯单独划一个扇区,防止被 App 误擦。
我的习惯: Bootloader 里放一个「强制升级模式」。按住某个 GPIO 引脚再上电,不管有没有升级标志都进入等待下载状态。这招救过我很多次——有一次 OTA 下载到一半断网了,App 校验失败启动不了,客户按住按键就救回来了。

升级流程大致是这样:

  1. App 运行时收到升级包,下载到外部 Flash 或 RAM 缓冲区。
  2. 下载完成后,App 在参数区写入「待升级」标志,然后软复位。
  3. Bootloader 启动,读到标志,擦除 App 分区,把新固件从缓冲区搬过去。
  4. 校验通过后清除标志,跳转到新 App。

这个架构的优点是实现简单、Flash 占用适中。缺点嘛——升级过程中如果掉电,App 分区可能只写了一半,下次启动 Bootloader 发现 App 校验失败,只能干等着你重新传固件。

注意: Bootloader 和 App 必须使用不同的中断向量表。App 启动后要立即把 VTOR 寄存器重映射到 App 的起始地址,否则中断全跑飞了。我曾经见过一个团队忘了做这件事,按一下按键就触发 HardFault,查了两天才找到原因。

2.2 单区覆盖架构

说白了就是「原地换血」。整个 Flash 只有一个 App 分区,升级时直接擦掉老固件,写入新固件。没有 Bootloader,或者 Bootloader 小到可以忽略。

这种架构常见于 Flash 极其紧张的场景,比如 32KB 或 64KB 的芯片。你想想看,双区架构至少要多占一整个 App 的空间,A/B 系统更是翻倍,单区覆盖是唯一的选择。

但代价也很明显:

  • 升级期间掉电 = 变砖。没有备份,没有回滚,擦到一半断电就彻底凉了。
  • 必须保证升级包的完整性。我建议在传输阶段做完整校验,确认包没问题再开始擦写。
  • 没有回退能力。新固件有 bug?只能再升一次。
适用场景: 传感器节点、一次性部署设备、Flash 小于 128KB 且升级频率极低的场合。

我曾经在一个智能灯的项目里被迫用了单区覆盖——芯片只有 32KB Flash,双区根本塞不下。我的做法是:先把新固件下载到外部 EEPROM,校验通过后再一次性写入内部 Flash。虽然慢了点,但至少降低了变砖概率。

如果你不得不用单区覆盖,记住几条保命原则:

  • 写入前做完整的 CRC 或 SHA 校验。
  • 写入过程中不要喂狗,或者把喂狗放在写入循环里。
  • 保留一小块「恢复标志区」,如果升级失败,下次上电还能进入一个最小引导模式。

2.3 A/B 系统架构

这是最安全、但也最奢侈的方案。Flash 里放两个完整的 App 分区(Slot A 和 Slot B),外加一个 Bootloader。系统在 Slot A 上运行,升级时把新固件下载到 Slot B,下载完成后切换启动 Slot B。如果 Slot B 启动失败,自动回滚到 Slot A。

你想想看,这相当于给系统买了一份「保险」。汽车 ECU、医疗设备、工业控制器这些不能停机的场合,A/B 系统几乎是标配。

分区布局:

分区 大小 作用
Bootloader 16KB~32KB 引导、切换 Slot、管理升级标志
Slot A App 大小 当前运行版本
Slot B App 大小 备用/新版本
元数据区 1~2 个扇区 记录当前活跃 Slot、升级状态、尝试次数

升级流程:

  1. 系统在 Slot A 运行,后台下载新固件到 Slot B。
  2. 下载完成后,Bootloader 把元数据区的「活跃 Slot」从 A 改为 B。
  3. 系统重启,Bootloader 启动 Slot B。
  4. Slot B 运行后做自检,通过后提交「确认」标志。
  5. 如果 Slot B 启动失败或自检不通过,Bootloader 把活跃 Slot 切回 A,实现回滚。
我建议: 在元数据区里加一个「尝试次数」字段。每次切换新 Slot 时先清零,启动成功后递增,超过最大尝试次数(比如 3 次)就自动回滚。这能防止「每次重启都尝试启动坏固件」的死循环。

A/B 系统的代价也很明显:Flash 容量翻倍。如果你的芯片只有 256KB,App 占了 120KB,那 A/B 系统直接吃掉 240KB,Bootloader 都没地方放了。所以选这个架构前,先确认 Flash 容量够不够。

注意: 两个 Slot 的中断向量表必须独立。Bootloader 跳转前要确保 VTOR 指向正确的 Slot 起始地址。另外,两个 Slot 的外设初始化逻辑要一致,否则切换后外设状态可能错乱。

2.4 如何选择?

我一般按这个思路来选:

  • Flash 小于 128KB,升级频率低 → 单区覆盖。但一定要加外部存储或传输校验做保护。
  • Flash 128KB~512KB,需要远程升级 → Bootloader + App 双区。这是最平衡的方案。
  • Flash 大于 512KB,设备不允许停机 → A/B 系统。安全第一,成本第二。
  • 产品要过功能安全认证 → 直接上 A/B 系统。认证机构看到单区覆盖会摇头的。

最后说一句:架构选型没有银弹。我见过有人用 1MB Flash 的芯片还坚持用单区覆盖,也见过 64KB 的芯片硬上 A/B 系统结果塞不下。搞清楚你的约束条件,再动手画分区图。