4、U-Boot环境变量基础:概念、存储与分类

环境变量这东西,说白了就是U-Boot的“记忆”。没有它,你每次重启都得重新告诉板子“从哪启动”、“内核文件名叫什么”、“IP地址是多少”。我刚开始接触U-Boot时,总觉得环境变量就是个配置文件,后来踩过几次坑才明白——它比配置文件复杂得多,也重要得多。

4.1 环境变量的概念

U-Boot环境变量是一组键值对,存储在非易失性存储介质中。键是字符串,值也是字符串。比如:

bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2
bootcmd=mmc read 0x42000000 0x800 0x4000; bootm 0x42000000
ipaddr=192.168.1.100
serverip=192.168.1.1

这些变量控制着U-Boot的几乎所有行为。从启动流程到网络配置,从内存布局到调试开关,全由它们说了算。

核心要点:环境变量是U-Boot与用户之间的“契约”。你设定规则,U-Boot按规则执行。一旦变量损坏或丢失,板子就可能变砖。

4.2 存储位置:不同介质,不同玩法

环境变量存哪儿?这取决于你的硬件平台。我经手过的项目里,NOR Flash、NAND Flash、MMC(eMMC/SD卡)、SPI Flash都遇到过。每种介质都有它的脾气。

存储介质 特点 典型偏移地址 注意事项
NOR Flash 支持XIP,字节寻址,擦除粒度大(64KB~128KB) 0x40000 ~ 0x60000 擦除前必须整块擦除,写入前要确保已擦除
NAND Flash 密度高,价格低,有坏块问题 0x200000 ~ 0x400000 需要坏块管理,环境变量通常存2份互为备份
MMC/eMMC 块设备,有分区表,读写方便 第1个分区的某偏移 注意分区对齐,避免被文件系统覆盖
SPI Flash 小容量,低成本,常用于IoT设备 0x100000 ~ 0x200000 写入速度慢,频繁擦写会缩短寿命

为什么偏移地址这么重要?因为U-Boot在启动早期就要读取环境变量,那时文件系统还没挂载,只能靠绝对地址去读。我曾经在一个项目里,因为SPI Flash的偏移地址算错了1个扇区,结果板子死活起不来,查了两天才发现是环境变量读到了全0xFF。

4.2.1 NOR/NAND Flash上的环境变量

NOR Flash上,环境变量通常放在U-Boot镜像之后的一个独立扇区。U-Boot会先读取这个扇区,如果校验通过就用它;如果校验失败(比如全0xFF或CRC错误),就回退到默认环境变量。

NAND Flash的情况更复杂。因为NAND有坏块,环境变量不能只存一份。我见过的主流做法是存两份:一份在主区域,一份在备份区域。如果主区域读出来有ECC错误,就自动切到备份。嗯,这里要注意——备份区域也得做坏块检测,否则可能两份都坏掉。

4.2.2 MMC/eMMC上的环境变量

MMC设备上,环境变量通常存放在一个专用分区或Boot分区里。U-Boot通过读写块设备的方式来操作它。我个人习惯把环境变量放在一个独立的、没有文件系统的小分区里,这样既不会被误删,也方便备份。

# 在U-Boot命令行中查看当前环境变量存储位置
U-Boot> env info
Environment:   MMC
Device:        mmc@fa00000
Interface:     MMC
Device number: 0
Partition:     0
Redundant:     No

4.2.3 SPI Flash上的环境变量

SPI Flash在IoT设备里很常见。它的容量小(通常8MB~32MB),但胜在引脚少、成本低。环境变量一般放在U-Boot镜像后面,紧挨着。我曾经在一个智能家居项目里,因为SPI Flash的擦写寿命只有10万次,而OTA升级每次都要更新环境变量,结果不到一年Flash就挂了。后来我加了磨损均衡,才解决问题。

4.3 默认环境变量 vs 用户环境变量

这是新手最容易混淆的概念。我简单解释一下:

  • 默认环境变量:编译U-Boot时,写在代码里的“出厂设置”。它在include/configs/目录下的头文件中定义,比如CONFIG_BOOTARGS、CONFIG_BOOTCOMMAND。这些变量是只读的,你改不了——除非重新编译U-Boot。
  • 用户环境变量:存储在Flash/MMC里的“当前设置”。你可以用setenv修改,用saveenv保存。重启后,U-Boot会先读用户环境变量;如果读不到或校验失败,就回退到默认环境变量。

我的经验:调试阶段,我经常故意把用户环境变量清空(env default -a),让U-Boot回到默认状态。这样可以排除“是不是我上次改坏了什么”的干扰。

两者的关系可以用一张图来表示:

默认环境变量与用户环境变量关系图 默认环境变量 vs 用户环境变量 编译阶段:默认环境变量 定义在 include/configs/ 头文件中 编译时写入 U-Boot 镜像 烧录阶段:默认环境变量写入存储 运行时:用户环境变量 存储在 Flash/MMC 的指定区域 可通过 setenv/saveenv 修改 启动时:优先读取用户环境变量 校验通过? 是 使用用户环境变量 否 回退到默认环境变量 说明 默认环境变量是“只读出厂设置” 用户环境变量是“可读写当前配置”

4.4 环境变量的生命周期

理解环境变量从哪来到哪去,对调试很有帮助。我总结了一个典型流程:

  1. 编译时:默认环境变量被编译进U-Boot二进制文件。
  2. 烧录时:U-Boot镜像(含默认环境变量)被写入存储介质。
  3. 首次启动:U-Boot检查存储介质中是否有有效的用户环境变量。没有?那就用默认的,并自动创建一份用户环境变量。
  4. 运行时:你可以用setenv修改变量,用saveenv保存到存储介质。
  5. 后续启动:U-Boot先读用户环境变量。如果校验失败(比如CRC错误),就回退到默认环境变量。

注意:如果你执行了env default -a但没有执行saveenv,那么重启后U-Boot读到的还是原来的用户环境变量。因为env default只改了内存中的值,没写回存储介质。我曾经因为这个疏忽,在客户现场折腾了半小时才反应过来。

4.5 环境变量的校验机制

U-Boot怎么知道读到的环境变量是完整的、没被篡改的?靠的是CRC32校验。每个环境变量块的开头都有一个CRC值,U-Boot读取后先算一遍CRC,跟存储的CRC比对。对不上?那就说明数据坏了,回退到默认。

环境变量在存储介质中的布局大致如下:

+------------------+  <-- 起始地址
| CRC32 (4字节)    |
+------------------+
| 数据长度 (4字节)  |
+------------------+
| 环境变量数据      |
| (键=值\0键=值\0) |
| ...              |
+------------------+
| 结束标记 (0x00)   |
+------------------+

这种设计很巧妙——你不需要知道环境变量总共有多少,读到结束标记就停。而且CRC保护了整个数据块,任何比特翻转都能被检测到。

4.6 实战:查看和操作环境变量

在U-Boot命令行里,最常用的几个命令:

命令 作用 示例
printenv 打印所有环境变量 printenv bootargs
setenv 设置或删除变量 setenv bootdelay 5
saveenv 保存到存储介质 saveenv
env default -a 恢复所有变量为默认值 env default -a
env info 查看环境变量存储信息 env info

举个例子,假设你想修改启动延时:

U-Boot> setenv bootdelay 3
U-Boot> saveenv
Saving Environment to MMC... OK
U-Boot> printenv bootdelay
bootdelay=3

你看,就这么简单。但要注意——saveenv这一步不能省。我见过太多人改了变量不保存,重启后一脸懵:“我明明改了啊!”

我的习惯:每次修改关键变量(比如bootargs、bootcmd)后,我都会先printenv确认一下,再saveenv。多花5秒钟,省得后面花5小时排查问题。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 环境变量空间不够:默认的环境变量缓冲区大小是CONFIG_ENV_SIZE,通常是4KB~16KB。如果你塞了太多变量,U-Boot会报错。我曾经在一个项目里,因为加了20多个WiFi配置参数,把环境变量撑爆了。后来只能扩大CONFIG_ENV_SIZE重新编译。
  • 存储介质坏块:NAND和SPI Flash都有坏块问题。如果环境变量恰好写在坏块上,U-Boot就读不到。解决方案是启用冗余环境变量(CONFIG_ENV_REDUND),存两份。
  • CRC不匹配:如果你手动用烧录器写了环境变量区域,但CRC没算对,U-Boot会认为数据损坏。这时候别慌,进U-Boot命令行执行env default -a; saveenv就能重建。

嗯,环境变量这部分内容就这些。理解了概念、存储位置和默认/用户的区别,后面讲OTA配置时你就能轻松上手了。