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 分区设为只读,防止误操作。
⚠️ 注意:分区名不要用特殊字符,U-Boot 的字符串解析有时会出问题。我建议只用字母、数字和下划线。

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 分区。排查步骤是:

  1. 先遍历全局链表找到所有 mtd_device
  2. 对每个设备,遍历它的 parts 数组
  3. 检查分区名和 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 分区表初始化流程 分区表初始化流程 U-Boot 启动 MTD 设备注册 解析分区表 创建 part_info 数组 挂入 mtd_devices_list 关键函数 add_mtd_device() add_mtd_partitions() part_create() list_add_tail()

流程其实不复杂:

  1. U-Boot 启动后,MTD 子系统初始化
  2. 检测到 Flash 设备,调用 add_mtd_device() 注册
  3. 解析分区表(从环境变量或代码中的静态表)
  4. 为每个分区创建 struct part_info,填充 name、offset、size
  5. 把 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 分区表的核心骨架。理解了它们,你就能看懂分区表的创建、查找、修改代码,也能在固件升级时准确定位问题。