4. 设备树基础语法:dts/dtsi/dtb 文件格式、节点与属性
设备树,说白了就是一块「硬件描述板」。它告诉内核:你有哪些外设、它们挂在哪根总线上、地址是多少、中断怎么接。没有它,Linux 内核就是个瞎子,连串口都不知道去哪找。
我记得刚接触设备树那会儿,看着一堆 .dts 文件,心里直犯嘀咕:这不就是个 XML 的变种吗?后来调 NPU 驱动时被设备树坑过几次,才老老实实把语法啃了一遍。今天咱们就把这块硬骨头啃下来。
4.1 三种文件格式:dts、dtsi、dtb
先搞清楚这三个东西是干嘛的:
| 后缀 | 全称 | 作用 | 谁写的 |
|---|---|---|---|
.dts |
Device Tree Source | 设备树源文件,描述单板硬件 | 板级工程师 |
.dtsi |
Device Tree Source Include | 公共头文件,被 .dts 包含 |
芯片厂商 |
.dtb |
Device Tree Blob | 编译后的二进制,内核直接解析 | 编译工具生成 |
.dtsi 就像 C 语言的 .h 头文件。芯片厂商会把 SoC 的公共部分——比如 CPU 核、GIC 中断控制器、UART 外设——都放在 .dtsi 里。板级工程师只需要写 .dts,然后 #include 进来,再添加板子特有的东西,比如 GPIO 按键、LED、外接的摄像头传感器。
.dts 和 .dtsi 文件。芯片厂商的 .dtsi 一般不会错,但板级 .dts 经常有引脚复用冲突。我曾经在 RK3588 的板子上排查了三天 NPU 不工作,最后发现是 .dts 里把 NPU 的中断引脚配成了普通 GPIO。
4.2 节点与属性:设备树的基本单元
设备树的语法其实很简单。一个节点代表一个设备,属性描述这个设备的特征。来看个例子:
/dts-v1/;
#include "rk3588.dtsi"
/ {
model = "Edge AI Board v1.0";
compatible = "mycompany,edge-ai-board", "rockchip,rk3588";
chosen {
stdout-path = &uart2;
};
npu: npu@fdab0000 {
compatible = "rockchip,rk3588-npu";
reg = <0x0 0xfdab0000 0x0 0x10000>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru CLK_NPU>;
clock-names = "npu";
status = "okay";
};
};
这里有几个关键点:
- 根节点:
/是设备树的根,所有设备都挂在这个根下面。 - 节点名:
npu@fdab0000,@后面是寄存器基地址,用来区分同名节点。 - 标签:
npu:是标签,方便其他地方引用。比如&npu就代表这个节点。 - 属性:
compatible、reg、interrupts这些都是属性,每个属性都有固定的格式。
@ 地址不是必须的,但如果同一个父节点下有多个同类型设备,必须用地址区分。否则编译会报重复节点错误。
4.3 标准属性详解
设备树里有一堆标准属性,内核的驱动框架就靠它们来匹配设备和驱动。我挑三个最常用的讲:
4.3.1 compatible:驱动匹配的钥匙
compatible 是设备树里最重要的属性,没有之一。它的格式是 "厂商,设备型号",内核通过它找到对应的驱动。
compatible = "mycompany,edge-ai-board", "rockchip,rk3588";
内核匹配时,会从左到右依次尝试。第一个匹配不上,就用第二个。这给兼容性留了空间——你可以先写自己的型号,再写通用的型号。
我在项目中遇到过一个问题:NPU 驱动死活不 probe,打印说 of_match_table 没匹配上。查了半天,发现 .dts 里写的是 "rockchip,rk3588-npu",但驱动里写的是 "rockchip,rk3588-npu-v2"。就差一个 -v2,驱动就不认了。
4.3.2 reg:告诉驱动去哪找寄存器
reg 描述设备的地址空间。格式是 <地址 长度>,可以有多个地址段。
reg = <0x0 0xfdab0000 0x0 0x10000>;
这个例子表示:NPU 的寄存器基地址是 0xfdab0000,地址范围是 0x10000(64KB)。为什么是两对?因为这是 64 位地址,高 32 位是 0x0,低 32 位是 0xfdab0000。
reg 里的长度写小了,驱动只映射了前 4KB,而 NPU 的关键寄存器在偏移 0x5000 的位置。驱动访问越界,读回来的全是 0。
4.3.3 interrupts:中断怎么接
interrupts 描述设备的中断信息。格式取决于中断控制器,ARM 平台一般用 GIC(Generic Interrupt Controller)。
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
三个字段的含义:
- 中断类型:
GIC_SPI是共享外设中断,GIC_PPI是私有外设中断。 - 中断号:82,这是硬件中断号,不是 Linux 的 IRQ 号。
- 触发方式:
IRQ_TYPE_LEVEL_HIGH是高电平触发,还有上升沿、下降沿等。
你想想看,如果中断号写错了,驱动注册的中断永远不会触发。或者触发方式写错了,中断可能被触发两次。这些坑我都踩过。
4.4 phandle 与标签:设备树里的指针
设备树里经常需要引用其他节点,比如时钟控制器、中断控制器、GPIO 控制器。这时候就需要 phandle(指针句柄)和标签。
标签就是节点名前面的那个标识符:
npu: npu@fdab0000 {
...
};
这里的 npu: 就是标签。其他地方可以用 &npu 来引用这个节点。比如:
clocks = <&cru CLK_NPU>;
这表示 NPU 的时钟来自 cru 节点(Clock and Reset Unit)的 CLK_NPU 时钟输出。
编译时,设备树编译器会把 &npu 转换成 phandle——一个 32 位的整数指针。内核解析时,通过这个 phandle 找到对应的节点。
dtc -I dtb -O dts 反编译 .dtb 文件,你会看到 phandle 被展开成数字。比如 phandle = <0x0000007d>。这有助于调试时确认引用关系是否正确。
4.5 知识结构图
下面这张图把设备树的核心概念串起来了:
4.6 编译与反编译
设备树源文件需要编译成 .dtb 才能被内核使用。工具是 dtc(Device Tree Compiler)。
# 编译:dts → dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译:dtb → dts
dtc -I dtb -O dts -o myboard.dts myboard.dtb
反编译是调试利器。内核启动时如果报设备树错误,你可以把 .dtb 反编译成 .dts,看看实际解析出来的内容对不对。我曾经用这个办法发现了一个 reg 属性被编译后地址偏移了 4 字节的问题——因为 .dts 里少写了一个 0x0 占位。
.dts是源文件,.dtsi是公共头文件,.dtb是二进制产物。- 节点代表设备,属性描述设备特征。
compatible匹配驱动,reg定义地址,interrupts配置中断。- 标签
npu:和引用&npu实现节点间关联。 dtc工具负责编译和反编译。