U-Boot 到底怎么切换升级分区?A/B 启动、环境变量与自动回滚 讲解
📺B站 嵌入式孙老师:博主个人介绍
📘博主书籍-京东购买链接*:Yocto项目实战教程
📘加博主微信,进技术交流群:jerrydev
U-Boot 到底怎么切换升级分区?A/B 启动、环境变量与自动回滚
做 A/B 升级时。例如瑞芯微 NXP 高通平台升级,最容易混淆的是:
- 要不要新增
U_BOOT_CMD()? bootcmd、bootargs、环境变量分别负责什么?- 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常见启动变量:
| 变量 | 作用 |
|---|---|
bootcmd | U-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_a2. 建议的环境变量
| 变量 | 示例 | 含义 |
|---|---|---|
boot_slot | A | 下一次启动的槽 |
rollback_slot | A | 失败后返回的槽 |
upgrade_available | 0 | 是否正在测试新系统 |
bootcount | 0 | 已尝试启动次数 |
bootlimit | 3 | 最大失败次数 |
rootfs_a_part | 2 | A 分区号 |
rootfs_b_part | 3 | B 分区号 |
默认值:
boot_slot=A rollback_slot=A upgrade_available=0 bootcount=0 bootlimit=3 rootfs_a_part=2 rootfs_b_part=3Linux 下检查分区:
lsblk-oNAME,SIZE,FSTYPE,PARTLABEL,PARTUUID,MOUNTPOINTScat/proc/cmdlineU-Boot 下检查:
=>part list mmc0=>printenvbootcmd=>printenvbootargs3. 设备号、标签和 PARTUUID 怎么选
| 写法 | 示例 | 评价 |
|---|---|---|
| 固定设备号 | /dev/mmcblk0p3 | 简单,但设备号可能变化 |
| 分区标签 | /dev/disk/by-partlabel/rootfs_b | 适合 Linux 升级程序 |
| PARTUUID | root=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_slot | rootfspart |
|---|---|
A | 2 |
B | 3 |
流程:
boot_slot │ ├── A → rootfs_a_part └── B → rootfs_b_part2. 生成 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.conf | distro boot |
boot.scr | U-Boot 脚本 |
| FIT Image | 内核、DTB、配置打包 |
boot.img | Android/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 | 正常启动,不计数 |
1 | bootcount加 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_slot5. 接入原来的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 回滚 A3. 环境变量持久化必须先验证
U-Boot 中测试:
=>setenv ota_test1234=>saveenv=>reset=>printenvota_testLinux 中继续测试:
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_setenv与saveenv是否操作同一区域 |
这三点确认后,A/B 升级的主体就清楚了:写备用分区、切换启动槽、健康检查、失败回滚。
