环境变量基础:什么是环境变量、存储位置与生命周期

各位同学,咱们今天聊聊U-Boot环境变量的底子。说白了,环境变量就是U-Boot运行时的一堆键值对——你设个bootargs,它就知道怎么传参数给内核;你改个baudrate,串口波特率就跟着变。我刚开始接触U-Boot时,以为这玩意儿就是个配置文件,后来踩了坑才明白,它的存储和生命周期才是真正要命的地方。

1. 环境变量到底是什么?

环境变量本质上是一块内存区域,里面存着key=value\0格式的字符串。U-Boot启动时,会从存储介质里把这块数据读到内存里,然后解析成一个哈希表。你敲printenv看到的就是这个哈希表的内容。

核心要点:环境变量不是文件,是二进制块。它有自己的校验机制(CRC32),数据坏了U-Boot会用默认值覆盖。

举个例子,我手头一块板子的环境变量区布局是这样的:

+------------------+  <-- 起始地址
| CRC32 (4字节)    |  <-- 对整个数据区的校验
+------------------+
| 数据区           |
| key1=value1\0    |
| key2=value2\0    |
| ...              |
| \0               |  <-- 结束标志
+------------------+

嗯,这里要注意:CRC32是U-Boot自己算的,你手动改flash里的数据不改CRC,U-Boot会直接丢弃整块数据。我遇到过有人用烧录器直接写flash,结果启动后环境变量全丢了——就是CRC没更新。

3. 存储位置:NOR、NAND、MMC、eMMC

不同存储介质,环境变量的存放方式天差地别。我按项目经验给大家捋一捋。

存储介质 特点 典型偏移 我踩过的坑
NOR Flash XIP执行,字节寻址,擦除按扇区 0x100000 附近 扇区大小不匹配,写穿到别的分区
NAND Flash 页读写,块擦除,有坏块问题 块号偏移 坏块导致环境变量区损坏
MMC/SD 块设备,按扇区读写 第1~2个扇区 分区表被误覆盖
eMMC 内置MMC控制器,有RPMB分区 BOOT分区或用户分区 硬件写保护导致保存失败

3.1 NOR Flash

NOR是最简单的场景。环境变量直接放在一个固定偏移地址,U-Boot通过指针访问。配置宏是CONFIG_ENV_IS_IN_FLASH,加上CONFIG_ENV_ADDR和CONFIG_ENV_SECT_SIZE。我习惯把环境变量放在U-Boot镜像后面,留足空间。

小技巧:NOR的环境变量区可以放两个备份(冗余环境变量),一个坏了自动用另一个。宏CONFIG_ENV_OFFSET_REDUND就是干这个的。

3.2 NAND Flash

NAND就麻烦多了。它有坏块,环境变量区如果落在坏块上,U-Boot会尝试跳到下一个好块。但问题是——如果连续坏块太多,环境变量就彻底丢了。我在一个项目里遇到过,NAND老化后坏块扩散,环境变量区正好中招,板子变砖。后来我强制把环境变量放到NAND的前几个块,并且开了ECC校验。

配置方式:

#define CONFIG_ENV_IS_IN_NAND
#define CONFIG_ENV_OFFSET     0x200000
#define CONFIG_ENV_SIZE       0x20000
#define CONFIG_ENV_RANGE      CONFIG_ENV_SIZE

3.3 MMC/eMMC

MMC和eMMC本质一样,都是块设备。环境变量通常存在某个固定扇区偏移。我见过两种做法:

  • 放用户分区头部:比如第0~1扇区,但容易被分区表覆盖。
  • 放BOOT分区:eMMC有独立的BOOT分区,硬件写保护更安全。

配置示例:

#define CONFIG_ENV_IS_IN_MMC
#define CONFIG_ENV_OFFSET     0x2000
#define CONFIG_ENV_SIZE       0x20000
#define CONFIG_SYS_MMC_ENV_DEV  0   // 使用mmc0

警告:MMC的环境变量保存是整块擦写的。如果你频繁调用saveenv,会加速MMC磨损。我见过一个产品每天保存100次,半年后MMC就挂了。

4. 环境变量的生命周期

生命周期这个词听着玄乎,其实就三个阶段:加载 → 使用 → 保存。我画了张图,大家一看就明白。

环境变量生命周期流程图 1. 加载阶段 2. 使用阶段 3. 保存阶段 加载子流程 ① 读取存储介质 ② 校验CRC32 ③ 解析到哈希表 ④ 失败则用默认值 使用子流程 ① 命令行读写 ② 脚本引用 ③ 内核传递 ④ 驱动读取 保存子流程 ① 计算新CRC32 ② 擦除存储区 ③ 写入数据 ④ 回读校验 关键说明 • 加载阶段:U-Boot启动时自动执行,不可跳过 • 使用阶段:运行时修改只影响内存,不写存储 • 保存阶段:必须显式调用 saveenv 才持久化

4.1 加载阶段

U-Boot启动流程里,board_init_f之后会调用env_init或env_relocate。这时候从存储介质读数据,校验CRC。如果校验失败,U-Boot会打印警告,然后使用default_environment——就是编译时写死的默认值。

我记得有一次调试,板子启动后bootargs总是被重置。查了半天,发现是NAND的坏块导致环境变量区读出来全是0xFF,CRC自然不对。U-Boot默默用了默认值,我改的配置全白费。

4.2 使用阶段

运行时,环境变量在内存里。你敲setenv改的只是内存里的哈希表,不会写回存储。这意味着:

  • 重启后修改丢失——除非你执行saveenv
  • 多个环境变量可以同时修改,最后一次性保存

经验之谈:我习惯在调试时先用setenv改参数,测试没问题了再saveenv。这样万一改错了,重启就恢复,不至于把板子搞死。

4.3 保存阶段

saveenv函数会把当前内存里的环境变量数据,重新计算CRC,然后擦除存储区并写入。这里有个性能问题:NOR和NAND的擦除很慢,一个扇区可能要几百毫秒。如果你在脚本里循环调用saveenv,启动时间会暴涨。

我曾经优化过一个项目,客户要求启动时间小于3秒。结果发现环境变量保存占了1.5秒——因为脚本里每次修改都调saveenv。改成最后统一保存后,启动时间直接砍半。

5. 环境变量的默认值与持久化

每个U-Boot编译时都会生成一个default_environment数组,里面是硬编码的默认值。这个数组定义在env/common.c里,由各种CONFIG_*宏拼接而成。

当存储介质里的环境变量损坏或不存在时,U-Boot就用这个默认值。所以:

  • 你改include/configs/xxx.h里的宏,会影响默认值
  • 但已经保存到存储里的环境变量不受影响——除非你手动env default -f -a强制恢复

注意:有些开发板出厂时,存储介质里是空的。第一次启动U-Boot会用默认值,但不会自动保存。你必须手动执行一次saveenv,否则下次启动还是默认值。我见过有人烧完镜像发现参数不对,折腾半天才发现是没保存。

6. 冗余环境变量

对于可靠性要求高的场景,U-Boot支持冗余环境变量。简单说就是存两份:一份主区,一份备份区。启动时先读主区,如果CRC校验失败,自动切到备份区。保存时同时更新两份。

配置方式:

#define CONFIG_ENV_OFFSET       0x100000
#define CONFIG_ENV_OFFSET_REDUND 0x120000
#define CONFIG_ENV_SIZE         0x20000
#define CONFIG_SYS_REDUNDAND_ENVIRONMENT

我在电力行业的项目里必须用这个——设备运行环境恶劣,flash可能出位翻转。冗余环境变量虽然多占一倍空间,但换来的是可靠性翻倍。

7. 小结

环境变量这块,说白了就是三件事:存哪里、怎么读、怎么改。不同存储介质有各自的脾气,NOR简单直接,NAND要防坏块,MMC/eMMC要注意磨损。生命周期里最容易被忽略的是“修改不自动保存”这个点——我见过太多人改完环境变量直接断电,然后跑来问我为什么没生效。

嗯,这一章的内容就到这儿。记住:环境变量是U-Boot和内核之间的桥梁,搞懂它的存储和生命周期,后面讲分区布局、启动参数传递时你才能游刃有余。