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工具重新划分。

我画了张图,帮你理解两者的区别:

静态分区 vs 动态分区 静态分区(编译时固定) 源码中定义 mtdparts 编译生成默认分区表 运行时不可修改 动态分区(运行时可变) 环境变量 mtdparts= U-Boot命令行动态调整 支持 OTA 分区大小调整 核心差异 • 静态分区:简单可靠,适合量产固件固定的产品。但升级时如果分区大小变了,就得重新烧写整片Flash。 • 动态分区:灵活,适合需要OTA升级、A/B分区切换的场景。但依赖环境变量完整性,环境变量坏了分区表就丢了。 • 我的建议:消费类产品用动态分区,工业控制用静态分区。别问为什么,问就是吃过亏。

两种方式的对比,我整理了一个表格:

对比项 静态分区 动态分区
定义方式 编译时宏/源码 环境变量/设备树
修改时机 需重新编译烧写 运行时命令行修改
可靠性 高(不受环境变量影响) 中(依赖环境变量完整性)
灵活性 低 高
适用场景 工业、车规、医疗 消费电子、IoT、A/B升级

避坑指南:我曾经在一个项目里用了动态分区,但环境变量存的是NAND Flash的坏块信息。结果有一次NAND出现新坏块,环境变量区被写坏了,分区表全丢。从那以后,我坚持在动态分区方案里加一个“硬编码后备分区表”——如果环境变量里的mtdparts读不出来,就fallback到编译时默认值。这个习惯救了我好几次。

4.4 实战:在U-Boot中配置分区

假设你有一块256MB的NAND Flash,想分成4个区:

  1. U-Boot:2MB,从0开始
  2. 环境变量:256KB,紧接U-Boot
  3. Kernel:8MB
  4. 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。不对齐的话,擦除操作可能会擦到相邻分区,后果你懂的。