跳转至

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 加载对应分区的内核和设备树
     启动内核

脚本逻辑解析:

  1. 升级检测:判断 slot_updated 是否为 1。如果是,说明 OTA 刚刚完成,需要切换 slot_active 到另一个分区,并初始化 boot_count。
  2. 启动尝试与回滚:如果 boot_count 存在,说明正处于“试用”新系统的阶段。每次启动都将其减一。如果 boot_count 减到 0,说明新系统连续多次启动失败,U-Boot 会自动切换回原来的 slot_active 并重启,实现无人值守的回滚。
  3. 加载启动:根据 slot_active 的值,从对应的 boot_a/b 和 rootfs_a/b 加载并启动系统。

这里有一个看到 为什么允许 3 次启动尝试? 经验值。有些系统第一次启动可能因为初始化问题失败,但第二次就能成功。3 次是一个平衡值。在U-Boot代码中有看到。

4. Linux 应用层触发与确认

U-Boot 的脚本提供了自动化的切换和回滚能力,而 Linux 应用层则负责在合适的时机“触发”这个流程。

OTA 更新流程:

  1. 下载固件:应用从服务器下载新的固件包到 data 分区。
  2. 安装固件:应用判断当前活动分区是 A 还是 B,然后将新固件解压并写入到非活动分区(例如,当前是 A,就写入 B 的 boot_b 和 rootfs_b)。
  3. 触发更新:使用 fw_setenv 工具(U-Boot 提供的用户空间工具)将 slot_updated 环境变量设置为 1。 bash fw_setenv slot_updated 1
  4. 重启:执行 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 升级能力,更能对嵌入式系统的启动流程和健壮性设计有更深层次的理解。那问题又来了,那有没有那种,三个分区的?另外又如何保证升级安全性也是一些问题。