设备树语法精讲:节点、属性、标签、包含文件、宏定义的使用
设备树(Device Tree)的语法,说白了就是一套描述硬件拓扑的「领域特定语言」。你把它想象成一张硬件配置清单——CPU 有几个核、外设挂在哪条总线上、中断号是多少,全都写清楚。我刚开始接触时也觉得这东西有点绕,但摸清几个核心语法要素后,其实就那么回事。
这一节我们就把设备树语法的五个核心构件掰开揉碎讲清楚:节点、属性、标签、包含文件、宏定义。每个我都会结合项目里的实际场景来说。
核心要点:设备树语法本质上是「树形结构 + 键值对」的文本描述。节点表示硬件设备或总线,属性描述设备特性,标签提供引用锚点,包含文件和宏定义则解决复用和可维护性问题。
2.1 节点:设备树的骨架
节点是设备树最基本的组成单元。每个节点用一对花括号 { } 包裹,以分号结尾。节点可以嵌套,形成树状结构。
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
根节点 / 是整棵树的起点。每个子节点代表一个硬件设备或总线。节点名后面可以跟 @地址,用来区分同类型设备——比如两个串口,一个 serial@10000000,另一个 serial@20000000。
我的习惯:节点命名尽量和芯片手册里的外设名称保持一致。这样后续查问题翻手册时,一眼就能对上号,不用来回猜。
2.2 属性:描述设备的细节
属性就是键值对,写在节点内部。常见的属性类型有:
| 属性类型 | 语法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "vendor,device"; |
驱动匹配的关键标识 |
| 32位整数 | reg = <0x1000 0x100>; |
地址、长度、时钟频率等 |
| 64位整数 | reg = <0x00000000 0x10000000>; |
高32位在前,低32位在后 |
| 布尔值 | status = "disabled"; |
空属性表示 true,有值表示 false |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
MAC 地址、UUID 等 |
| 字符串列表 | clock-names = "uart", "spi"; |
多个字符串用空格分隔 |
这里我要特别提一下 compatible 属性。它决定了内核用哪个驱动来绑定这个设备。格式通常是 "厂商,设备型号"。我在项目中遇到过一个问题:某款 Wi-Fi 芯片的 compatible 写成了 "broadcom,bcm4330",但驱动里匹配的是 "brcm,bcm4330",结果驱动死活加载不上。查了两天才发现是厂商缩写不一致。
避坑指南:compatible 字符串必须和驱动源码里的 of_match_table 完全一致,大小写、逗号、空格都不能错。我曾经因为多了一个空格,浪费了整整一个下午。
2.3 标签:给节点起个「外号」
标签(Label)是节点前面的标识符,用冒号分隔。它的作用是在其他地方引用或覆盖这个节点。
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
有了 uart0 这个标签,我可以在别的 .dts 文件里通过 &uart0 来引用它:
&uart0 {
status = "okay";
clock-frequency = <24000000>;
};
这种写法叫「覆盖(overlay)」。它不会重新定义节点,而是往已有节点里追加或修改属性。我个人觉得这是设备树最优雅的设计之一——板级文件只需要覆盖 SoC 公共文件里的少量属性,不用把整个节点抄一遍。
我的习惯:标签命名用「外设缩写 + 序号」,比如 uart0、i2c1、spi2。这样在几十个节点的文件里也能快速定位。
2.4 包含文件:别重复造轮子
设备树支持 C 语言的 #include 预处理指令。通常 SoC 厂商会提供一个 .dtsi 文件,里面定义了芯片内部所有外设的节点骨架。板级 .dts 文件通过 #include 引入它,然后只修改自己板子特有的部分。
// imx6ull.dtsi(SoC 公共文件)
#include "skeleton.dtsi"
/ {
soc {
aips1: aips-bus@2000000 {
uart1: serial@2020000 {
compatible = "fsl,imx6ul-uart";
reg = <0x2020000 0x4000>;
interrupts = <0 26 4>;
status = "disabled";
};
};
};
};
// myboard.dts(板级文件)
#include "imx6ull.dtsi"
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
这样做的好处很明显:SoC 升级时只需要更新 .dtsi,所有板级文件自动受益。我参与过一个项目,芯片从 A 版升级到 B 版,外设地址变了,我们只改了 .dtsi 里的 reg 值,十几个板级文件一行代码没动。
2.5 宏定义:让数字有名字
设备树编译器(DTC)支持 C 预处理器,所以 #define 宏定义也能用。这在中大型项目中几乎是标配。
#include "imx6ull.dtsi"
#define UART_BAUD_115200 <115200>
#define IRQ_TYPE_EDGE_RISING <0 1 4>
&uart1 {
status = "okay";
clock-frequency = UART_BAUD_115200;
interrupts = IRQ_TYPE_EDGE_RISING;
};
你想想看,如果没有宏定义,每个中断类型都要写 <0 1 4>,谁记得住 4 代表什么?用宏定义之后,代码的可读性一下子就上来了。
实用技巧:很多 SoC 厂商会在 .dtsi 里预定义好 GPIO 和中断的宏,比如 #define GPIO_ACTIVE_HIGH 0、#define IRQ_TYPE_LEVEL_HIGH 4。建议直接复用这些定义,不要自己另起一套。
2.6 综合示例:一个完整的设备树片段
把上面几个要素串起来,看一个实际项目中可能出现的写法:
/dts-v1/;
#include "soc.dtsi"
#include "dt-bindings/gpio/gpio.h"
#include "dt-bindings/interrupt-controller/irq.h"
/ {
model = "Industrial Controller v2.1";
compatible = "mycompany,ic-210", "mycompany,ic-200";
leds {
compatible = "gpio-leds";
status-led {
label = "status:green";
gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
&uart2 {
status = "okay";
pinctrl-0 = <&pinctrl_uart2>;
current-speed = <115200>;
};
&i2c1 {
status = "okay";
clock-frequency = <100000>;
temp-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupts-extended = <&gpio2 5 IRQ_TYPE_EDGE_FALLING>;
};
};
这个例子里:
#include引入了 SoC 公共定义和头文件中的宏- 根节点下定义了板级信息和 GPIO LED
&uart2和&i2c1通过标签覆盖了 SoC 文件中的节点GPIO_ACTIVE_HIGH和IRQ_TYPE_EDGE_FALLING都是宏定义- 温度传感器节点挂在 I2C 总线下,体现了节点嵌套
避坑指南:我曾经在覆盖节点时忘记写 & 符号,结果 DTC 报错说节点重复定义。记住:&uart2 { ... }; 是覆盖,uart2 { ... }; 是新定义。两者差别很大。