设备树基础语法:DTS、DTC、DTB、DTSI 文件格式与编译流程
设备树这东西,刚接触时容易懵。一堆缩写来回倒腾,DTS、DTB、DTC、DTSI……到底谁是谁?
我当年第一次移植内核时,就栽在设备树编译上。改了个 dts 文件,make 完直接黑屏。后来才发现,是忘了把 include 的 dtsi 路径写对。
说白了,设备树就是一套描述硬件的「配置文件」。它告诉内核:你有哪些外设、挂在哪条总线上、寄存器地址是多少、中断号是几。没有它,内核就是个瞎子。
四种文件格式,各司其职
先理清这四种文件的关系。我习惯把它们分成「源码」和「产物」两类:
| 文件后缀 | 全称 | 本质 | 是否可读 |
|---|---|---|---|
| .dts | Device Tree Source | 设备树源码(文本) | 是 |
| .dtsi | Device Tree Source Include | 设备树头文件(文本) | 是 |
| .dtb | Device Tree Blob | 编译后的二进制 | 否 |
| .dtbo | Device Tree Blob Overlay | 叠加层二进制 | 否 |
.dts 是主文件,一个板子通常对应一个。比如 imx6ull-myboard.dts。
.dtsi 是公共片段,类似 C 语言的 .h 头文件。SoC 厂商会把 CPU、内存控制器、时钟这些通用部分写在 .dtsi 里,板级差异才放 .dts。
.dtb 是最终产物,Bootloader 直接加载它传给内核。
.dtbo 是后来加的,用于运行时动态修改设备树。我用的不多,但树莓派和 FPGA 场景很常见。
.dtsi 里的 soc 节点。那里藏着 SoC 所有外设的骨架,比看原理图快多了。
DTS 语法速览:节点、属性、值
设备树的语法其实很简单。一个节点就是一个设备,属性就是设备的参数。
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "MyBoard i.MX6ULL";
compatible = "mycompany,imx6ull-myboard";
chosen {
stdout-path = &uart1;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
leds {
compatible = "gpio-leds";
led0 {
label = "heartbeat";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
};
};
几个关键点:
- 根节点用
/表示,一个文件只有一个。 - 节点名可以带
@地址,比如memory@80000000,表示这个设备在 0x80000000 处。 - 属性分几种:字符串用双引号,32 位整数用尖括号
<>,字节数组用方括号[]。 - &uart1 这种写法叫「引用」,用来引用其他节点。我经常用它来给已有节点追加属性。
LED_0,结果驱动匹配不上,查了一下午。
编译流程:从 DTS 到 DTB
整个过程就三步:预处理、编译、打包。嗯,跟 C 语言编译几乎一样。
# 第一步:预处理(展开 #include 和宏)
cpp -nostdinc -I include -undef -x assembler-with-cpp \
imx6ull-myboard.dts imx6ull-myboard.dts.pre
# 第二步:编译为 DTB
dtc -I dts -O dtb -o imx6ull-myboard.dtb imx6ull-myboard.dts.pre
# 第三步(可选):反编译验证
dtc -I dtb -O dts -o imx6ull-myboard.dts.dump imx6ull-myboard.dtb
实际工程中,你不需要手动敲这些命令。内核的 Makefile 已经封装好了:
make ARCH=arm imx6ull-myboard.dtb
它会自动处理依赖关系。你改了 .dtsi,它也会重新编译所有引用它的 .dts。
DTC 编译器:不只是翻译
DTC(Device Tree Compiler)是设备树的灵魂工具。它除了把文本转二进制,还会做两件重要的事:
- 语法检查: 节点名是否合法、属性值类型是否匹配、地址长度是否对齐。我遇到过最坑的是
reg属性少写了一个<>,DTC 直接报段错误。 - 自动合并: 同一个节点如果在多个地方出现,DTC 会把属性合并到一起。这就是为什么
.dtsi里定义节点,.dts里可以追加属性。
DTC 还有一些实用选项:
| 选项 | 作用 | 我什么时候用 |
|---|---|---|
-@ |
生成叠加层支持信息 | 做动态设备树时必加 |
-H |
指定 phandle 格式 | 兼容旧版 U-Boot 时用 |
-Wno-unit_address_vs_reg |
忽略地址不匹配警告 | 某些老 SoC 的 dtsi 会触发,不影响功能 |
SVG:设备树编译与加载全流程
下面这张图,把从源码到内核解析的完整路径画出来了。我每次给新人讲设备树,都会先画一遍这个流程。
避坑指南:我踩过的三个坑
这些年跟设备树打交道,没少被折腾。挑三个典型的说说:
-
phandle 冲突
我曾经在.dtsi里定义了一个gpio0节点,又在.dts里重新定义了一个同名节点。DTC 编译不报错,但运行时内核拿到的 phandle 是错的,GPIO 控制完全乱套。后来我养成了习惯:所有节点引用都用&语法,绝不重复定义完整节点。 -
reg 属性长度不匹配
有一次移植 LCD 驱动,屏幕死活不亮。反编译 DTB 一看,reg属性里地址是 32 位,长度却是 64 位的格式。DTC 的#address-cells和#size-cells没配对。这个坑特别隐蔽,因为 DTC 只检查格式,不检查语义。 -
U-Boot 传错 DTB
有块板子有两个 DTB 文件,一个对应 512MB 内存,一个对应 1GB。U-Boot 的 bootcmd 里写死了文件名,换了内存颗粒后忘了改。内核启动时 panic,提示Unable to handle kernel NULL pointer dereference。查了两天才发现是 DTB 里的内存大小跟实际不匹配。
/sys/firmware/devicetree/base 看看。那里是内核解析后的设备树,跟你的 .dts 对比一下,能快速定位问题。我几乎每次调设备树都会用这招。
小结
设备树的基础语法不复杂,但细节决定成败。DTS 是源码,DTSI 是公共片段,DTC 是编译器,DTB 是最终产物。记住这个链条,你就掌握了设备树的骨架。