U-Boot环境变量与分区表:env存储位置、默认分区表解析、mtdparts命令
好,咱们进入第二章。这一章要聊的东西,是你在做任何嵌入式系统移植时都绕不开的——环境变量和分区表。说白了,U-Boot 启动后怎么知道从哪里加载内核?怎么知道你的 Flash 上哪块是文件系统、哪块是 bootloader?全靠这两样东西撑着。
我个人习惯,拿到一块新板子,第一件事就是先搞清楚它的分区布局。不然你后面改固件升级逻辑,分分钟把整个系统搞崩。
2.1 环境变量存储位置
环境变量,就是 U-Boot 用来存配置参数的地方。比如 bootargs、bootcmd、serverip 这些。你每次在 U-Boot 命令行里用 setenv 设的值,最后都要找个地方存下来,不然掉电就丢了。
那存哪儿呢?常见的有这么几种:
| 存储介质 | 特点 | 适用场景 |
|---|---|---|
| NOR Flash | 直接寻址,读写慢,可靠性高 | 小容量、启动快的系统 |
| NAND Flash | 容量大,有坏块问题 | 大容量存储设备 |
| MMC/SD | 块设备,分区管理方便 | 树莓派、手机等主流方案 |
| SPI Flash | 小容量,速度快 | 路由器、IoT 设备 |
| EMMC | 内置控制器,寿命长 | 工业级、车规级产品 |
嗯,这里要注意:环境变量不是随便找个地方就塞的。它需要一块专门的区域,而且这块区域不能和别的数据重叠。我在项目中遇到过,有人把 env 分区和 kernel 分区设成了同一个偏移,结果一改环境变量,内核就启动不了了。排查了三天才发现是分区重叠。
关键配置宏
在 U-Boot 源码中,环境变量的存储位置由以下宏控制:
// 环境变量在 Flash 中的偏移
#define CONFIG_ENV_OFFSET 0x100000
// 环境变量占用大小
#define CONFIG_ENV_SIZE 0x20000
// 存储介质类型
#define CONFIG_ENV_IS_IN_NAND
// 或者
#define CONFIG_ENV_IS_IN_MMC
你想想看,如果你的 Flash 总大小是 16MB,分区表里把 env 放在 1MB 偏移处,占 128KB。那这个偏移和大小就必须和 CONFIG_ENV_OFFSET、CONFIG_ENV_SIZE 完全一致。不一致会怎样?U-Boot 启动时会去错误的位置读环境变量,读出来全是乱码,然后 fallback 到默认值。
我的小技巧
调试阶段,我建议把环境变量冗余备份打开。就是设两个相同的 env 区域,一个坏了还能从另一个恢复。宏是 CONFIG_ENV_OFFSET_REDUND。这招救过我一次,那次 Flash 某个块刚好坏了,正好落在 env 区域,还好有冗余备份。
2.2 默认分区表解析
分区表,就是告诉 U-Boot:你的 Flash 上每一块区域叫什么名字、从哪开始、有多大。U-Boot 默认的分区表定义在 include/configs/ 目录下,每个板子一个头文件。
举个例子,一个典型的 NAND Flash 分区表长这样:
#define CONFIG_MTD_PARTS
#define CONFIG_MTD_PARTITIONS
static const struct mtd_partition my_partitions[] = {
{
.name = "bootloader",
.offset = 0,
.size = 0x100000, // 1MB
},
{
.name = "env",
.offset = 0x100000,
.size = 0x20000, // 128KB
},
{
.name = "kernel",
.offset = 0x120000,
.size = 0x400000, // 4MB
},
{
.name = "rootfs",
.offset = 0x520000,
.size = 0xA00000, // 10MB
},
{
.name = "data",
.offset = 0xF20000,
.size = MTDPART_SIZ_FULL, // 剩余全部
},
};
这里每个分区都有名字、偏移和大小。名字很重要,因为后面你用 mtdparts 命令操作时,就是通过名字来引用分区的。
我曾经接手过一个项目,前任工程师把分区表写在代码里,但没和实际 Flash 布局对齐。结果 kernel 分区实际只有 2MB,但代码里写了 4MB。U-Boot 启动时往 kernel 分区写数据,写到 2MB 以后就写到 rootfs 区域去了。系统跑起来后 rootfs 莫名其妙损坏,查了两个月才发现是分区表定义错了。
避坑指南
分区表的偏移和大小,必须和硬件设计文档完全一致。不要相信代码里的注释,要亲自拿示波器或者读 Flash 的 ID 确认实际容量。我曾经因为 Flash 型号手册写的是 128MB,实际焊上去的是 64MB,分区表按 128MB 写的,结果最后几个分区根本不存在,一读写就挂。
2.3 mtdparts 命令详解
mtdparts 是 U-Boot 里操作分区表的利器。它可以在运行时查看、修改、保存分区信息。说白了,就是不用重新编译 U-Boot,也能调整分区布局。
先看基本用法:
# 查看当前分区表
U-Boot> mtdparts
# 设置新的分区表
U-Boot> mtdparts default
# 保存分区表到环境变量
U-Boot> saveenv
输出结果大概长这样:
device nand0 <nand_flash>, # parts = 5
#: name size offset mask_flags
0: bootloader 0x00100000 0x00000000 0
1: env 0x00020000 0x00100000 0
2: kernel 0x00400000 0x00120000 0
3: rootfs 0x00A00000 0x00520000 0
4: data 0x030E0000 0x00F20000 0
嗯,这里要注意:mtdparts 命令依赖 CONFIG_MTD_PARTS 和 CONFIG_CMD_MTDPARTS 这两个宏。如果你的 U-Boot 没开这两个宏,那 mtdparts 命令根本不存在。
mtdparts 命令格式
手动设置分区表的完整格式:
mtdparts add <device> <size>[@<offset>] <name> [<mask_flags>]
# 示例:在 nand0 设备上添加一个 2MB 的 recovery 分区
mtdparts add nand0 0x200000@0x1F20000 recovery
我个人习惯,在开发阶段从来不用硬编码的分区表。我会在 include/configs/ 里把 CONFIG_MTD_PARTS 打开,然后在 U-Boot 启动脚本里用 mtdparts default 加载默认分区。这样如果后期要调整分区大小,只需要改环境变量,不用重新编译 U-Boot。
但注意,生产环境我建议还是把分区表编译进 U-Boot。因为如果环境变量被误删了,U-Boot 还能从默认配置里读到正确的分区表。不然你想想,env 丢了,分区表也没了,系统就彻底变砖了。
调试技巧
如果你不确定当前分区表是否正确,可以用 mtdparts 配合 mtd_debug 来验证:
# 先查看分区表
U-Boot> mtdparts
# 然后读某个分区的数据看看
U-Boot> mtd_debug read /dev/mtd2 0 0x100 0x100000
# 如果读出来的数据和你预期的不一样,说明分区偏移或大小有问题
2.4 环境变量与分区表的联动关系
环境变量和分区表不是孤立的。它们之间有几个关键的联动点:
- bootargs 中的 mtdparts 参数:Linux 内核启动时,会通过 bootargs 环境变量里的
mtdparts=参数来获取分区信息。如果 U-Boot 里的分区表和 bootargs 里的不一致,内核看到的分区布局就和 U-Boot 不一样,读写就会错位。 - env 分区本身就在分区表里:环境变量存储的区域,必须在分区表中有对应的条目。不然 U-Boot 不知道这块区域是干什么的。
- 固件升级时依赖分区表:升级脚本里写的是
flash_erase /dev/mtd2 0 0,这个mtd2对应的就是分区表里的第三个分区。如果分区表变了,mtd 设备编号也会变。
我画了一张图,帮你理清它们之间的关系:
从这张图你可以看到,整个链路是这样的:U-Boot 启动 → 读取环境变量 → 环境变量里的 bootargs 包含 mtdparts 参数 → 内核解析 mtdparts → 生成 mtd 设备列表 → 固件升级脚本操作 mtd 设备。任何一个环节对不上,系统就出问题。
重要提醒
修改分区表后,一定要同步更新以下三个地方:
- U-Boot 源码中的分区表定义(
include/configs/) - 环境变量中的
mtdparts参数(通过setenv bootargs修改) - 固件升级脚本中的 mtd 设备编号(比如
/dev/mtd2可能变成/dev/mtd3)
少改一个,系统就可能在某个环节崩溃。我见过太多人只改了 U-Boot 源码,忘了改 bootargs,结果内核启动时分区全乱套了。
2.5 实战:查看和修改分区表
好,咱们来点实际的。假设你拿到一块板子,想看看它的分区表是什么样的:
# 进入 U-Boot 命令行
U-Boot> mtdparts
# 如果提示 "mtdparts not found",说明没开这个命令
# 需要重新编译 U-Boot,加上 CONFIG_CMD_MTDPARTS=y
# 查看更详细的信息
U-Boot> mtdparts detail
# 查看某个分区的具体信息
U-Boot> mtdparts info nand0
如果你想临时修改分区表,比如把 kernel 分区从 4MB 改成 6MB:
# 先删除旧分区表
U-Boot> mtdparts del nand0 kernel
# 再添加新分区
U-Boot> mtdparts add nand0 0x600000@0x120000 kernel
# 查看修改后的结果
U-Boot> mtdparts
# 保存到环境变量
U-Boot> saveenv
嗯,这里要注意:mtdparts del 删除分区时,后面的分区偏移不会自动调整。你删了 kernel 分区,rootfs 的偏移还是 0x520000,不会自动往前移。所以手动调整分区表时,一定要自己算好每个分区的偏移。
我的经验
我建议在开发板上先试好分区表,确认没问题了,再写到 U-Boot 源码里。怎么试?用 mtdparts 命令设好分区后,用 mtd_debug read 读一下每个分区的数据,看看是不是你期望的内容。比如读 kernel 分区,应该能看到 uImage 的头部。如果读到的是全 0xFF 或者乱码,说明分区偏移算错了。
这一章的内容就到这里。环境变量和分区表是 U-Boot 移植的基础,也是固件升级的基石。搞懂了它们,后面讲固件升级流程时,你就能理解为什么升级脚本要那么写了。