跳转到内容
新建笔记

U-Boot 加载、校验与存储更新:长度、失败处理和恢复

在 U-Boot 中,“文件已下载到 RAM”“内容校验通过”“已经写入存储”“下次可以启动”是四个不同结果。每个阶段都要使用对应对象的地址和长度,并检查返回状态。

本页按上游 U-Boot v2024.01 的命令行为整理。硬件分区、启动镜像格式和烧录偏移必须来自目标板的 BSP/ROM 文档;文中的 kernel_addr_r 等变量需先与实际内存布局核对。命令参数速查见U-Boot 命令参考。

1. 建立本次操作的对象清单

跳转到“1. 建立本次操作的对象清单”

开始前记录 version、bdinfo、printenv bootcmd bootargs,再分别确认:

对象必须知道的信息
内核zImage / Image / Legacy / FIT 等格式,加载和解压区,实际文件长度。
DTB准确板型、加载地址、完整性,以及供修改使用的额外空间。
initramfs有无镜像头,加载地址及长度;不能借用最后一次任意加载的 filesize。
存储目标设备、eMMC硬件分区、磁盘/MTD分区、偏移、容量及擦除/块大小。
恢复入口当前启动副本、配置备份、可用的 ROM 恢复或调试方法。

先将 autoload 和 autostart 配置为符合本次调试意图的值。试验性 setenv 不需要立刻 saveenv。不要把一块板的 0x80000000、MMC设备0、NAND分区名和SPI偏移组合成另一块板的更新脚本。

flowchart TD
A[确认板型、镜像、目标范围] --> B[加载一个对象到RAM]
B --> C{加载成功且长度合理?}
C -- 否 --> X[停止该分支并保留原启动内容]
C -- 是 --> D[立即保存该对象长度]
D --> E[核对格式与可信来源]
E --> F{临时启动或更新存储?}
F -- 临时启动 --> G[加载并检查其余启动对象]
G --> H[执行匹配镜像格式的启动命令]
F -- 更新 --> I[检查目标容量、对齐和擦除边界]
I --> J[写入、读回、比较]
J --> K[启动验证后再切换持久配置]

2. 先理解 filesize 的生命周期

跳转到“2. 先理解 filesize 的生命周期”

成功的 load / fatload / ext4load、网络加载和部分导出命令会更新 filesize。它保存的是最近一次相关操作的长度,不是“内核长度”这一永久含义。

假设内核为 0x600000 字节,DTB为 0x10000 字节:

tftpboot ${kernel_addr_r} zImage
setenv kernel_size ${filesize}
tftpboot ${fdt_addr_r} board.dtb
setenv fdt_size ${filesize}

两个加载命令都成功时,最终 kernel_size=600000、fdt_size=10000,而 filesize=10000。此时若拿 ${filesize} 写内核,只会按 DTB 的长度处理内核。

上面只解释长度保存位置。实际脚本必须先判断加载成功,再保存长度;失败之后 RAM 和旧环境值可能仍在,不能继续使用。

3. 从文件系统临时启动 Linux

跳转到“3. 从文件系统临时启动 Linux”

以下示例适用于已经确认支持 bootz 的 32 位 ARM zImage + DTB 流程。mmc 0:1 和 board.dtb 均须换成目标工程的准确值;不要用于 ARM64 Image。

if mmc dev 0; then
if load mmc 0:1 ${kernel_addr_r} zImage; then
setenv kernel_size ${filesize}
if load mmc 0:1 ${fdt_addr_r} board.dtb; then
setenv fdt_size ${filesize}
if fdt addr ${fdt_addr_r}; then
bootz ${kernel_addr_r} - ${fdt_addr_r}
else
echo Invalid-device-tree
false
fi
else
echo Device-tree-load-failed
false
fi
else
echo Kernel-load-failed
false
fi
else
echo MMC-selection-failed
false
fi

这个结构保证加载失败的分支不会继续到启动命令。它仍不负责证明板型正确、地址不重叠或镜像可信,这些属于对象清单中的前置条件。设备树能通过头部解析,也不等于它描述的外设和实际硬件一致。

内核命令行应来自对应板卡,例如根设备的 PARTUUID、控制台名称、文件系统类型和 rootwait。rootwait 让内核等待根设备出现,不会把未编入或未加载的驱动自动补齐。U-Boot 的网络设置也不会自动等同于 Linux 的网络配置。

在内核和 DTB 之外,再成功加载 initramfs,立即保存 ramdisk_size。原始 initramfs通常以 地址:大小 传入:

bootz ${kernel_addr_r} ${ramdisk_addr_r}:${ramdisk_size} ${fdt_addr_r}

Legacy/FIT封装的ramdisk有不同的解释路径,应按镜像格式处理,不能看到扩展名 .img 就确定是否需要显式长度。

4. 网络启动、NFS 与多设备回退

跳转到“4. 网络启动、NFS 与多设备回退”

TFTP 的 serverip 是服务器,ipaddr 是板卡自身地址。直连网络先检查网卡、链路、IP和子网,再检查服务器导出目录及文件读取权限。ping 成功不代表 TFTP 成功;初始请求使用 UDP 69,后续数据传输还涉及协商端口,防火墙应按实际流量放行,不能仅凭“开了69”判断。

setenv autoload no
setenv autostart no
setenv ipaddr 192.168.1.10
setenv serverip 192.168.1.100
setenv netmask 255.255.255.0
ping ${serverip}

上述地址仅为一个隔离开发网的例子。随后把第3节的 load 换成相应 tftpboot,保留同样的成功判断和独立长度变量。不要在 TFTP 失败后擦除旧镜像。

NFS有两种用途:U-Boot 的 nfs 命令把文件装入RAM;Linux 的 root=/dev/nfs、nfsroot=…、ip=… 指示内核挂载网络根文件系统。后者需要内核的网络、网卡、NFS root支持和服务器导出配置,不能只增加一行启动参数就保证可用。

多设备回退应把“选择设备、加载内核、加载DTB、格式检查、启动”作为完整分支。某个介质只加载成功内核,不能拿上一个分支残留的DTB启动。支持 Standard Boot 的工程可使用 bootflow,已有 extlinux 配置可考虑 sysboot;先确认目标版本提供这些入口。

mmc write 地址 起始块 块数 按块操作。设有效镜像长度为 LL 字节,块长为 BB 字节,需要写的块数为

N=⌈LB⌉=⌊L+B−1B⌋,P=NB−L.N=\left\lceil\frac{L}{B}\right\rceil =\left\lfloor\frac{L+B-1}{B}\right\rfloor, \qquad P=NB-L.

其中 PP 是最后一块需要补齐的字节数。在已经确认 B=512=0x200B=512=0x200 的情况下:

setexpr blocks ${image_size} + 0x1ff
setexpr blocks ${blocks} / 0x200
setexpr padded_size ${blocks} '*' 0x200
setexpr padding ${padded_size} - ${image_size}

例如 L=513=0x201:N=2,实际写入1024字节,尾部需补511字节。直接把 filesize=0x201 传给 mmc write 会请求513块,即262656字节,而不是513字节。

算出块数之后仍需验证:

  • 镜像长度非零、加法和乘法没有超出当前命令字宽。
  • 起始块与块数落在获准更新的完整范围内,且设备及eMMC硬件分区正确。
  • 源RAM和读回RAM各能容纳 padded_size,彼此及其他运行区域不重叠。
  • 最后一块的所有字节都属于目标镜像区域;不能为了补齐而覆盖其他文件或数据。

补齐尾部必须显式初始化,否则会把RAM中的不相关内容一起写入介质。对一个独占的原始镜像区域,可以在加载后的末尾补零;对文件系统文件,应使用 fatwrite / save 等文件接口,由文件系统处理分配,不自行算整卡块号。

原始写入步骤应逐步执行并检查状态:选择目标 → 填充尾部 → mmc write → mmc read 到独立缓冲区 → cmp.b 比较 padded_size。发现任何失败立即停止,不把最后打印一行“完成”当作结果。

6. SPI NOR、NAND 与 UBI 的更新边界

跳转到“6. SPI NOR、NAND 与 UBI 的更新边界”

先通过 sf probe 识别器件和擦除单元,再核对板级布局。sf erase 0 0x80000 不是通用的U-Boot预留区:有的设备需要SPL、ROM头、签名容器、冗余环境或多份引导镜像。

当偏移和长度都已经确定,sf update 可以比较已有内容并更新变更的擦除单元。它不是事务,也不自动保护最后一个擦除单元中镜像以外的数据。更新后应读回到另一块RAM,再比较准确长度;源镜像与目标Flash偏移不能凭常见数值推测。SPI Flash 命令

NAND还需区分页、擦除块、OOB、ECC和坏块跳过规则。Boot ROM读取引导镜像的布局,可能与普通 nand write 数据路径不同。写入需要满足的是实际命令/控制器的页及ECC约束,不能笼统说所有数据长度必须按擦除块对齐。

保持出厂坏块信息;不要把 nand scrub 当作常规格式化。nand erase.part kernel 也只有在当前分区定义确实对应目标板布局时才有意义。备份和读回校验要采用与目标格式一致的方式。

对已有的UBI卷,确认卷名和可用容量后可使用 ubi write 更新卷内容,UBI管理底层坏块。创建卷与分段更新是不同操作:

ubi part rootfs
ubi info layout

检查这两个查询结果后,再决定是否需要创建或更新特定卷。ubi write.part 向已有卷分段写入,不会自动创建文件系统或卷。给UBIFS卷写 .ubi 容器,或把 .ubifs 当原始NAND整机镜像,都会混淆层次。

涉及断电恢复的系统应设计冗余镜像、受保护的恢复入口或A/B机制。单副本“先擦再写”没有自动回滚能力,命令执行成功也不等于断电过程安全。

7. 环境备份与脚本封装

跳转到“7. 环境备份与脚本封装”

先确认 loadaddr 对应专用缓冲区、环境导出能放入其中、目标文件系统支持写入,再执行:

if env export -t ${loadaddr}; then
setenv env_backup_size ${filesize}
fatwrite mmc 0:1 ${loadaddr} uboot-env.txt ${env_backup_size}
else
echo Environment-export-failed
false
fi

不能固定保存 0x1000 或 0x10000:前者可能截断,后者可能把无关RAM内容写进文件。恢复时也使用本次成功加载的准确长度,并先审阅其中的MAC、网络、启动和设备特有变量;导入后先临时验证,最后才 saveenv。

在主机创建文本 boot.cmd,用对应工具封装,再在U-Boot中加载执行:

终端窗口
mkimage -A arm -O linux -T script -C none -n 'Board boot' -d boot.cmd boot.scr
if load mmc 0:1 ${loadaddr} boot.scr; then
source ${loadaddr}
else
echo Script-load-failed
false
fi

体系结构参数和内容应与工程一致。普通文本不能仅改名成 boot.scr;source 地址 arg1 arg2 也不是通用的参数传递语法。FIT脚本可携带签名,但是否真正强制验签仍取决于信任链配置。source 官方说明

仅在 bootcmd 中对内存变量 bootcount 加一,不保证跨掉电保留计数。应核对 CONFIG_BOOTCOUNT_LIMIT、实际计数后端、bootlimit、altbootcmd,以及操作系统确认健康后如何复位计数。也不要在每次启动随意 saveenv,把计数功能变成无控制的Flash写入。

8. 校验、签名与启动验证分别证明什么

跳转到“8. 校验、签名与启动验证分别证明什么”
检查能回答的问题不能单独证明
下载返回成功与长度本次协议加载是否完成、收了多少字节镜像适合该板或来自可信发布者
CRC32是否出现许多常见意外损坏抵抗主动伪造
SHA-256与可信预期摘要比较字节是否与可信基准一致预期摘要自身的来源可信性
FIT强制签名验证在正确配置和密钥保护下验证被签名对象整条系统链路不可绕过或具备防回滚
读回比较存储读回内容与预期字节一致ROM一定认得布局,内核一定能运行
实际重启及功能检查本板当前条件下能启动并运行检查项所有断电、温度或异常条件都已覆盖

不要编写仅含注释 # verify_signature ... 的脚本,却在其末尾打印“安全验证通过”。同样,crc32 打印摘要之后不会自然产生名为 crc32_result 的变量;校验值的获取、可信基准和比较都要真实实现。

9. 失败时沿着边界定位

跳转到“9. 失败时沿着边界定位”
  1. 加载失败:设备选择、分区、路径、网络连通和权限;此时不擦除目标。
  2. 格式失败:确认所用启动命令与镜像封装匹配,检查是否截断、压缩格式不符或使用旧长度。
  3. 写入失败:设备容量、分区范围、块/页/擦除对齐、写保护、坏块/ECC、返回状态。
  4. 读回一致但无法启动:ROM头、引导偏移、入口、DTB板型、内核命令行、签名配置和地址覆盖。
  5. 一次成功但偶发失败:供电、介质、时序和断电恢复机制,需要硬件与系统层证据,不能只重复烧录。

本页的数值推导与软件检查不能代替目标板烧录验收。实际项目应保存版本、命令、返回结果和镜像摘要;从中区分“已核对的规则”“已在模拟环境运行的脚本”和“已在板上验证的行为”。

本次在本地编译的 U-Boot v2024.01 sandbox 中验证了块数取整、两次加载后的长度保存、环境导出/导入、Legacy 脚本封装及加载失败分支。失败分支测试用主机临时文件和状态标记代替 MMC 与 ARM 内核启动,因此只证明所用 Hush 控制结构,不证明实际板卡可启动。