设备树语法精讲:节点、属性、标签、包含文件、宏定义的使用

设备树(Device Tree)的语法,说白了就是一套描述硬件拓扑的「领域特定语言」。你把它想象成一张硬件配置清单——CPU 有几个核、外设挂在哪条总线上、中断号是多少,全都写清楚。我刚开始接触时也觉得这东西有点绕,但摸清几个核心语法要素后,其实就那么回事。

这一节我们就把设备树语法的五个核心构件掰开揉碎讲清楚:节点、属性、标签、包含文件、宏定义。每个我都会结合项目里的实际场景来说。

核心要点:设备树语法本质上是「树形结构 + 键值对」的文本描述。节点表示硬件设备或总线,属性描述设备特性,标签提供引用锚点,包含文件和宏定义则解决复用和可维护性问题。

设备树语法核心要素关系图 设备树语法核心要素 设备树源文件 .dts 节点 (Node) / { cpu { ... } }; 属性 (Property) reg = <0x1000>; 标签 (Label) uart0: serial@...; 包含文件 #include "soc.dtsi" 宏定义 #define GPIO_ACTIVE_HIGH 0 语法要素协作关系 • 节点构成树形结构,每个节点代表一个设备或总线 • 属性附着在节点上,描述设备的寄存器地址、中断、时钟等 • 标签为节点起别名,方便在其他地方引用或覆盖 • 包含文件引入 .dtsi 公共定义,避免重复编写 • 宏定义配合包含文件,实现参数化配置

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 { ... }; 是新定义。两者差别很大。