4、U-Boot环境变量与分区:存储、mtdparts命令与动态/静态分区
U-Boot的环境变量,说白了就是Bootloader的“记忆”。板子一断电,所有寄存器、内存里的东西全没了。但环境变量能记住你上次设的IP地址、启动参数、分区布局。为什么?因为它被存到了Flash里。
我刚开始做嵌入式时,总觉得环境变量就是个配置文件,没啥好研究的。直到有一次产线反馈,同一批板子有的能启动有的不行。查了两天,最后发现是环境变量在Flash里被写坏了,导致分区表读出来是乱的。嗯,从那以后,我对环境变量的存储机制再也不敢马虎了。
4.1 环境变量存储:从内存到Flash的旅程
U-Boot的环境变量,运行时全在内存里。你敲setenv改的只是RAM中的副本。要让它掉电不丢,得用saveenv写回存储设备。
存储位置由配置决定,常见的有:
- NOR Flash:直接映射到CPU地址空间,读写快,但擦除慢。环境变量通常放在末尾扇区。
- NAND Flash:需要ECC校验,环境变量一般放在独立的块里,避免被坏块影响。
- MMC/SD卡:环境变量可以放在FAT分区的一个文件里(比如
uboot.env),或者放在RAW分区偏移处。 - SPI Flash:类似NOR,但容量小,环境变量通常紧挨着U-Boot镜像。
关键点:环境变量在Flash中采用“双备份”机制。U-Boot会写两份,启动时校验CRC。如果主备份坏了,自动用备份恢复。我曾经遇到过Flash老化导致某位翻转,就是靠这个机制救回来的。
环境变量在Flash里的布局,大致是这样:
+------------------+ <-- 起始地址
| CRC32校验值 (4B) |
+------------------+
| 数据长度 (4B) |
+------------------+
| 标志位 (2B) | -- 0xFFFF表示有效,0x0000表示已删除
+------------------+
| 环境变量数据 |
| key=value\0 |
| key2=value2\0 |
| ... |
| \0 | -- 结束符
+------------------+
为什么要有标志位?因为Flash擦除后是全1(0xFF),写操作只能把1变0。U-Boot利用这个特性做“增量更新”——只修改变化的变量,而不是整块重写。这个设计在NOR Flash上特别有用,能减少擦除次数,延长寿命。
4.2 mtdparts命令详解:分区表的瑞士军刀
mtdparts,全称是MTD分区表命令。它是U-Boot里操作Flash分区的核心工具。你想想看,没有它,你怎么知道Flash的哪个区域是kernel、哪个是rootfs?
命令格式如下:
mtdparts [add|del|set] <分区名> <大小>[@<偏移>] [<标志>]
常用操作:
mtdparts:显示当前分区表mtdparts default:恢复默认分区表(从编译时的配置读取)mtdparts del <分区名>:删除指定分区mtdparts add <名称> <大小>[@<偏移>]:添加分区
举个例子,我在一个项目里给SPI Flash分了区:
U-Boot> mtdparts
device nand0 <NAND 256MiB>, # parts = 4
#: name size offset mask_flags
0: spl 0x00040000 0x00000000 0
1: uboot 0x001c0000 0x00040000 0
2: env 0x00040000 0x00200000 0
3: kernel 0x00400000 0x00240000 0
4: rootfs 0x0e000000 0x00640000 0
active partition: nand0,0 - (spl) 0x00040000 @ 0x00000000
注意看偏移和大小。分区必须连续,不能重叠。U-Boot启动时会根据这个表初始化MTD设备,Linux内核也会通过设备树或内核参数读到同样的布局。
个人习惯:我一般把环境变量分区放在U-Boot后面,紧挨着。这样如果U-Boot升级了,环境变量区的位置不变,不会导致旧的环境变量读不出来。另外,环境变量分区大小建议至少256KB,别省这点空间,否则变量多了写不进去很麻烦。
4.3 动态分区 vs 静态分区:两种哲学
分区策略,说白了就是“谁来决定Flash怎么切”。
静态分区:编译时定死。分区表写在U-Boot源码里,或者通过CONFIG_MTDPARTS_DEFAULT宏定义。板子跑起来后,分区大小和位置就不能改了。
动态分区:运行时决定。分区表存在环境变量mtdparts里,或者通过设备树传递。你可以在U-Boot命令行里随时调整,甚至可以在Linux内核里用mtdparts工具重新划分。
我画了张图,帮你理解两者的区别:
两种方式的对比,我整理了一个表格:
| 对比项 | 静态分区 | 动态分区 |
|---|---|---|
| 定义方式 | 编译时宏/源码 | 环境变量/设备树 |
| 修改时机 | 需重新编译烧写 | 运行时命令行修改 |
| 可靠性 | 高(不受环境变量影响) | 中(依赖环境变量完整性) |
| 灵活性 | 低 | 高 |
| 适用场景 | 工业、车规、医疗 | 消费电子、IoT、A/B升级 |
避坑指南:我曾经在一个项目里用了动态分区,但环境变量存的是NAND Flash的坏块信息。结果有一次NAND出现新坏块,环境变量区被写坏了,分区表全丢。从那以后,我坚持在动态分区方案里加一个“硬编码后备分区表”——如果环境变量里的mtdparts读不出来,就fallback到编译时默认值。这个习惯救了我好几次。
4.4 实战:在U-Boot中配置分区
假设你有一块256MB的NAND Flash,想分成4个区:
- U-Boot:2MB,从0开始
- 环境变量:256KB,紧接U-Boot
- Kernel:8MB
- Rootfs:剩余空间
在U-Boot命令行里操作:
# 先设置环境变量
setenv mtdids nand0=nand0
setenv mtdparts mtdparts=nand0:2m(uboot),256k(env),8m(kernel),-(rootfs)
saveenv
# 查看分区表
mtdparts
# 测试读写
nand read 0x82000000 kernel
nand write 0x82000000 kernel 0x800000
注意-(rootfs)的写法,减号表示“剩余所有空间”。这个语法很实用,你不需要手动计算rootfs的大小。
如果要在Linux内核里看到同样的分区,需要在U-Boot启动时把mtdparts传给内核:
setenv bootargs console=ttyS0,115200 root=/dev/mtdblock3 mtdparts=${mtdparts}
bootm 0x82000000
或者通过设备树:
// 在设备树中定义分区
&nand0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "uboot";
reg = <0x0000000 0x200000>;
};
partition@200000 {
label = "env";
reg = <0x200000 0x40000>;
};
// ...
};
};
个人经验:设备树方式更推荐,因为它和环境变量解耦了。即使环境变量丢了,内核还能从设备树拿到正确的分区表。我现在的项目都是“设备树静态分区 + 环境变量动态覆盖”的混合模式——既有可靠性,又有灵活性。
最后说一句,分区大小要对齐Flash的擦除块大小。NAND一般是128KB或256KB,NOR是64KB或128KB。不对齐的话,擦除操作可能会擦到相邻分区,后果你懂的。