在嵌入式系统 OTA 固件升级文章中,我们探讨了其必要性与核心机制。其中,A/B分区方案作为实现高可靠性、无缝升级和安全回滚的关键技术被反复提及。它有效解决了传统单分区升级中可能出现的“变砖”风险,显著提升了设备的健壮性与用户体验。
然而,理解原理只是第一步。如何在实际项目中落地A/B分区,特别是如何利用Bootloader(如U-Boot)进行分区切换,才是工程师面临的核心挑战。本文旨在聚焦实战,详细阐述如何设计分区表、配置U-Boot环境变量,并编写Linux层的切换逻辑,从而构建一套稳定可靠的A/B升级系统。我们将以U-Boot作为典型引导程序进行讲解,提供一份可直接参考的操作指南。本文是对相关理论知识的概述,更深入的内容可参考瑞芯微、NXP等SoC厂商的相关文档。欢迎在云栈社区交流更多嵌入式开发经验。
1. 分区表设计
实现A/B分区的第一步,是合理规划设备的存储布局。一个典型的A/B分区方案通常包含以下几个关键分区:
| 分区名称 |
描述 |
| bootloader |
存放U-Boot镜像,通常为只读,不轻易升级。 |
| bootloader_env |
存放U-Boot的环境变量,A/B切换的核心逻辑所在。 |
| boot_a / boot_b |
存放A/B两套内核镜像(如zImage)。 |
| rootfs_a / rootfs_b |
存放A/B两套根文件系统。 |
| data |
存放用户数据与应用配置,该分区在A/B系统间共享,升级时不被擦除。 |
设计要点:
- boot_a 与 boot_b、rootfs_a 与 rootfs_b 的大小必须完全一致。
- bootloader_env 分区的大小需根据U-Boot配置确定,通常为128KB或256KB。
2. 三层协同机制
在深入细节前,我们先理清整个系统架构的层次关系。A/B分区的实现依赖于以下三个层次的协同工作:
┌─────────────────────────────────────────┐
│ 应用层(Linux 用户空间) │ ← 触发更新、确认启动
│ - OTA 更新程序 │
│ - 启动确认服务 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 引导层(U-Boot) │ ← 切换分区、自动回滚
│ - 环境变量管理 │
│ - bootcmd 启动脚本 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 存储层(eMMC 分区) │ ← 物理存储
│ - boot_a / boot_b │
│ - rootfs_a / rootfs_b │
│ - data(共享数据) │
└─────────────────────────────────────────┘
- 应用层:负责下载固件、写入非活动分区、触发更新标志。
- 引导层:负责读取更新标志、切换启动分区、处理启动失败。
- 存储层:提供物理隔离,保证两个分区相互独立。
3. U-Boot环境变量:A/B切换的“开关”
U-Boot的环境变量是实现A/B切换逻辑的核心。我们通过几个关键变量来控制系统的启动流程,这涉及到对系统底层内存管理的精细操作。
核心环境变量设计:
slot_active:标记当前哪个分区是活动分区(a 或 b)。
slot_updated:标记非活动分区是否刚刚被更新(1 或 0)。当OTA更新完成后,应用层会将其置为1。
boot_count:启动计数器。每次尝试从新分区启动时,该值递减。如果减到0仍启动失败,则判定新分区有问题,触发自动回滚。
bootcmd启动脚本逻辑:
bootcmd是U-Boot自动执行的命令,我们将在这里实现A/B切换的完整逻辑。其流程图如下:
启动 U-Boot
↓
检查 slot_updated 标志
↓
是否为 1?
├─→ 是:切换 slot_active,设置 boot_count=3,清除 slot_updated
└─→ 否:继续
↓
检查 boot_count 是否存在
↓
是否存在?
├─→ 是:减 1 并保存
│ ├─→ boot_count > 0:继续启动
│ └─→ boot_count = 0:回滚到另一个 slot,重启
└─→ 否:正常启动(没有重试机制)
↓
根据 slot_active 加载对应分区的内核和设备树
↓
启动内核
脚本逻辑解析:
- 升级检测:判断
slot_updated是否为1。如果是,说明OTA刚完成,需要将slot_active切换到另一分区,并初始化boot_count。
- 启动尝试与回滚:如果
boot_count存在,说明正处于“试用”新系统的阶段。每次启动都将其减一。当boot_count减至0,意味着新系统连续多次启动失败,U-Boot会自动切回原来的slot_active并重启,实现无人值守的回滚。
- 加载启动:根据
slot_active的值,从对应的boot_a/b和rootfs_a/b加载并启动系统。
为什么允许3次启动尝试? 这是一个经验值。有些系统第一次启动可能因初始化问题失败,第二次就能成功。3次是一个平衡尝试次数与回滚效率的值。在U-Boot的代码中也能看到类似设计。
4. Linux应用层触发与确认
U-Boot的脚本提供了自动化的切换和回滚能力,而Linux应用层则负责在合适的时机“触发”并最终“确认”这个流程。
OTA更新流程:
- 下载固件:应用从服务器下载新的固件包到
data分区。
- 安装固件:应用判断当前活动分区是A还是B,然后将新固件解压并写入到非活动分区(例如,当前是A,就写入B的
boot_b和rootfs_b)。
- 触发更新:使用
fw_setenv工具(U-Boot提供的用户空间工具)将slot_updated环境变量设置为1。
fw_setenv slot_updated 1
- 重启:执行
reboot命令,U-Boot将接管后续的切换工作。
新系统启动成功后的确认:
当新系统成功启动后,需要在应用层执行“确认”操作,告知Bootloader:“新系统运行稳定,以后就固定从这个分区启动了。” 这个操作的核心是清除boot_count环境变量。
#include <stdlib.h>
void confirm_boot_successful() {
// 检查系统是否稳定,例如网络、核心服务是否正常
if (is_system_stable()) {
// 系统稳定,清除 boot_count,锁定当前分区
system("fw_setenv boot_count");
printf("Boot successful, boot_count cleared.\n");
} else {
// 如果检查失败,可以直接执行 reboot
// U-Boot 会因为 boot_count 递减而最终触发回滚
printf("System unstable, rebooting for rollback...\n");
system("reboot");
}
}
这段C语言代码示例展示了如何在应用层进行启动确认,其中用到了system函数来调用shell命令,这要求开发者对程序与内核及系统环境的交互有清晰的理解。
5. 实战避坑指南
fw_setenv工具的配置:fw_setenv需要一个配置文件/etc/fw_env.config,用于指定环境变量分区所在的设备及偏移量。此配置必须与硬件存储布局完全匹配,否则无法正确读写环境变量。
- 环境变量的原子性:必须确保U-Boot在
saveenv(保存环境变量)期间具备掉电保护机制(通常采用双备份或CRC校验)。否则,环境变量在写入中途因断电而损坏,将导致系统无法启动。
- 共享数据分区的兼容性:
data分区是A/B系统共享的,必须确保新旧两个版本的系统都能兼容地读写其中的数据格式。在进行数据表结构或文件格式变更时需要格外谨慎,做好向前或向后兼容。
6. 总结
通过“分区设计 + U-Boot环境变量 + Linux应用层工具”三者的紧密结合,我们构建了一套健壮的A/B OTA升级系统。这套系统的核心优势在于,利用U-Boot的脚本能力,将复杂的启动失败检测与自动回滚逻辑下沉到Bootloader层,极大地简化了上层应用的设计复杂度。
掌握了这套实战方法,你不仅能为自己产品构建高可靠的OTA升级能力,更能对嵌入式系统的启动流程与健壮性设计有更深层次的理解。那么,新的问题来了:是否存在三备份(A/B/C)的分区方案?又如何进一步保证升级过程与固件本身的安全性呢?
