U-Boot的环境变量,跑起来其实有三套逻辑:Bootloader运行时用的、Flash上持久化的、还有Linux用户态 fw_printenv / fw_setenv 自己内置的。调试时最容易踩的坑,是把U-Boot内置的默认环境和fw工具内置的默认环境当成一回事——它们根本是两套各自编译生成的 default_environment 数组。本文基于君正平台(uboot.2013)上实际踩过的坑,将工作机制、冗余备份、常见故障根因整理一遍,加深印象。
一 先搞清环境变量存在哪
1. 存储结构 env_t
环境变量在Flash上是一个二进制结构体,分普通版和冗余备份版(也即主/备份环境变量)两种。
// 非冗余模式
struct env_image_single {
uint32_t crc; /* CRC32:对后续全部data区做校验 */
char data[]; /* 变量区:name=value\0name2=value2\0\0,双\0标记结束 */
};
// CONFIG_SYS_REDUNDAND_ENVIRONMENT 开启冗余备份
struct env_image_redundant {
uint32_t crc;
unsigned char flags; /* 版本标记:NOR用布尔ACTIVE/OBSOLETE;NAND/UBI用递增序列号 */
char data[];
};
三个字段各管一件事:
crc:完整性校验。Flash读出来后重算crc和头部比对,对不上就判定环境损坏。
flags:冗余模式专用,区分主/备份哪个分区是当前有效副本。
data:真正的变量存储区,连续 name=value\0,末尾两个 \0 结束。
存在哪个偏移由 CONFIG_ENV_OFFSET、CONFIG_ENV_OFFSET_REDUND 决定。我们平台在这里有个特殊布局:SPI-NAND上env分区嵌套在u-boot的MTD分区内部(0xc0000、0xe0000),不是一个独立MTD分区——这个布局在后面第三节埋了好几个坑。
2. U-Boot内部的两套数据源
U-Boot内部实际有两套数据源,优先级很明确。
第一套:Flash上的持久化环境(优先)。 上电后U-Boot从指定Flash偏移读 env_t 镜像,CRC校验通过就把变量解析到内存哈希表 env_htab,作为运行时环境。setenv 只改内存哈希表,不写Flash;真正落盘是 saveenv,把哈希表导出成完整 env_t 镜像,擦写Flash环境分区。
第二套:编译进 u-boot.bin 的 default_environment(故障兜底)。 编译U-Boot时,include/env_default.h 根据Board配置宏生成静态字符数组,编进 u-boot.bin 的 rodata 只读段:
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
只有三种情况会用到它:Flash环境分区读取失败、CRC校验错误、Flash被擦成全0xFF。串口会打一行:
*** Warning - bad CRC, using default environment
这里有个很容易误解的点:CONFIG_BOOTARGS 只是故障兜底值。只要Flash上env的CRC正常,这个宏压根不生效——我们改了宏重新编译,也覆盖不了已经 saveenv 进Flash的变量。
3. 启动加载 env 的完整流程
把上面串起来就是:
env_init():先把 gd->env_addr 指向内置默认环境,gd->env_valid=0;
env_relocate():调平台实现 env_relocate_spec()(env_sf.c / env_nand.c / env_sfc_nand.c),从Flash读env镜像到内存buffer,再走 env_import() 做CRC校验;
- CRC正常:解析 name=value 构建内存哈希表
env_htab,gd->env_valid=1,用Flash里的环境;
- CRC失败:调
set_default_env(),导入 u-boot.bin 内置的 default_environment,gd->env_valid=0。
之后就是老套路:setenv 改内存哈希,saveenv 导出镜像写Flash。
4. 冗余环境
CONFIG_SYS_REDUNDAND_ENVIRONMENT
开了冗余后,主、备份两份env镜像分别放两个Flash偏移,saveenv 不会覆盖当前在用的那个,总是写对端备份分区。选哪个生效靠 flags:
NOR Flash: flags=1(ACTIVE有效)/ flags=0(OBSOLETE废弃),布尔标记;
NAND / SPI-NAND / UBI: flags 用递增序列号,数值大的算新版本。
上电时两份都读,CRC+flags一起决定用哪份。设计目的是防 saveenv 写Flash中途掉电——两套副本不会同时坏。但别指望它万能:两份都被擦坏,照样回退内置 default_environment。
二 fw_printenv / fw_setenv:Linux用户态的另一个玩家
源码在 u-boot/tools/env,独立编译成可执行文件,跑在Linux用户空间,直接操作MTD设备——读写的和U-Boot是同一块Flash环境分区。头文件 fw_env.h,主逻辑 fw_env.c,命令行入口 fw_env_main.c。
1. 工具一次调用的完整流程
fw_setenv/fw_printenv命令行
->拿排他锁 /var/lock/fw_printenv.lock,防止多进程并发写Flash
->fw_env_open()
->parse_config():读fw_env.h编译宏或fw_env.config,拿到MTD设备、env偏移、大小、擦除块大小
->读Flash主、备份env镜像到内存buffer,做CRC校验
->按CRC+flags选当前有效镜像
->CRC全挂:加载fw工具自己内置的default_environment数组
->fw_printenv():解析内存镜像打印环境变量
->fw_setenv():在RAM镜像里做增删改(fw_env_write只动内存,不写Flash)
->fw_env_close():重算CRC,走MTD接口擦写Flash环境分区
2. 最容易踩坑:fw工具也有一份独立的 default_environment
这是整个体系里最容易绕晕的地方。fw_printenv / fw_setenv 是独立编译的ELF程序,编译 tools/env 的时候同样会 include board 头文件,生成一份属于工具自己的 default_environment 静态数组,编进 fw_printenv 二进制的 rodata 段。
| 对象 |
default_environment放哪 |
默认环境被触发时打什么 |
| U-Boot Bootloader |
u-boot.bin |
*** Warning - bad CRC, using default environment |
| fw_printenv 工具 |
/usr/bin/fw_printenv |
Warning: The environment is empty ... loading default environment Success! |
两份默认环境完全相互独立。Flash上env分区CRC校验失败时,fw_printenv 不会去翻 u-boot.bin,直接输出工具自己内置的那份;U-Boot上电才会读 u-boot.bin 里的那份。如果编译fw工具时的Board宏和编译U-Boot bin时不一致,两套默认环境内容就对不上。
这次在项目中我就栽过这个坑:迭代改过 CONFIG_BOOTARGS 里的 mem 内存参数,U-Boot更新了,文件系统里的 fw_printenv 还是旧版(没有重新编译过)。结果Flash env一坏,fw工具打印出旧的 mem=224M,U-Boot上电实际跑的是新配置。当时在U-Boot源码里怎么搜都搜不到 mem=224M——这字符串根本不在 u-boot.bin 里,结果分析了一圈居然是躺在 fw_printenv 工具二进制里的:
# 在fw_printenv二进制里找旧字符串,确认它在工具内部
strings /usr/bin/fw_printenv | grep "mem=224M"
# u-boot.bin里不会有这个旧字符串
strings u-boot.bin | grep "mem=224M"
记住一句话:Linux下 fw_printenv 打出来的“默认环境”,不代表板子上电后U-Boot真实的默认环境。 U-Boot真实默认环境,以U-Boot串口控制台输出为准。
3. fw工具与U-Boot的配置对齐是硬要求
fw_env.h 里的编译宏必须和U-Boot配置一一对齐:
| 宏 |
对应 |
| DEVICE1_NAME / DEVICE2_NAME |
MTD设备节点 |
| DEVICE1_OFFSET / DEVICE2_OFFSET |
env主、备份Flash偏移 |
| ENV1_SIZE / ENV2_SIZE |
环境分区大小 |
| DEVICE1_ESIZE |
Flash擦除块大小 |
| HAVE_REDUND |
冗余开关,对应CONFIG_SYS_REDUNDAND_ENVIRONMENT |
这些对不齐,fw工具读写偏移就错位,直接把U-Boot环境分区写坏。
4. 变量校验机制
fw_env.c 里同样实现了和U-Boot本体同源的 env_flags 逻辑,支持变量只读、禁止删除、禁止覆盖这类访问权限控制,保证Bootloader和Linux用户态的约束一致。
5. 两种编译配置模式
编译期硬编码(我们项目用的):fw_env.h 里写死MTD、offset、size宏;
运行时配置文件:定义 CONFIG_FILE="/system/etc/fw_env.config",工具启动时读文本配置文件。
三 SPI-NAND平台踩坑复盘
先交代分区布局,前面768K存放uboot,紧接着128K存放主env,最后128K存放备份env:
mtd0: 1M(uboot),0 ~ 0xFFFFF
├─ uboot程序:0 ~ 0xc0000
├─ 主env分区:0xc0000 ~ 0xdFFFF
└─ 备份env分区:0xe0000 ~ 0xFFFFF
注意env分区是嵌套在uboot这个 mtd0 分区内部的,不是独立分区——后面几个坑都跟这个布局有关。
现象1:flashcp烧了uboot,环境变量全丢
flashcp u-boot.bin /dev/mtd0
flashcp 的做法是先把整个MTD分区整体擦除,再写镜像。我们的 u-boot.bin 只有455K,不会溢出到env区,但 flashcp 把整个1M的 mtd0 都擦成了0xFF,嵌套在末尾的主/备份env分区连坐,全被擦了,Flash上env镜像的CRC自然就挂了。
不重启时在Linux里跑 fw_printenv,打印的是fw工具内置的旧版默认环境(旧 mem=224M);一重启,U-Boot上电打出 *** Warning - bad CRC, using default environment,加载 u-boot.bin 里新版 default_environment。但此时Flash env分区还是全0xFF,只有进U-Boot执行一次 saveenv,把默认环境真正写进Flash,之后再跑 fw_printenv 才是对的。
对比一下:nandwrite 只写镜像文件的实际长度,不擦分区剩余块,也就不会碰后面嵌套的env分区。
现象2:fw_printenv和U-Boot上电的默认值对不上
根因不复杂: 文件系统里 fw_printenv 版本旧了,编译它的时候Board宏没跟着更新,工具内置的 default_environment 还是项目早期配置。解决也直接:编译 tools/env 时用和U-Boot完全一致的Board配置宏,重新出 fw_printenv,保证两份默认环境内容一致。
现象3:改了CONFIG_BOOTARGS,擦掉env分区,bootargs没变
先确认 CONFIG_BOOTARGS 确实被定义(检查条件编译)。然后记住它只是故障兜底:Flash env CRC正常时完全不生效。另外要分清两个 bootargs:U-Boot环境变量里的 bootargs,和dtb chosen节点里的 bootargs。U-Boot环境变量里存在 bootargs 时,会覆盖dtb chosen节点;没有的话,内核直接使用dtb的 bootargs。
四 关键调试排查手段
几个常用手段:
判断Flash env是否损坏:U-Boot串口打 *** Warning - bad CRC, using default environment;Linux下 fw_printenv 打 Warning: The environment is empty ... loading default environment Success!。
区分两套 default_environment:Linux下 strings /usr/bin/fw_printenv | grep xxx 看fw工具内置的;U-Boot串口 printenv bootargs、md ${env_addr} dump内存看U-Boot内置的。这里涉及的源码解析思路,在 技术文档 板块里有大量同类案例可以参考。
直接看Flash原始二进制,确认env镜像状态:
dd if=/dev/mtd0 of=env_raw.bin skip=$((0xc0000/4096)) bs=4096 count=32
hexdump env_raw.bin
全0xFF说明env分区被擦过。
烧uboot又不想动嵌套的env分区,就只擦uboot程序区域,别碰后面:
flash_erase /dev/mtd0 0 0xc0000
nandwrite /dev/mtd0 u-boot.bin
含嵌套env的MTD分区不建议用 flashcp。
五 踩坑心得
踩完坑之后,最值得做的就这几条:
- env分区尽量独立出来,别嵌套在uboot的MTD分区里。 把
CONFIG_ENV_OFFSET 挪到uboot分区后面,MTDPARTS里加独立env分区,从根上避免烧uboot连坐擦掉环境变量。fw_env.h 里的偏移要同步改。
mtdparts=nand:1M(uboot),128K(env),3M(kernel),20M(root),-(appfs)
-
fw_printenv / fw_setenv 必须跟着U-Boot同步编译发布。 tools/env 编译时用和u-boot完全相同的board配置,保证fw工具和 u-boot.bin 内置的两份默认环境一致。不然Flash env一坏,两边输出对不上,纯纯增加调试成本。这类构建链路上的同步问题,在 技术文档 的避坑指南分类里也经常被提起。
-
不要把 fw_printenv 输出的默认值当U-Boot运行时的基准。 故障状态下以U-Boot串口控制台 printenv 为准。
-
量产烧写时,Flash全擦除后让U-Boot执行一次 saveenv,把默认环境固化进env分区,避免每次上电都走 bad-CRC 恢复分支。
六 文末小结
把这几个关键点再钉一下:
正常运行时,U-Boot用Flash上的持久化env;只有Flash损坏(读失败/CRC错/全0xFF)才回退到 u-boot.bin 内置的 default_environment,CONFIG_BOOTARGS 只是兜底值。
fw_printenv/fw_setenv 和 U-Boot 读写的是同一份Flash env分区,但fw工具另有一份独立编译内置的 default_environment。Flash env坏了它输出的是工具自带的默认,不等于U-Boot上电跑的那个默认。
我们SPI-NAND这个平台,env嵌套在uboot的MTD分区里,flashcp 一擦就把env连坐擦掉,这个布局新项目开发时最好改成独立env分区。
改了 bootargs 这类默认配置,u-boot.bin 和 fw_printenv 要同步编译更新,否则两份默认环境不一致,调试时很容易误判。这类底层机制与用户态工具的配合问题,在 云栈社区 的嵌入式讨论中也不少见,有兴趣可以进去翻翻同行的踩坑记录。
两条故障信号,背下来直接对上号:
U-Boot串口:*** Warning - bad CRC, using default environment
->Bootloader用u-boot.bin内置默认
fw_printenv:Warning: The environment is empty ... loading default environment Success!
->Linux工具用fw_printenv二进制内置默认
就先记这么多,后面再踩到新坑再补。