4. 镜像与槽位机制

MCUboot 的安全启动能力,最终要落到“镜像放在哪里、如何被识别、升级时如何切换、失败时如何退回”这几个工程问题上。镜像与槽位机制就是这套落地规则的总和:它定义了固件的封装格式、Flash 上的分区布局、每个槽位的状态语义,以及升级过程中镜像如何从副槽搬运到主槽。理解这一章,是后续做签名、加密、防回滚和量产升级的前提。

一、镜像格式:不只是二进制,而是带元数据的容器

MCUboot 使用的镜像不是裸的 .bin,而是一个带头部、尾部校验和可选 TLV 区域的容器。最小编织结构如下:

+----------------------+  <- 镜像起始地址
|  image_header (32B)  |   magic / load_addr / hdr_size / img_size
|                      |   flags / ver_major / ver_minor / pad
+----------------------+
|  payload (固件本体)   |
+----------------------+
|  TLV area            |   可选:密钥哈希、签名、加密信息
+----------------------+
|  trailer (状态区)     |   swap info / copy done / image ok
+----------------------+  <- 镜像结束

头部中的 magic 用于快速判断该区域是否是一个合法镜像;img_size 决定 payload 长度;ver_major/ver_minor 是版本比较的依据。TLV 区域承载签名、公钥哈希等安全元数据,是安全启动校验的入口。尾部 trailer 则保存槽位状态,是“交换”和“回滚”能够工作的关键。

镜像头部和 TLV 都参与哈希与签名计算,因此任何对镜像内容的修改都会导致校验失败。这也是 MCUboot 能识别“被篡改镜像”的根本原因。

二、主槽与副槽:Flash 上的双分区布局

MCUboot 在 Flash 上划分出两个镜像槽位:主槽(primary slot) 和 副槽(secondary slot)。主槽是当前运行镜像的存放位置,副槽是升级镜像的暂存位置。两者大小通常相同,且都包含完整的头部、payload、TLV 和 trailer。

MCUboot 双槽位 Flash 布局与升级数据流 主槽 Primary Slot image_header(magic / 版本 / 大小) payload(当前运行固件) TLV(签名 / 公钥哈希) trailer(image_ok / copy_done) 副槽 Secondary Slot image_header(新版本) payload(待升级固件) TLV(签名 / 公钥哈希) trailer(swap info / 状态) 升级搬运 升级时:副槽镜像经校验后搬运到主槽;回滚时:主槽镜像退回副槽

主槽与副槽的地址由 Bootloader 的链接脚本和分区表决定,通常通过 flash_layout.h 或设备树中的 partitions 节点配置。两者必须严格对齐,否则交换算法会因地址偏移错误而破坏镜像。

三、槽位状态:trailer 中的三个关键标志

每个槽位的 trailer 中保存着状态信息,MCUboot 通过它们判断“镜像是否可用、是否已完成交换、是否需要回滚”。核心标志有三个:

  • image_ok:主槽镜像已被确认可用,升级成功后由应用或 Bootloader 置位,表示“这个版本可以保留”。
  • copy_done:副槽镜像已经完整搬运到主槽,交换动作完成。
  • swap_info:记录交换进度和方向,用于掉电恢复,保证交换过程可中断、可续传。
状态标志 所在位置 含义 典型操作
image_ok 主槽 trailer 当前主槽镜像已被确认 应用启动后调用 boot_set_confirmed()
copy_done 副槽 trailer 副槽已搬运到主槽 Bootloader 交换完成后置位
swap_info 两槽 trailer 交换进度与方向 掉电后据此恢复交换

四、交换类型:覆盖、交换与直接执行

MCUboot 提供多种升级策略,常见的有三种:

  1. 覆盖升级(overwrite-only):直接把副槽镜像复制到主槽,不保留旧镜像。实现简单、占用空间小,但升级过程中掉电会导致主槽损坏,且无法回滚。
  2. 交换升级(swap):主槽与副槽镜像互换位置,旧镜像被保留在副槽中。升级失败或新镜像未确认时,可以回滚到旧版本。这是最常用的安全升级方式。
  3. 直接执行(direct-xip):不搬运镜像,Bootloader 直接跳转到副槽执行。适合支持 XIP 的 MCU,但需要额外的地址映射和版本管理。
交换升级的代价是需要两倍 Flash 空间,并且交换过程涉及大量擦写操作。工程上常通过“交换 + 压缩”或“交换 + 差分”来降低空间与时间开销。

五、升级与回滚的基本流程

以交换升级为例,一次完整的升级与回滚流程如下:

1. 应用将新镜像写入副槽
2. 重启,Bootloader 校验副槽镜像的签名与版本
3. 校验通过 -> 执行交换:副槽镜像搬运到主槽
4. 交换完成,置 copy_done,跳转主槽运行新固件
5. 新固件启动后自检,确认可用 -> 调用 boot_set_confirmed()
6. 若新固件启动失败或未确认 -> 下次重启触发回滚
7. 回滚:将副槽中的旧镜像恢复回主槽,回到旧版本

回滚的触发条件通常包括:新镜像启动后未在规定时间内确认、看门狗复位、应用主动请求回滚。MCUboot 通过 image_ok 是否置位来判断“是否需要回滚”,因此应用必须在启动成功后及时确认,否则会被误判为失败。

六、案例与常见误区

某工业网关项目使用 MCUboot 交换升级,量产时发现部分设备升级后无法启动。排查发现:应用在初始化外设时耗时较长,未在 Bootloader 规定的确认窗口内调用 boot_set_confirmed(),导致设备重启后触发回滚,反复在新旧版本之间切换。解决办法是把确认动作提前到系统初始化早期,或延长确认窗口。

常见误区:把 image_ok 当成“升级成功标志”而忽略确认时机;或者认为覆盖升级也能回滚。实际上,覆盖升级不保留旧镜像,一旦新镜像有问题,只能通过外部工具重新烧录。

另一个高频问题是槽位大小不一致。副槽若小于主槽,交换时会出现越界写入;若大于主槽,则浪费 Flash。工程上应保证两槽大小完全一致,并在分区表中显式声明。