你的观察非常敏锐!U-Boot 和 Linux 内核确实各自有一个设备树,而且在文件形式上,它们看起来确实是两个独立的文件(比如 u-boot.dtbkernel.dtb)。

但它们的关系并不是“毫无关联”,而是 “同宗同源,动态传递” 的关系。

以下是详细的解析:

1. 它们是“双胞胎”:源自同一个文件

在绝大多数 ARM 平台上,U-Boot 和内核的设备树源自同一个源文件(.dts.dtsi

  • 开发阶段:工程师编写一套设备树源码,描述这块开发板上有多少 CPU、内存多大、有哪些控制器(串口、网口、MMC 等)。
  • 编译阶段:同一个 .dts 文件被编译器(DTC)编译成二进制的 .dtb 文件。
  • 结果:U-Boot 和 Kernel 使用的设备树,在硬件描述的主体部分是一模一样的。

2. 为什么看起来是两个独立文件?

虽然源码相同,但在构建和使用过程中,它们确实存在差异:

A. U-Boot 的设备树
  • 目的:U-Boot 运行时需要知道自己有哪些驱动可用。比如,U-Boot 需要知道串口的地址以便打印启动日志,需要知道 MMC 控制器的地址以便读取内核。
  • 形式:通常,U-Boot 会将一份精简或完整的 DTB 嵌入到 U-Boot 的二进制文件内部(尾部)。
  • 独立性:U-Boot 启动初期,会先使用自己肚子里的这份 DTB 来初始化自己。
B. Linux 内核的设备树
  • 目的:内核运行时需要完整的硬件视图来加载驱动。
  • 形式:通常作为单独的文件(如 image.dtb)存放在启动分区,或者被打包在 Image.gz 中。

3. 它们是如何关联起来的?(关键流程)

这是最关键的部分:U-Boot 并不是直接把“内核文件”扔给 CPU 就不管了,它还充当了“设备树代理人”的角色。

启动流程通常是这样的:

  1. 加载:U-Boot 从存储设备(如 Flash、SD 卡)中读取内核镜像和内核的 DTB 文件到 DDR 内存中。
  2. 修正(关键步骤):U-Boot 会根据自己的环境变量,动态修改内存中的那份内核 DTB
    • 例子 1:U-Boot 会把 bootargs(启动参数,如 console=ttyS0)写入 DTB 的 chosen 节点,告诉内核控制台在哪。
    • 例子 2:如果 U-Boot 发现内存大小是动态检测的,它会更新 DTB 中的 memory 节点。
  3. 传递:U-Boot 跳转到内核时,会把内核 DTB 在内存中的地址通过寄存器(通常是 x0r2)告诉内核。
  4. 接管:Linux 内核启动,解析 U-Boot 传过来的这份 DTB,并根据它来加载驱动。

4. 总结:区别与联系

特性 U-Boot 的 DTB Linux Kernel 的 DTB
数据来源 同一个 .dts 源文件 同一个 .dts 源文件
主要用途 驱动 U-Boot 自身的外设(如初始化 DDR、读取 Flash) 驱动 Linux 内核的所有设备驱动
存在形式 通常嵌入在 U-Boot 二进制文件内部 通常作为独立文件加载到内存
是否可修改 U-Boot 运行时一般只读(用于初始化) U-Boot 会动态修改它(填入 bootargs 等),然后传给内核

5. 为什么不共用一个文件变量?

你可能会问:“既然 U-Boot 肚子里已经有一个 DTB 了,为什么还要再加载一个内核的 DTB?直接用 U-Boot 的传给内核不行吗?”

答案是:理论上可以,但实际中通常分开。

  1. 解耦:U-Boot 的 DTB 可能为了节省空间做过裁剪,只包含 U-Boot 关心的节点,而内核需要完整的硬件描述。
  2. 版本匹配:有时候 U-Boot 版本很老,但内核更新了,新增了硬件特性。如果强制共用 U-Boot 内部的 DTB,内核可能缺信息。加载外部的独立 DTB 文件可以让内核和 DTB 保持配套更新。

一句话总结:
它们确实是两个文件,但描述的是同一块板子。U-Boot 是“编辑者”,它加载内核的 DTB,在上面填好参数,然后作为“介绍信”递给内核。 内核拿到这份介绍信,才知道自己是谁,该干什么。
这是一个非常好的切入点!通过“内存大小动态检测”这个例子,可以非常直观地展示 U-Boot 是如何作为“中间人”去修改设备树的。

我们重新梳理一下这个关系,并把这个例子讲透。

1. 核心关系回顾:同源,但动态传递

首先,你说的没错,它们在物理文件上确实是两个独立的文件:

  • 文件 1 (U-Boot DTB):通常嵌入在 U-Boot 二进制内部,U-Boot 启动时用它来初始化自己(比如初始化 DDR 控制器)。
  • 文件 2 (Kernel DTB):通常放在启动分区(如 boot.dtb),专门准备给 Linux 内核使用。

关联的关键在于: U-Boot 不仅仅是把 Kernel DTB 加载到内存,它还会像编辑 Word 文档一样,修改 Kernel DTB 的内容,然后再把地址告诉内核。


2. 具体例子:内存大小的动态检测

场景背景

假设你设计了一款开发板,原理图上支持 1GB2GB 两种 DDR 内存颗粒(为了节省成本,同一个 PCB 可能贴不同的内存芯片)。

在设备树源文件(.dts)中,通常只能写死一个默认值,比如:

/* kernel.dts 源码片段 */
memory@80000000 {
    device_type = "memory";
    reg = <0x80000000 0x40000000>; /* 默认写死为 1GB (0x40000000) */
};

如果用户贴的是 2GB 内存,但设备树里写的是 1GB,Linux 启动后就会浪费掉另外 1GB 内存。这时候 U-Boot 的动态修改能力 就派上用场了。

详细流程演示

第一步:U-Boot 自身初始化(硬件探测)
U-Boot 启动时,会运行 DDR 初始化驱动。这个驱动非常智能,它会读取 DDR 颗粒的 ID 或 SPD 信息,探测出物理内存的实际大小。

  • 假设 U-Boot 探测到实际焊接的是 2GB 内存。
  • U-Boot 将这个大小记录在自己的全局变量 gd->ram_size 中。

第二步:加载内核设备树
U-Boot 从 Flash/SD 卡中将 kernel.dtb 文件加载到 DDR 的某个空闲地址处。此时,这个 DTB 里的 memory 节点还是 1GB(文件里写死的)。

第三步:U-Boot 动态修改 DTB(关键!)
在启动内核前的最后一步,U-Boot 会执行一个叫做 ft_board_setup (或类似名称) 的回调函数。这个函数会做以下操作:

  1. 定位节点:在内存中的 DTB 里找到 /memory 节点。
  2. 计算新值:读取刚才探测到的 gd->ram_size (2GB)。
  3. 修改属性:使用 U-Boot 提供的设备树操作库(fdt_setprop 系列函数),强行修改 reg 属性的值。

伪代码逻辑如下:

/* U-Boot 代码逻辑示意 */
void fixup_memory_dt(void *fdt_blob) {
    /* 1. 获取实际探测到的内存大小 (假设是 2GB) */
    u64 actual_ram_size = gd->ram_size; // 0x80000000 (2GB)

    /* 2. 在 DTB 中找到 memory 节点 */
    int nodeoffset = fdt_path_offset(fdt_blob, "/memory");

    /* 3. 准备新的数据 (起始地址 + 新的大小) */
    u32 mem_regs[2] = {0x80000000, actual_ram_size};

    /* 4. 强制修改 DTB 中的 reg 属性 */
    fdt_setprop(fdt_blob, nodeoffset, "reg", mem_regs, sizeof(mem_regs));
}

第四步:传递给内核
此时,内存中的 DTB 已经被 U-Boot 篡改了,memory 节点变成了 2GB。U-Boot 跳转到内核入口点,并把这个 DTB 的地址传给内核。

第五步:内核启动
Linux 内核解析设备树,读到 memory 节点是 2GB,于是正确地管理了所有内存。

3. 总结

通过这个例子,你可以看到 U-Boot 和 Kernel 设备树的关系:

  1. 独立性:Kernel DTB 是静态文件,描述的是“默认情况”。
  2. 关联性:U-Boot 是“运行时环境”,它知道真实的硬件状态。
  3. 动态性:U-Boot 充当了 补丁工具,在启动瞬间,将真实的硬件信息(如内存大小、启动参数 bootargs、MAC 地址等)“擦写”进 Kernel DTB,实现了“一份镜像,适配多种硬件配置”的灵活性。

如果没有 U-Boot 这一步,你就必须为每一种内存大小的板子单独编译一个内核设备树,这在量产和维护中简直是灾难。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐