U-Boot 到底怎么切换升级分区?A/B 启动、环境变量与自动回滚 讲解

📅 2026/7/18 21:56:50 👁️ 阅读次数 📝 编程学习
U-Boot 到底怎么切换升级分区?A/B 启动、环境变量与自动回滚 讲解

📺B站 嵌入式孙老师:博主个人介绍

📘博主书籍-京东购买链接*:Yocto项目实战教程

📘加博主微信,进技术交流群jerrydev


U-Boot 到底怎么切换升级分区?A/B 启动、环境变量与自动回滚

做 A/B 升级时。例如瑞芯微 NXP 高通平台升级,最容易混淆的是:

  • 要不要新增U_BOOT_CMD()
  • bootcmdbootargs、环境变量分别负责什么?
  • Linux 写完备用分区后,怎么让 U-Boot 启动它?
  • 新系统启动失败,怎么切回旧分区?

实际核心只有一句话:

升级程序负责“写哪个分区”,U-Boot 环境变量负责“下次启动哪个分区”。

完整链路如下:

当前运行 A │ ├── 升级程序写入 B │ ├── 设置 boot_slot=B │ └── 重启 ↓ U-Boot 启动 B │ ├── 系统正常:确认 B └── 多次失败:回滚 A

一、先分清 U-Boot 的三类机制

机制典型写法触发方式主要作用
驱动U_BOOT_DRIVER()设备树匹配驱动硬件设备
命令U_BOOT_CMD()用户输入命令执行一段 C 代码
环境变量setenv/saveenv启动脚本或用户修改控制启动行为

1.U_BOOT_DRIVER:处理设备

例如:

staticconststructudevice_idadc_key_bus_match[]={{.compatible="adc-keys"},{},};U_BOOT_DRIVER(adc_key_bus)={.name="adc_key_bus",.id=UCLASS_SIMPLE_BUS,.of_match=adc_key_bus_match,.bind=adc_key_bus_bind,};

设备树:

adc-keys { compatible = "adc-keys"; };

匹配关系:

DTS compatible ↓ of_match ↓ U_BOOT_DRIVER ↓ bind / probe

它解决的是:

这个硬件节点由哪个驱动处理。

2.U_BOOT_CMD:注册命令

例如:

staticintdo_hello(structcmd_tbl*cmdtp,intflag,intargc,char*constargv[]){printf("Hello U-Boot\n");return0;}U_BOOT_CMD(hello,1,0,do_hello,"print hello message","");

命令行执行:

=>hello Hello U-Boot

它解决的是:

输入某条命令后,执行哪段代码。

3. 环境变量:控制启动流程

例如:

=>setenv boot_slot B=>setenv upgrade_available1=>saveenv

常见启动变量:

变量作用
bootcmdU-Boot 最终执行的启动脚本
bootargs传给 Linux 内核的参数
boot_slot选择 A 或 B
bootcount记录启动尝试次数
bootlimit最大允许失败次数
upgrade_available是否正在试启动新系统

A/B 升级大多数情况下只需要环境变量,不需要重新写命令。

二、A/B 升级需要哪些配置

1. 分区布局

一个简单布局:

分区用途
boot内核、DTB、启动文件
rootfs_a当前系统
rootfs_b备用升级系统
data用户数据
uboot-env可选,保存 U-Boot 环境

示例:

mmcblk0p1 boot mmcblk0p2 rootfs_a mmcblk0p3 rootfs_b mmcblk0p4 data

升级状态:

当前系统:rootfs_a 升级目标:rootfs_b 回滚目标:rootfs_a

2. 建议的环境变量

变量示例含义
boot_slotA下一次启动的槽
rollback_slotA失败后返回的槽
upgrade_available0是否正在测试新系统
bootcount0已尝试启动次数
bootlimit3最大失败次数
rootfs_a_part2A 分区号
rootfs_b_part3B 分区号

默认值:

boot_slot=A rollback_slot=A upgrade_available=0 bootcount=0 bootlimit=3 rootfs_a_part=2 rootfs_b_part=3

Linux 下检查分区:

lsblk-oNAME,SIZE,FSTYPE,PARTLABEL,PARTUUID,MOUNTPOINTScat/proc/cmdline

U-Boot 下检查:

=>part list mmc0=>printenvbootcmd=>printenvbootargs

3. 设备号、标签和 PARTUUID 怎么选

写法示例评价
固定设备号/dev/mmcblk0p3简单,但设备号可能变化
分区标签/dev/disk/by-partlabel/rootfs_b适合 Linux 升级程序
PARTUUIDroot=PARTUUID=xxxx适合 U-Boot 传给内核

建议:

升级程序写分区:使用 PARTLABEL 内核查找 rootfs:使用 PARTUUID

三、U-Boot 如何完成分区选择和回滚

1. 根据boot_slot选根分区

"select_rootfs="\"if test \"${boot_slot}\" = \"A\"; then "\"setenv rootfspart ${rootfs_a_part}; "\"else "\"setenv rootfspart ${rootfs_b_part}; "\"fi\0"

执行结果:

boot_slotrootfspart
A2
B3

流程:

boot_slot │ ├── A → rootfs_a_part └── B → rootfs_b_part

2. 生成 Linux 根文件系统参数

推荐使用PARTUUID

"prepare_rootfs="\"part uuid mmc 0:${rootfspart} rootuuid; "\"setenv bootargs \"${bootargs_base} "\"root=PARTUUID=${rootuuid} rootwait rw\"\0"

逻辑:

rootfspart ↓ 读取对应分区 PARTUUID ↓ 生成 bootargs ↓ Linux 挂载对应 rootfs

但要注意,root=不一定来自bootargs,它也可能在:

位置常见场景
extlinux.confdistro boot
boot.scrU-Boot 脚本
FIT Image内核、DTB、配置打包
boot.imgAndroid/Rockchip 启动方式
环境变量传统 U-Boot 启动

检查:

cat/proc/cmdlinecat/boot/extlinux/extlinux.conf

源码中搜索:

grep-R"root=/dev/mmcblk".grep-R"CONFIG_BOOTCOMMAND".grep-R"CONFIG_EXTRA_ENV_SETTINGS".

3. 试启动计数

"check_upgrade="\"if test \"${upgrade_available}\" = \"1\"; then "\"setexpr bootcount ${bootcount} + 1; "\"saveenv; "\"fi\0"

状态关系:

upgrade_available行为
0正常启动,不计数
1bootcount加 1

4. 超过次数后回滚

"rollback_upgrade="\"echo Rollback to slot ${rollback_slot}; "\"setenv boot_slot ${rollback_slot}; "\"setenv upgrade_available 0; "\"setenv bootcount 0; "\"saveenv\0"

完整判断:

"check_upgrade="\"if test \"${upgrade_available}\" = \"1\"; then "\"setexpr bootcount ${bootcount} + 1; "\"echo Trial boot ${bootcount}/${bootlimit}; "\"if test ${bootcount} -gt ${bootlimit}; then "\"run rollback_upgrade; "\"else "\"saveenv; "\"fi; "\"fi\0"

流程:

upgrade_available=1 ↓ bootcount + 1 ↓ bootcount > bootlimit? ┌────┴────┐ │ │ 否 是 │ │ 启动新系统 切回 rollback_slot

5. 接入原来的bootcmd

先查看:

=>printenvbootcmd

假设原来是:

bootcmd=run distro_bootcmd

保存原始启动逻辑:

normal_bootcmd=run distro_bootcmd

重新组合:

bootcmd= run check_upgrade; run select_rootfs; run prepare_rootfs; run normal_bootcmd

最终启动链路:

bootcmd │ ├── check_upgrade │ └── 试启动计数或回滚 │ ├── select_rootfs │ └── 选择 A/B │ ├── prepare_rootfs │ └── 生成 root=PARTUUID │ └── normal_bootcmd └── 执行原有启动流程

四、Linux 与 U-Boot 如何形成升级闭环

1. Linux 写入非活动分区

假设当前运行 A:

findmnt /

升级程序应写 B:

ddif=system.ext4\of=/dev/disk/by-partlabel/rootfs_b\bs=4Mconv=fsync

升级顺序必须是:

写备用分区 ↓ 刷新数据 ↓ 校验镜像 ↓ 修改 boot_slot ↓ 重启

对应命令:

fw_setenv rollback_slot A fw_setenv boot_slot B fw_setenv upgrade_available1fw_setenv bootcount0syncreboot

不要反过来:

错误顺序: 先设置 boot_slot=B 再写 rootfs_b

否则写入中途掉电,U-Boot 可能启动一个不完整的系统。

2. 新系统启动后确认成功

新系统启动到 Shell,不代表升级成功。

建议至少检查:

检查项目的
根文件系统确认挂载正常
关键驱动确认硬件可用
网络确认通信正常
核心应用确认业务正常
数据分区确认数据可访问
版本号确认升级版本正确

确认成功后:

fw_setenv rollback_slot B fw_setenv upgrade_available0fw_setenv bootcount0

示例脚本:

#!/bin/shset-euslot="$(fw_printenv-nboot_slot)"systemctl is-active--quietapplication.service mountpoint-q/ fw_setenv rollback_slot"${slot}"fw_setenv upgrade_available0fw_setenv bootcount0echo"Slot${slot}marked successful"

完整闭环:

Linux A 运行 ↓ 写入 Linux B ↓ 设置 boot_slot=B ↓ U-Boot 启动 B ↓ B 执行健康检查 │ ├── 成功 │ ├── 清除 upgrade_available │ └── B 成为稳定槽 │ └── 失败 └── U-Boot 回滚 A

3. 环境变量持久化必须先验证

U-Boot 中测试:

=>setenv ota_test1234=>saveenv=>reset=>printenvota_test

Linux 中继续测试:

fw_printenv ota_test fw_setenv ota_test5678

再次进入 U-Boot:

=>printenvota_test

预期结果:

ota_test=5678

只有这样才能证明:

U-Boot saveenv 和 Linux fw_setenv ↓ 读写同一份环境

需要确认:

配置必须一致
U-Boot 环境存储位置CONFIG_ENV_OFFSET
环境大小CONFIG_ENV_SIZE
Linux 配置fw_env.config
磁盘布局WIC/GPT 保留区域

如果 offset 配错,可能覆盖:

GPT loader U-Boot boot 分区 rootfs

总结

A/B 升级不是一条 U-Boot 命令,而是一套状态切换机制:

分区负责保存系统 环境变量负责保存状态 bootcmd 负责选择系统 Linux 负责确认成功 U-Boot 负责失败回滚

普通 A/B 升级通常只需要已有命令:

setenv saveenv run test setexpr part mmc booti

只有涉及特殊硬件或私有状态存储时,才需要新增:

U_BOOT_CMD(...)

真正需要优先确认的是:

核心问题检查内容
环境能否保存saveenv后重启是否还在
root 参数来自哪里bootargs、extlinux、boot.scr 或 boot.img
Linux 与 U-Boot 是否一致fw_setenvsaveenv是否操作同一区域

这三点确认后,A/B 升级的主体就清楚了:写备用分区、切换启动槽、健康检查、失败回滚。