4. Bootloader 深度解析:U-Boot 的启动流程、配置与编译、常用命令、环境变量、从 SD 卡启动

Bootloader 是嵌入式系统的「看门人」。上电后第一段代码就是它,负责把硬件初始化好,再把内核请进内存。市面上 Bootloader 不少,但 U-Boot 几乎成了事实标准。为什么?因为它支持架构多、社区活跃、可裁剪性极强。今天我们就把它彻底拆开看看。

本章核心脉络: U-Boot 的两阶段启动 → 配置裁剪与交叉编译 → 常用命令实战 → 环境变量机制 → SD 卡启动完整流程
U-Boot 启动全流程 上电复位 / CPU 从 0x0 取指 Stage 1:SPL / TPL(~4KB SRAM) 初始化 DDR / 时钟 / 串口 从存储介质加载完整 U-Boot 到 DDR Stage 2:完整 U-Boot 运行 读取环境变量 → bootcmd → 启动内核 SPL 是「最小引导程序」,负责把完整 U-Boot 从 SD/eMMC/NAND 搬到 DDR 里

4.1 U-Boot 的两阶段启动:SPL 与完整 U-Boot

很多人第一次接触 U-Boot 源码时,会被它的启动流程搞晕。其实说白了就两段:SPL(Secondary Program Loader)和完整 U-Boot。

为什么非要分两段?因为 SoC 上电后内部 SRAM 通常只有 4KB 到 128KB,这点空间放不下完整的 U-Boot。SPL 就是那个「瘦身版」的加载器,它只做三件事:

  • 初始化最基本的硬件(时钟、DDR 控制器、串口)
  • 从启动介质(SD 卡、eMMC、NAND Flash)读取完整 U-Boot
  • 跳转到 DDR 中的完整 U-Boot 入口

我记得第一次调一块 i.MX6 板子时,SPL 死活跑不起来。后来发现是 DDR 时序参数配错了,SPL 把数据读到错误地址,完整 U-Boot 校验失败直接死循环。嗯,从那以后我养成了一个习惯——SPL 阶段先点亮一个 LED 做心跳,确认它活着再往下调。

调试小技巧: 在 SPL 源码里加 puts("SPL alive\n"); 配合串口输出,是判断 SPL 是否运行的最快方法。别一上来就上 JTAG。

4.2 U-Boot 的配置与编译

U-Boot 的配置系统继承自 Linux 内核的 Kconfig,但又有自己的「板级配置」传统。我个人习惯把配置分为三步走:

  1. 选择板级默认配置 — 在 configs/ 目录下找到对应开发板的 _defconfig 文件
  2. 运行 menuconfig 微调 — 裁剪不需要的命令、驱动、文件系统支持
  3. 交叉编译 — 指定交叉工具链,生成 u-boot.bin 或 u-boot.imx

一个典型的编译流程如下:

# 1. 清理旧配置
make distclean

# 2. 加载板级默认配置(以 Raspberry Pi 3 为例)
make rpi_3_defconfig

# 3. 进入图形化配置界面(可选)
make menuconfig

# 4. 指定交叉编译器并编译
export CROSS_COMPILE=aarch64-linux-gnu-
make -j4

这里有个坑:U-Boot 的 _defconfig 文件只包含非默认的配置项。如果你用 make savedefconfig 生成新配置,它会自动去掉默认值,只保留差异。我曾经因为手动改了 .config 但没有执行 savedefconfig,导致版本管理里全是冗余配置项,后来同事接手时一脸懵。

注意: 不同 SoC 厂商对 U-Boot 有各自的补丁和打包工具。比如 NXP 的 i.MX 系列需要 mkimage 工具生成 .imx 格式镜像;Allwinner 平台则需要 sunxi-fel 工具。编译前务必阅读对应 README 文件。

4.3 U-Boot 常用命令

U-Boot 的命令行是个微型 shell,功能相当丰富。我把它按使用频率分成三组:

类别 命令 作用 使用频率
信息查询 bdinfo 显示板级信息(内存布局、时钟频率) ⭐⭐⭐⭐⭐
信息查询 printenv 打印所有环境变量 ⭐⭐⭐⭐⭐
内存操作 md / mw / mm 内存显示、写入、修改 ⭐⭐⭐⭐
存储操作 mmc read / mmc write 读写 SD/eMMC 设备 ⭐⭐⭐⭐⭐
网络操作 tftp / ping 通过网络加载文件 / 测试网络连通 ⭐⭐⭐⭐
文件系统 fatload / ext4load 从 FAT/ext4 分区加载文件到内存 ⭐⭐⭐⭐
启动相关 bootm / bootz / booti 启动内核镜像(uImage/zImage/Image) ⭐⭐⭐⭐⭐
脚本执行 run 执行一个环境变量中定义的命令序列 ⭐⭐⭐⭐

我最常用的调试组合是:

# 从 SD 卡加载内核到内存地址 0x42000000
fatload mmc 0:1 0x42000000 zImage

# 加载设备树到 0x43000000
fatload mmc 0:1 0x43000000 imx6ull.dtb

# 启动内核
bootz 0x42000000 - 0x43000000

你可能会问:为什么地址是 0x42000000 和 0x43000000?这取决于 SoC 的 DDR 地址映射。我一般会在 bdinfo 里确认 DRAM 起始地址和大小,然后选一个不会被内核覆盖的位置。曾经有次我把内核加载地址设得太低,结果解压时把自己覆盖了,板子直接挂掉——这种错误犯一次就记住了。

4.4 U-Boot 的环境变量

环境变量是 U-Boot 的灵魂。它不像 Linux 的环境变量那样只在当前会话有效,U-Boot 的环境变量是持久化存储的——保存在 Flash、SD 卡或 EEPROM 的固定区域里。

几个关键环境变量你必须理解:

  • bootcmd — 自动启动时执行的命令。上电后 U-Boot 会先等几秒(由 bootdelay 控制),如果没收到中断按键,就执行 bootcmd
  • bootargs — 传递给内核的启动参数。比如控制台设备、根文件系统位置、内存大小等
  • loadaddr — 默认的加载地址,很多命令会默认使用这个地址
  • fdt_addr — 设备树在内存中的地址

举个例子,一个典型的 bootcmd 可能是这样的:

setenv bootcmd 'fatload mmc 0:1 0x42000000 zImage; fatload mmc 0:1 0x43000000 imx6ull.dtb; bootz 0x42000000 - 0x43000000'
saveenv

这里有个细节:saveenv 会把当前所有环境变量写入存储介质。如果存储介质坏了或者分区表不对,saveenv 会失败。我曾经在一块 NAND Flash 的板子上遇到过——坏块管理没做好,环境变量保存几次后就损坏了,板子每次启动都恢复默认值。后来我改用 SD 卡的最后几个扇区来存环境变量,再也没出过问题。

环境变量调试技巧: 如果板子启动后环境变量总是「丢失」,先检查 CONFIG_ENV_OFFSET 和 CONFIG_ENV_SIZE 是否与存储介质的擦除块大小对齐。不对齐的话,保存时可能只写了部分数据。

4.5 从 SD 卡启动 U-Boot

从 SD 卡启动是嵌入式开发中最常见的场景。不同 SoC 对 SD 卡启动的布局要求不同,但大体思路一致:

  1. SD 卡前若干个扇区(通常是第 1~8 扇区)存放 SPL
  2. 紧接着存放完整 U-Boot 镜像
  3. 后续分区存放内核、设备树、根文件系统

以 i.MX6ULL 为例,烧写命令如下:

# 假设 SD 卡设备是 /dev/sdb(请务必确认!)
# 1. 烧写 SPL(偏移 1KB 处)
sudo dd if=SPL of=/dev/sdb bs=1K seek=1 conv=fsync

# 2. 烧写完整 U-Boot(偏移 69KB 处)
sudo dd if=u-boot.img of=/dev/sdb bs=1K seek=69 conv=fsync

# 3. 创建分区并格式化(使用 fdisk 或 parted)
# 第一个分区从 8MB 开始,格式化为 FAT32
sudo mkfs.vfat /dev/sdb1

为什么 SPL 要从第 1KB 开始?因为 i.MX6ULL 的 ROM 代码会跳过前 1KB(用于存放 IVT 头和 DCD 配置),直接从第 1KB 处加载 SPL。不同 SoC 的偏移量完全不同——永远以芯片参考手册为准。

血的教训: 我曾经在烧写时把 of=/dev/sdb 写成了 of=/dev/sda,直接把电脑的系统盘给覆盖了。从那以后,每次烧写 SD 卡前我都会执行 lsblk 确认设备名,并且拔掉所有不用的 USB 存储设备。你也应该这么做。

烧写完成后,把 SD 卡插入开发板,上电。如果一切正常,串口会输出类似这样的信息:

U-Boot SPL 2023.04 (Jan 15 2025 - 14:30:22 +0800)
Trying to boot from MMC1

U-Boot 2023.04 (Jan 15 2025 - 14:30:22 +0800)

CPU:   i.MX6ULL rev1.1 at 792 MHz
DRAM:  512 MiB
MMC:   FSL_SDHC: 0
In:    serial
Out:   serial
Err:   serial
Net:   FEC0
Hit any key to stop autoboot:  0
=>

看到这个 => 提示符,说明 U-Boot 已经成功从 SD 卡启动了。接下来你就可以输入命令、加载内核、调试驱动——整个嵌入式系统的舞台,从这一刻才算真正拉开帷幕。