3. 分区表数据结构:struct part_info、struct mtd_device、链表管理机制
分区表这东西,说白了就是一张地图。U-Boot 启动时得知道 Flash 上哪块是 bootloader、哪块是内核、哪块是文件系统。没有这张地图,固件升级就是盲人摸象。
我刚开始接触 U-Boot 分区表时,觉得不就是几个结构体嘛,有啥好研究的?后来踩了坑才明白——数据结构设计决定了你能做什么、不能做什么。今天咱们就把 struct part_info、struct mtd_device 和链表管理机制掰开揉碎了讲。
3.1 struct part_info:分区的身份证
每个分区在 U-Boot 里都对应一个 struct part_info。它记录了分区的名字、起始偏移、大小、标志位等信息。来看定义:
struct part_info {
char name[NAME_LEN]; /* 分区名,比如 "kernel"、"rootfs" */
ulong size; /* 分区大小,单位字节 */
ulong offset; /* 分区在设备上的起始偏移 */
int mask_flags; /* 标志位,控制读写权限 */
int auto_loaded; /* 是否自动加载 */
struct mtd_device *dev; /* 指向所属的 mtd_device */
int num_sectors; /* 扇区数 */
ulong sector_size; /* 扇区大小 */
int idx; /* 分区索引号 */
};
这里我重点说几个字段:
- name:分区名长度有限制,
NAME_LEN通常是 32 或 64。我见过有人把分区名写成 "u-boot-env-backup-2024",结果截断了,查了半天才找到原因。 - offset 和 size:这两个字段决定了分区的物理范围。升级固件时,写错 offset 会直接覆盖别的分区,板子就变砖了。
- mask_flags:可以设置
PART_FLAG_WRITEABLE或PART_FLAG_READ_ONLY。我习惯把 bootloader 分区设为只读,防止误操作。
3.2 struct mtd_device:分区的容器
一个 MTD 设备(比如 SPI Flash、NAND Flash)对应一个 struct mtd_device。它管理着该设备上的所有分区:
struct mtd_device {
struct mtd_info *mtd_info; /* MTD 设备信息 */
int num_parts; /* 分区数量 */
struct part_info *parts; /* 分区数组指针 */
struct list_head link; /* 链表节点,用于挂接到全局链表 */
};
这里有个关键点:parts 是一个数组,不是链表。每个分区通过 part_info.dev 反向指向所属的 mtd_device。这种双向关联的设计,让我在调试时能快速定位问题。
举个例子,我在项目中遇到过这样的情况:系统启动后找不到 rootfs 分区。排查步骤是:
- 先遍历全局链表找到所有
mtd_device - 对每个设备,遍历它的
parts数组 - 检查分区名和 offset 是否匹配
最后发现是分区表偏移量写错了,rootfs 的 offset 和 kernel 重叠了。
3.3 链表管理机制:把分区串起来
U-Boot 用双向链表管理所有 MTD 设备。链表头定义在 common/env_flash.c 或 drivers/mtd/mtdcore.c 中:
static LIST_HEAD(mtd_devices_list);
每个 mtd_device 通过 link 字段挂接到这个链表。操作接口很简单:
list_add_tail():添加设备到链表尾部list_del():从链表移除设备list_for_each():遍历所有设备
为什么要用链表而不是数组?因为 MTD 设备是动态注册的。比如你插了个 USB 闪存盘,它会在运行时创建新的 mtd_device。链表可以灵活增删,数组就做不到。
💡 核心思路:分区表的数据结构是三层嵌套——全局链表管理设备,设备管理分区数组,分区记录自己的属性。理解了这个层次,你就能看懂 U-Boot 的分区操作代码。
3.4 分区表初始化流程
U-Boot 启动时,分区表是怎么建立起来的?我画了张图:
流程其实不复杂:
- U-Boot 启动后,MTD 子系统初始化
- 检测到 Flash 设备,调用
add_mtd_device()注册 - 解析分区表(从环境变量或代码中的静态表)
- 为每个分区创建
struct part_info,填充 name、offset、size - 把
mtd_device挂入全局链表
这里有个细节:分区表可以来自 mtdparts 环境变量,也可以硬编码在代码里。我建议用环境变量方式,这样不用重新编译就能调整分区。
🔧 调试技巧:在 U-Boot 命令行输入 mtdparts 可以查看当前分区表。如果显示不对,先检查 mtdparts 环境变量是否设置正确。
3.5 分区表与固件升级的联动
固件升级时,分区表数据结构是怎么参与进来的?我举个例子:
/* 伪代码:升级内核分区 */
int update_kernel(struct mtd_device *dev, void *data, size_t len) {
struct part_info *part;
int i;
/* 遍历分区,找到 kernel */
for (i = 0; i < dev->num_parts; i++) {
part = &dev->parts[i];
if (strcmp(part->name, "kernel") == 0) {
/* 检查大小是否匹配 */
if (len > part->size) {
printf("Error: data too large for partition\n");
return -1;
}
/* 擦除并写入 */
mtd_erase(dev->mtd_info, part->offset, part->size);
mtd_write(dev->mtd_info, part->offset, data, len);
return 0;
}
}
printf("Error: kernel partition not found\n");
return -1;
}
你看,升级函数依赖 part_info 的 offset 和 size。如果分区表数据错了,升级就会写到错误的位置。我曾经遇到过一个问题:升级后系统启动不了,排查发现是分区表里 kernel 的 offset 比实际小了 0x10000,结果写覆盖了前面的 bootloader。
3.6 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 分区表显示为空 | mtdparts 环境变量未设置 | 检查 env 中 mtdparts 是否存在 |
| 分区大小不对 | 分区表定义与实际 Flash 不匹配 | 用 flinfo 查看 Flash 实际大小 |
| 升级后系统无法启动 | 分区 offset 写错,覆盖了关键数据 | 对比分区表与升级脚本中的偏移量 |
| 链表遍历崩溃 | mtd_device 未正确初始化 | 检查 add_mtd_device 返回值 |
嗯,这里要注意:链表操作在 U-Boot 里没有锁保护,所以不要在中断上下文修改分区表。我见过有人想在按键中断里动态添加分区,结果链表指针乱了,直接死机。
总结一下今天的内容:struct part_info 是分区的身份证,struct mtd_device 是分区的容器,双向链表把多个设备串起来。这三者构成了 U-Boot 分区表的核心骨架。理解了它们,你就能看懂分区表的创建、查找、修改代码,也能在固件升级时准确定位问题。