3、设备树文件结构:dts、dtsi、dtb、dtbo文件的区别与联系

做嵌入式Linux开发,尤其是跟内核和驱动打交道,你一定会碰到一堆后缀名奇怪的文件:.dts、.dtsi、.dtb、.dtbo。我第一次接触时也懵了——这玩意儿怎么跟C语言的头文件和源文件似的?

其实说白了,它们就是设备树的几种“形态”。就像你写代码,有源码、有头文件、有编译后的二进制文件。设备树也一样,只不过它描述的是硬件资源。

我个人习惯把这几类文件分成两组:源文件和编译产物。咱们一个一个来看。

3.1 dts:设备树源文件

.dts 是设备树的“主源文件”。它描述了一整块板子上的硬件信息,比如CPU、内存地址、外设挂在哪个总线上、中断号是多少等等。

每个板子通常对应一个 .dts 文件。比如我手头有块i.MX6ULL的板子,它的设备树源文件就是 imx6ull-myboard.dts。

一个典型的 .dts 文件长这样:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "My i.MX6ULL Board";
    compatible = "mycompany,imx6ull-board", "fsl,imx6ull";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };

    &uart1 {
        status = "okay";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

注意看,第一行 /dts-v1/; 是版本声明,告诉编译器这是设备树v1语法。后面用 #include 引用了另一个文件——这就是咱们下面要说的 .dtsi。

我的经验: 写 .dts 时,尽量只放板级特有的内容。通用的SoC配置放到 .dtsi 里,这样换板子时只需要改 .dts,省事不少。

3.2 dtsi:设备树头文件/包含文件

.dtsi 就是设备树的“头文件”。它存放的是可复用的硬件描述。比如某个SoC的芯片级配置、时钟、中断控制器、GPIO控制器等,这些在不同板子上基本一样,没必要每个 .dts 都写一遍。

举个例子,imx6ull.dtsi 里定义了芯片内部几乎所有外设的节点:

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu0: cpu@0 {
            compatible = "arm,cortex-a7";
            device_type = "cpu";
            reg = <0>;
        };
    };

    uart1: serial@02020000 {
        compatible = "fsl,imx6ull-uart", "fsl,imx-uart";
        reg = <0x02020000 0x4000>;
        interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
        status = "disabled";
    };

    // ... 其他外设
};

你看,uart1 节点里 status = "disabled",表示默认关闭。然后在板级的 .dts 里,通过 &uart1 引用这个节点,把 status 改成 "okay",再配上具体的引脚复用配置。这就是设备树的“覆盖”机制。

关键区别: .dts 是“最终板级描述”,一个板子一个;.dtsi 是“公共描述”,可以被多个 .dts 包含。关系就像 .c 和 .h。

3.3 dtb:设备树二进制文件

.dtb 是 .dts 经过设备树编译器(dtc)编译后生成的二进制文件。它就是内核启动时真正加载和解析的东西。

编译命令很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

或者在内核源码里,直接 make dtbs 就能把整个目录下的 .dts 都编译成 .dtb。

内核启动时,uboot会把 .dtb 文件加载到内存,然后把地址传给内核。内核解析这个二进制数据,构建出设备树结构,驱动就能从中读取硬件信息了。

注意: .dtb 是二进制文件,不要试图用文本编辑器打开它。我曾经见过有人用vim打开 .dtb 然后说“怎么全是乱码”——嗯,那是二进制数据,不是文本。

3.4 dtbo:设备树覆盖文件

.dtbo 是设备树覆盖(Device Tree Overlay)的编译产物。它用于在系统运行时动态修改设备树,而不需要重新编译整个 .dtb。

这个机制特别有用。比如你的板子上有个可插拔的扩展板,插上后需要添加一些设备节点。你可以在扩展板上放一个 .dtbo 文件,系统检测到后动态加载它,设备树就自动更新了。

一个简单的 .dtso(覆盖源文件)长这样:

/dts-v1/;
/plugin/;

/ {
    compatible = "mycompany,imx6ull-board";

    fragment@0 {
        target = <&ecspi1>;
        __overlay__ {
            status = "okay";
            #address-cells = <1>;
            #size-cells = <0>;

            spidev@0 {
                compatible = "spidev";
                reg = <0>;
                spi-max-frequency = <1000000>;
            };
        };
    };
};

编译成 .dtbo:

dtc -I dts -O dtb -@ -o myoverlay.dtbo myoverlay.dtso

注意那个 -@ 选项,它告诉编译器生成覆盖所需的符号信息。

避坑指南: 我曾经在调试一个FPGA加速卡时,加载 .dtbo 后系统直接挂了。后来发现是覆盖的节点跟已有节点有资源冲突。所以加载覆盖前,最好先用 configfs 检查一下当前设备树状态。

3.5 四者关系一览

为了让你看得更清楚,我画了一张图,展示这四类文件的关系:

设备树文件结构关系图 .dtsi 设备树包含文件 .dts 设备树源文件 #include .dtb 设备树二进制文件 dtc编译 .dtbo 设备树覆盖文件 dtc -@编译 Linux内核 uboot加载 动态加载 虚线表示可选路径(dtbo动态覆盖)

从这张图可以清楚看到:

  • .dtsi 被 .dts 通过 #include 包含,提供公共硬件描述
  • .dts 是完整的板级描述,通过 dtc 编译成 .dtb
  • .dtb 是内核启动时加载的最终二进制文件
  • .dtbo 由 .dtso 编译而来,可在系统运行时动态覆盖设备树

3.6 实际项目中的使用建议

我在几个量产项目中总结了一些经验,分享给你:

  1. 尽量复用 .dtsi:同一个SoC的不同板子,把公共部分抽到 .dtsi 里。我见过一个项目,三块板子共用同一个 .dtsi,只改 .dts 里的几十行,维护起来特别轻松。
  2. .dtb 要跟内核版本匹配:不同内核版本的设备树绑定可能有变化。我曾经把4.19的 .dtb 用在5.4的内核上,结果网卡驱动死活不认——因为绑定接口变了。
  3. dtbo 慎用:动态覆盖虽然方便,但调试起来比较麻烦。如果只是固定硬件,建议直接改 .dts 重新编译。覆盖更适合可插拔模块、FPGA动态重配置这类场景。
  4. 反编译 .dtb 看看:有时候拿到别人的 .dtb,想知道里面有什么,可以用 dtc -I dtb -O dts -o dump.dts dump.dtb 反编译成文本。这是排查问题的好办法。
一句话总结: .dtsi 是公共头,.dts 是板级源,.dtb 是编译结果,.dtbo 是运行时补丁。搞懂这四者的关系,设备树就算入门一半了。

可可精品资料库