OTA A/B 分区:基于 U-Boot 的环境变量介绍¶
在上一篇文章中,我们深入探讨了嵌入式系统 OTA(Over-The-Air)固件升级的必要性与核心机制。其中,A/B 分区方案作为实现高可靠性、无缝升级和安全回滚的关键技术,被反复提及。它解决了传统单分区升级可能导致的“变砖”风险,极大地提升了设备的健壮性和用户体验。然而,理解其原理只是第一步,如何在实际项目中落地 A/B 分区,特别是如何利用 Bootloader(如 U-Boot)进行分区切换,才是摆在工程师面前的真正挑战。
本文将聚焦于 A/B 分区的实战实现,简单看看如何设计分区表、配置 U-Boot 环境变量,并编写 Linux 侧的切换逻辑,从而构建一个稳定可靠的 A/B 升级系统。以 U-Boot 作为 Bootloader 的典型场景进行讲解,力求提供一份可操作的实践指南。主要对理论知识进行概述介绍,有兴趣可以查看瑞芯微/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。 bash 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");
}
}
5. 实战避坑指南¶
fw_setenv 工具的配置:fw_setenv 需要一个配置文件 /etc/fw_env.config 来知道环境变量分区在哪个设备、偏移量是多少。这个配置必须和硬件完全匹配。
环境变量的原子性:确保 U-Boot 在 saveenv 期间有掉电保护机制(通常是双备份或 CRC 校验),否则环境变量损坏会导致系统无法启动。
共享数据分区的兼容性:data 分区是共享的,要确保新旧两个版本的系统都能兼容地读写其中的数据。例如,数据库 schema 的变更需要谨慎处理。
6. 总结¶
通过“分区设计 + U-Boot 环境变量 + Linux 应用层工具”三者的结合,我们成功构建了一套健壮的 A/B 升级系统。这套系统的核心在于利用 U-Boot 的脚本能力,实现了启动失败后的自动回滚,将复杂的逻辑下沉到 Bootloader,极大地简化了上层应用的设计。
掌握了这套实战方法,你不仅能为你的产品构建起高可靠的 OTA 升级能力,更能对嵌入式系统的启动流程和健壮性设计有更深层次的理解。那问题又来了,那有没有那种,三个分区的?另外又如何保证升级安全性也是一些问题。