4. U-Boot环境变量:存储位置与常用解析

环境变量这东西,说白了就是U-Boot的“记忆”。没有它,你每次上电都得手动敲命令,那还怎么搞自动化启动?我最早接触U-Boot时,总觉得环境变量不就是几个字符串嘛,有啥好研究的。直到有一次,板子启动到一半卡死,折腾了两天才发现是环境变量区被写坏了……嗯,从那以后我再也不敢小看它了。

4.1 环境变量存在哪?三种主流介质

U-Boot的环境变量不是存在内存里的——掉电就没了。它必须存在非易失性存储上。根据你的板子用啥存储,环境变量的位置和布局也不一样。我列一下最常见的三种:

存储介质 特点 典型分区名 我遇到的坑
SPI Flash 小容量(几MB),速度快,适合小系统 env, env_redundant 擦除寿命有限,频繁写会坏块
MMC/eMMC 大容量,可分区,主流方案 mmcblk0p1 或特定偏移 分区表搞错,环境变量被覆盖
NAND Flash 大容量,但需要ECC和坏块管理 mtdblock 分区 坏块导致环境变量丢失

关键点:环境变量在存储介质中通常以“键=值\0”的格式连续存放。U-Boot启动时会根据 CONFIG_ENV_IS_IN_SPI_FLASH 等宏定义,决定从哪个位置加载。

4.1.1 SPI Flash 上的布局

SPI Flash 上,环境变量通常放在一个独立的 sector 里。我习惯在分区表里给它留两个 sector——一个主区,一个冗余区。为啥要冗余?因为写环境变量时如果突然掉电,主区可能写一半就坏了,冗余区还能顶上。

// 典型配置(include/configs/myboard.h)
#define CONFIG_ENV_IS_IN_SPI_FLASH
#define CONFIG_ENV_OFFSET        0x100000   // 1MB 偏移
#define CONFIG_ENV_SIZE          0x20000    // 128KB
#define CONFIG_ENV_SECT_SIZE     0x10000    // 64KB 每扇区

你看,偏移设在 1MB 位置,是因为前面通常放了 SPL 和 U-Boot 本体。环境变量区不能跟它们重叠,否则一写环境变量就把启动代码覆盖了——我见过有人犯过这种错,板子直接变砖。

4.1.2 MMC/eMMC 上的布局

MMC 上更灵活。你可以把环境变量放在一个独立分区里,也可以放在某个分区的末尾。我个人更推荐独立分区,因为这样不容易被文件系统误伤。

// MMC 环境变量配置
#define CONFIG_ENV_IS_IN_MMC
#define CONFIG_SYS_MMC_ENV_DEV   0          // 使用 mmc 0
#define CONFIG_ENV_OFFSET        0x800000   // 8MB 偏移
#define CONFIG_ENV_SIZE          0x40000    // 256KB

小技巧:如果你用 MMC,可以在 U-Boot 命令行里用 mmc part 查看分区表,然后用 mmc read 直接读环境变量所在的扇区,确认数据是否正确。我曾经靠这招定位过一次环境变量被内核误写的问题。

4.1.3 NAND Flash 上的布局

NAND 比较特殊。它有坏块,所以环境变量不能简单地放在固定偏移。U-Boot 会扫描坏块表,找一个好的块来存放。配置时要注意:

// NAND 环境变量配置
#define CONFIG_ENV_IS_IN_NAND
#define CONFIG_ENV_OFFSET        0x200000
#define CONFIG_ENV_SIZE          0x20000
#define CONFIG_ENV_RANGE         CONFIG_ENV_SIZE * 4  // 预留 4 倍空间用于坏块替换

嗯,这里有个坑:CONFIG_ENV_RANGE 要设得比实际大小大几倍,否则坏块一出现,环境变量就没地方放了。我早期做 NAND 方案时,就因为这个参数设小了,导致批量生产的板子有 5% 启动失败。

4.2 常用环境变量解析

环境变量少说几十个,但真正关键的其实就那几个。你只要搞懂下面这三个,大部分启动问题都能定位。

4.2.1 bootcmd——启动的“总指挥”

bootcmd 是 U-Boot 自动启动时执行的命令。说白了,它就是一段脚本。U-Boot 启动后,如果倒计时内没被用户打断,就会执行 bootcmd 里的内容。

// 一个典型的 bootcmd 示例
bootcmd=run load_kernel; run load_dtb; bootm ${kernel_addr} - ${dtb_addr}

// 其中 load_kernel 和 load_dtb 也是环境变量
load_kernel=mmc read ${kernel_addr} 0x1000 0x8000
load_dtb=mmc read ${dtb_addr} 0x9000 0x1000

你看,bootcmd 本身不复杂,但它调用了其他变量。这种“变量套变量”的写法,我建议你尽量用,因为调试时只需要改一个子变量,不用动主流程。

注意:如果 bootcmd 执行失败(比如读不到内核),U-Boot 会进入命令行等待用户输入。这时候别慌,先 printenv 看看 bootcmd 是不是被改过。我遇到过好几次,同事调试时临时改了 bootcmd,忘了恢复,结果下一版固件启动不了。

4.2.2 bootargs——传给内核的“参数包”

bootargs 是 U-Boot 启动内核时,通过设备树或 ATAG 传给内核的命令行参数。内核用它来知道根文件系统在哪、控制台用哪个串口、内存多大等等。

// 一个典型的 bootargs
bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait

// 带网络启动的 bootargs
bootargs=console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/nfsroot ip=dhcp

这里我重点说一个坑:rootwait 这个参数。如果你的根文件系统在 MMC 或 USB 上,一定要加 rootwait,否则内核可能在设备还没准备好时就尝试挂载,导致 kernel panic。我当年调试一块 i.MX6 板子时,就因为这个参数漏了,折腾了整整一个下午。

参数 作用 典型值
console 指定控制台设备 ttyS0,115200
root 指定根文件系统位置 /dev/mmcblk0p2
rootwait 等待根设备就绪 (无参数值)
ip 网络配置 dhcp 或静态IP

4.2.3 serverip——网络启动的“目标地址”

serverip 是 TFTP/NFS 服务器地址。当你想通过网络加载内核或文件系统时,U-Boot 会向这个 IP 发请求。

// 设置 serverip
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.50
setenv netmask 255.255.255.0

// 然后用 tftp 下载内核
tftp ${kernel_addr} uImage

这里有个容易搞混的地方:serverip 是服务器的 IP,ipaddr 是板子自己的 IP。我见过有人把这两个设反了,结果 tftp 一直超时,还以为是网线坏了。

调试建议:网络启动时,先用 ping ${serverip} 确认连通性。如果 ping 不通,先查网线、交换机,再查 IP 配置。我个人的习惯是,在 bootcmd 里加一行 ping ${serverip},如果 ping 不通就自动 fallback 到本地启动。

4.3 环境变量存储与加载流程

为了让你更直观地理解整个过程,我画了一张流程图:

U-Boot环境变量加载与启动流程 U-Boot环境变量加载与启动流程 上电复位 SPL加载U-Boot到DRAM 从存储介质读取环境变量 校验CRC 成功 失败 使用环境变量启动 使用编译时默认值 执行 bootcmd 启动内核 环境变量损坏时,U-Boot会fallback 到默认值,但可能无法正常启动

从这张图你能看到,环境变量的读取发生在U-Boot启动早期。如果CRC校验失败,U-Boot会用编译时硬编码的默认值。这时候你 printenv 看到的变量可能不全,或者跟预期不一样——这就是一个典型的排查线索。

4.4 实操:查看与修改环境变量

最后,我快速过一下最常用的几个命令。这些你肯定知道,但我还是提一下,因为有些细节容易忽略。

// 查看所有环境变量
printenv

// 查看单个变量
printenv bootcmd

// 修改变量
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'

// 保存到存储介质(重要!)
saveenv

// 恢复出厂默认值
env default -a
saveenv

血的教训:很多人改了环境变量后忘了 saveenv,结果重启后变量又变回去了。另外,env default -a 会清空所有自定义变量,执行前最好先 printenv 备份一下。我曾经在客户现场手滑执行了这个命令,然后花了半小时重新配置所有网络参数……

好了,环境变量的存储位置和常用解析就讲到这里。你只要记住:bootcmd 管启动流程,bootargs 管内核参数,serverip 管网启目标。这三个搞明白了,大部分启动问题都能找到方向。