3. A/B升级机制原理

OTA升级最怕的不是“升级失败”,而是“升级失败后设备再也起不来”。A/B升级机制正是为解决这一风险而设计的:设备内部保留两套可独立运行的系统镜像,新固件写入非当前运行槽位,只有确认新系统能够正常启动并完成自检后,才把启动权正式交给它。这样,升级过程即使断电、写入出错或新固件启动异常,旧系统仍然完整保留,设备具备回退到可用状态的能力。

双系统分区设计:不是简单复制两份 rootfs

在STM32MP平台上,A/B机制通常围绕eMMC或SD卡上的GPT分区表展开。以一套典型布局为例,除了FSBL、SSBL等公共引导阶段外,系统会划分出两个对称的槽位:

  • bootfs_a / bootfs_b:存放各自槽位的Linux内核、设备树与启动所需文件;
  • rootfs_a / rootfs_b:存放各自槽位的根文件系统;
  • 公共数据区:如用户数据、配置、日志等,通常不随槽位切换而复制,但需要保证升级前后兼容;
  • 启动元数据区:保存当前活动槽位、槽位状态、尝试次数、校验值等,是A/B机制的“控制面”。

双分区并不等于把整个系统无脑复制两份。工程上更常见的做法是:内核与根文件系统按槽位成对存在,而大容量用户数据、日志、标定参数放在共享分区中,通过版本兼容策略避免回滚后数据不可读。对于STM32MP这类带MMC控制器的平台,分区表本身也可以由U-Boot读取,从而在引导阶段完成槽位选择。

STM32MP A/B 双槽位分区与启动元数据 eMMC / SD 存储布局(示例) 公共引导区:FSBL(TF-A) + SSBL(U-Boot) + 环境变量 负责读取启动元数据、选择槽位、加载对应内核 槽位 A(当前活动) bootfs_a:内核 zImage + 设备树 + extlinux 版本 v1.2.0 · 状态:good rootfs_a:根文件系统(只读 + overlay) 版本 v1.2.0 · 校验:sha256 通过 槽位 B(待升级目标) bootfs_b:内核 zImage + 设备树 + extlinux 版本 v1.3.0 · 状态:pending rootfs_b:根文件系统(只读 + overlay) 版本 v1.3.0 · 校验:sha256 通过 启动元数据(U-Boot env / misc) active_slot = A · boot_success = 1 slot_b_retry = 3 · slot_b_status = pending 共享数据区(不随槽位切换) 用户配置 · 标定参数 · 日志 · 证书 需保证 v1.2.0 与 v1.3.0 双向兼容 读取并决策 写入状态

槽位管理:谁在运行、谁在待命

槽位管理的核心是“活动槽位”与“目标槽位”的区分。设备正常运行时,U-Boot根据启动元数据中的active_slot选择从A或B启动。当OTA客户端下载新固件后,它不会覆盖当前运行的分区,而是把镜像写入另一个槽位,并更新元数据中的目标槽位状态。例如:

# 示例:U-Boot 环境变量中的槽位控制字段
active_slot=A
slot_a_status=good
slot_b_status=pending
slot_b_retry=3
bootcount=0
upgrade_available=1

这些字段的含义是:当前从A启动,A状态为good;B已写入新固件但尚未验证,允许尝试启动3次;upgrade_available=1表示存在待验证升级。U-Boot在启动时会读取这些变量,决定加载哪个槽位的内核。若B连续启动失败达到重试上限,引导逻辑应自动切回A,并把B标记为bad或invalid。

启动标志与回滚策略

启动标志是A/B机制从“能启动”走向“可回滚”的关键。常见做法是在用户空间由OTA服务维护一个“启动成功确认”流程:新系统首次启动后,服务执行自检(网络、存储、关键外设、应用进程),全部通过后调用fw_setenv boot_success 1或写入misc分区,将当前槽位标记为good,并清除重试计数。若自检失败或系统在限定时间内未确认,U-Boot在下一次启动时递减重试计数,归零后回退到旧槽位。

回滚策略需要明确三个边界条件:

  • 回滚触发条件:启动失败、内核panic、根文件系统挂载失败、自检超时、关键服务未就绪;
  • 回滚目标:始终回退到最近一个状态为good的槽位,而不是简单地在A/B之间来回切换;
  • 回滚后的数据一致性:共享数据区若已被新版本迁移过,旧版本必须能够识别并兼容,否则回滚后应用可能无法读取配置。
回滚不是“万能保险”。如果新固件已经对共享数据区做了不可逆的格式迁移,回滚后旧系统可能无法解析数据。因此,涉及数据格式变更的升级,应优先采用“向前兼容”的迁移策略,或在升级前备份关键数据。

原子性更新:让“切换”成为一个不可分割的动作

原子性更新指的是:系统从旧槽位切换到新槽位的过程,要么完整发生,要么完全不发生,不存在“一半新一半旧”的中间状态。在A/B机制中,原子性主要依靠两个手段保证:

  1. 镜像写入与校验分离:新固件先完整写入目标槽位,再计算并比对sha256或签名,校验通过后才更新元数据中的目标槽位状态;
  2. 启动决策单点化:U-Boot只根据元数据中的一个活动槽位字段决定加载路径,避免多个标志位互相矛盾。

以一次典型升级为例,其状态迁移可以概括为:当前A运行且good → 下载v1.3.0写入B → 校验B通过 → 设置slot_b_status=pending、upgrade_available=1 → 重启 → U-Boot尝试启动B → B自检通过 → 标记B为good、A为old → 完成。若B启动失败,重试计数递减,归零后U-Boot切回A,B标记为bad,升级流程结束并上报失败。

在STM32MP工程实践中,建议把“启动成功确认”放在一个独立的、最小化的服务中,而不是依赖完整应用栈。这样即使应用层异常,确认逻辑仍有机会执行,避免误回滚。

工程实现中的关键参数与判断依据

为了让A/B机制真正可落地,需要明确一组可配置参数。以下是一份示例参数表,用于说明各字段的作用与建议取值:

参数 作用 示例值 判断依据
slot_retry_max 新槽位最大启动尝试次数 3 过小易误回滚,过大延长故障恢复时间
boot_success_timeout 启动后确认成功的超时时间 120 s 需覆盖内核启动、根文件系统挂载与自检
verify_method 镜像校验方式 sha256 + 签名 安全要求高时启用签名校验
rollback_policy 回滚目标选择策略 最近good槽位 避免在多个坏槽位之间反复切换
shared_data_compat 共享数据兼容策略 向前兼容 确保回滚后旧版本可读

这些参数不是孤立的,它们共同决定了升级的“安全边界”。例如,slot_retry_max与boot_success_timeout需要配合:如果超时时间太短,系统可能还没完成自检就被判定失败;如果重试次数太多,设备可能在坏固件上反复重启,影响现场可用性。工程上通常先通过串口日志观察一次完整启动耗时,再据此设置超时和重试次数。

最后需要强调,A/B升级的本质不是“多占一份存储”,而是用空间换可靠性。它把升级风险从“设备可能变砖”降低为“最多回退到旧版本”。对于STM32MP这类需要长期现场运行、且现场维护成本较高的设备,这种机制带来的可靠性收益通常远大于存储开销。