3、U-Boot深度解析:U-Boot的编译、配置与常用命令,环境变量与启动脚本

好,我们直接进入正题。U-Boot 这东西,说白了就是嵌入式系统的“第一道门”。你想想看,CPU 上电后,第一个跑起来的软件就是它。它得把硬件初始化好,然后把内核加载进内存,最后跳转过去执行。今天我就带你把它彻底搞明白。

3.1 U-Boot 的编译与配置

先聊聊编译。很多新手拿到 U-Boot 源码,第一反应就是直接 make。嗯,这肯定不行。U-Boot 支持几百种开发板,你得先告诉它“我是谁”。

我个人习惯,拿到一个板子,先看 configs/ 目录。这里全是默认配置。比如我手头这块 i.MX6ULL 的板子,对应的配置文件就是 mx6ull_14x14_evk_defconfig

配置分两步走:

  1. 加载默认配置make mx6ull_14x14_evk_defconfig
  2. 编译make -j4

这里有个坑。我在项目中遇到过,直接 make 会报错,提示缺少交叉编译工具链。你得先设置环境变量:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

为什么要指定 ARCH?因为 U-Boot 支持 x86、ARM、RISC-V 等多种架构。你不告诉它,它默认用本机编译器,编译出来的东西根本跑不了。

核心要点:U-Boot 的编译流程就是“配置 -> 编译”。配置决定了你的板子有哪些外设、多大内存、用什么驱动。编译则把源码变成二进制镜像。

编译完成后,你会得到几个关键文件:

  • u-boot.bin:纯二进制镜像,直接烧写到 SPI Flash 或 eMMC 的起始位置。
  • u-boot.imx:i.MX 系列专用的镜像,头部加了 IVT(Image Vector Table)和 DCD(Device Configuration Data)。
  • u-boot-dtb.bin:把设备树和 U-Boot 打包在一起,适合新版本。

你可能会问:“我该用哪个?” 这取决于你的启动方式。如果是 SD 卡启动,i.MX 系列必须用 .imx 格式。如果是 NOR Flash 启动,用 .bin 就行。

3.2 常用命令实战

U-Boot 启动后,进入命令行。这里我挑几个最常用的命令,每个都是我在调试中反复用到的。

3.2.1 内存操作命令

md(内存显示)和 mw(内存写)是调试利器。我曾经遇到一个内存颗粒时序问题,就是靠 md 反复读同一地址,发现数据偶尔跳变,才定位到问题。

# 查看 0x80000000 地址开始的 16 个字节
md 0x80000000 10

# 向 0x80000000 写入 0xdeadbeef
mw 0x80000000 0xdeadbeef

md 的第二个参数是长度,单位是 32 位字(4 字节)。所以 md 0x80000000 10 显示的是 64 字节。

3.2.2 网络命令

网络是 U-Boot 的灵魂。没有网络,你只能插拔 SD 卡或者用串口传文件,效率极低。

# 设置网络参数
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10
setenv netmask 255.255.255.0

# 测试网络连通性
ping 192.168.1.1

# 从 TFTP 服务器下载内核
tftp 0x80800000 zImage

这里要注意,serverip 是 TFTP 服务器的 IP。我刚开始总搞混,以为 serverip 是网关。其实网关是 gatewayip,如果你跨网段访问才需要设置。

小技巧:如果 ping 不通,先检查网线,再检查驱动。U-Boot 的网卡驱动有时会因 PHY 芯片复位不完整而挂掉。可以试试 phy_reset 命令强制复位。

3.2.3 存储设备命令

从 eMMC 或 NAND 启动时,mmcnand 命令是核心。

# 查看 MMC 设备信息
mmc list
mmc dev 0
mmc info

# 从 MMC 读取内核到内存
mmc read 0x80800000 0x800 0x4000

mmc read 的参数:目标内存地址、起始块号、块数量。一个块是 512 字节。所以 0x800 块就是 1MB 偏移,0x4000 块就是 8MB 数据。

3.3 环境变量

环境变量是 U-Boot 的“记忆”。你每次设置的 IP、启动参数,都保存在环境变量里。下次重启,它还能记住。

环境变量默认存储在 bootargsbootcmd 等固定位置。你可以用 printenv 查看所有变量:

U-Boot> printenv
baudrate=115200
bootargs=console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait
bootcmd=mmc dev 0; fatload mmc 0:1 0x80800000 zImage; fatload mmc 0:1 0x83000000 imx6ull.dtb; bootz 0x80800000 - 0x83000000
bootdelay=2
ethaddr=00:04:9f:04:d2:35
ipaddr=192.168.1.100
serverip=192.168.1.10

这里最核心的是 bootcmdbootargs

  • bootcmd:自动启动时执行的命令序列。说白了就是“开机后自动做的事”。
  • bootargs:传递给 Linux 内核的启动参数。比如控制台设备、根文件系统位置。

修改环境变量用 setenv,保存用 saveenv

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait'
saveenv

如果不执行 saveenv,修改只在本次启动有效。重启后恢复原样。我曾经犯过这个错,调试了一下午,每次重启都回到原点,后来才发现忘了保存。

警告saveenv 会把环境变量写入存储设备。如果存储设备坏了,或者分区表不对,保存会失败。建议先 mmc dev 0 确认设备正常,再执行保存。

3.4 启动脚本

启动脚本,其实就是把 bootcmd 里的命令写成一个脚本,方便管理和复用。U-Boot 支持两种脚本格式:

3.4.1 环境变量脚本

直接在 bootcmd 里写多行命令,用分号隔开:

setenv bootcmd 'mmc dev 0; fatload mmc 0:1 0x80800000 zImage; fatload mmc 0:1 0x83000000 imx6ull.dtb; bootz 0x80800000 - 0x83000000'

这种方式简单直接,但可读性差。命令一多,你根本看不清顺序。

3.4.2 独立脚本文件

更好的做法是写一个 boot.scr 文件。先创建文本文件 boot.txt

# boot.txt
mmc dev 0
fatload mmc 0:1 0x80800000 zImage
fatload mmc 0:1 0x83000000 imx6ull.dtb
bootz 0x80800000 - 0x83000000

然后用 mkimage 工具转换成 U-Boot 可识别的格式:

mkimage -A arm -O linux -T script -C none -n "Boot Script" -d boot.txt boot.scr

boot.scr 放到 SD 卡或 eMMC 的 FAT 分区。然后在 U-Boot 中执行:

fatload mmc 0:1 0x81000000 boot.scr
source 0x81000000

source 命令会解析并执行脚本中的每一行。这样做的好处是,你修改启动流程时,只需要替换 boot.scr 文件,不用重新编译 U-Boot。

我在项目中遇到过,客户要求支持两种启动方式:SD 卡启动和 eMMC 启动。我就写了两套脚本,通过 GPIO 电平判断,在 bootcmd 里用 if 语句选择加载哪个脚本。这样既灵活又安全。

3.5 实战:从零构建一个启动流程

好,理论说完了,我们来个实战。假设你要从 eMMC 启动 Linux,步骤如下:

  1. 设置网络(方便调试):setenv ipaddr 192.168.1.100; setenv serverip 192.168.1.10
  2. 下载内核和设备树tftp 0x80800000 zImage; tftp 0x83000000 imx6ull.dtb
  3. 写入 eMMCmmc dev 0; mmc write 0x80800000 0x800 0x4000
  4. 设置启动参数setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait'
  5. 设置自动启动setenv bootcmd 'mmc dev 0; mmc read 0x80800000 0x800 0x4000; mmc read 0x83000000 0x4800 0x800; bootz 0x80800000 - 0x83000000'
  6. 保存并重启saveenv; reset

这里 0x800 是内核在 eMMC 中的起始块,0x4000 是大小。设备树放在 0x4800 块,大小 0x800 块(1MB 足够)。

你可能会问:“为什么内核要放在 0x80800000?” 这是约定俗成的地址。对于 ARM 平台,内存起始地址通常是 0x80000000,内核加载地址偏移 8MB,给 U-Boot 自己留点空间。

嗯,到这里,U-Boot 的核心内容就讲完了。编译、配置、命令、环境变量、启动脚本,这五块吃透了,你就能驾驭绝大多数嵌入式系统的启动流程。下一章,我们聊聊 AB 分区切换,那才是真正的高可用设计。