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
注意那个 -@ 选项,它告诉编译器生成覆盖所需的符号信息。
.dtbo 后系统直接挂了。后来发现是覆盖的节点跟已有节点有资源冲突。所以加载覆盖前,最好先用 configfs 检查一下当前设备树状态。
3.5 四者关系一览
为了让你看得更清楚,我画了一张图,展示这四类文件的关系:
从这张图可以清楚看到:
- .dtsi 被 .dts 通过
#include包含,提供公共硬件描述 - .dts 是完整的板级描述,通过
dtc编译成 .dtb - .dtb 是内核启动时加载的最终二进制文件
- .dtbo 由
.dtso编译而来,可在系统运行时动态覆盖设备树
3.6 实际项目中的使用建议
我在几个量产项目中总结了一些经验,分享给你:
- 尽量复用 .dtsi:同一个SoC的不同板子,把公共部分抽到
.dtsi里。我见过一个项目,三块板子共用同一个.dtsi,只改.dts里的几十行,维护起来特别轻松。 - .dtb 要跟内核版本匹配:不同内核版本的设备树绑定可能有变化。我曾经把4.19的
.dtb用在5.4的内核上,结果网卡驱动死活不认——因为绑定接口变了。 - dtbo 慎用:动态覆盖虽然方便,但调试起来比较麻烦。如果只是固定硬件,建议直接改
.dts重新编译。覆盖更适合可插拔模块、FPGA动态重配置这类场景。 - 反编译 .dtb 看看:有时候拿到别人的
.dtb,想知道里面有什么,可以用dtc -I dtb -O dts -o dump.dts dump.dtb反编译成文本。这是排查问题的好办法。
.dtsi 是公共头,.dts 是板级源,.dtb 是编译结果,.dtbo 是运行时补丁。搞懂这四者的关系,设备树就算入门一半了。