设备树语法入门:节点、属性、标签、引用、包含文件

设备树(Device Tree)的语法,说白了就是一套描述硬件拓扑的「领域特定语言」。你不需要把它想得多复杂——它本质上就是个树形结构的文本文件,每个硬件设备对应一个节点,节点下面挂属性和子节点。

我刚开始接触设备树时,总觉得这玩意儿跟 XML 差不多。后来调了几个驱动才发现,它的引用机制和编译方式有不少坑。今天咱们就把这些基础语法掰开揉碎,一个一个说清楚。

核心要点:设备树语法包含 5 个基本元素——节点(node)、属性(property)、标签(label)、引用(reference)、包含文件(#include)。掌握这 5 个,你就能读懂 90% 的设备树源文件。

2.1 节点(Node):硬件拓扑的基本单元

节点是设备树最基本的组成单位。每个节点代表一个硬件设备或总线,格式很简单:节点名@地址。

/dts-v1/;

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0 0x100>;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };
};

看到这个结构了吗?根节点 / 下面挂了两个子节点:cpu@0 和 memory@80000000。节点名后面的 @ 跟的是地址,用来区分同名的节点。比如两个串口,可以写成 uart@1000 和 uart@2000。

我在项目中遇到过一个问题:有人把节点名写成了 CPU@0(大写),结果驱动匹配不上。设备树的节点名是大小写敏感的,这一点千万注意。

个人习惯:节点名统一用小写字母,地址用十六进制小写。这样既规范,又不容易出错。

2.2 属性(Property):描述节点的特征

属性就是挂在节点上的键值对,用来描述硬件的各种参数。常见的属性类型有 4 种:

属性类型 格式 示例 说明
字符串 "string" compatible = "ti,am335x-uart"; 用于匹配驱动
32 位整数 <value> reg = <0x1000 0x200>; 地址、中断号等
64 位整数 <0x00000000 0x80000000> reg = <0x00000000 0x80000000 0x00000000 0x40000000>; 高 32 位 + 低 32 位
字节数组 [value] local-mac-address = [00 11 22 33 44 55]; MAC 地址等

最常用的属性是 compatible 和 reg。compatible 用来告诉内核:这个设备用哪个驱动。格式是 "厂商,设备型号",比如 "ti,am335x-uart"。

reg 描述设备的地址空间。它的长度由父节点的 #address-cells 和 #size-cells 决定。举个例子:

bus {
    #address-cells = <1>;
    #size-cells = <1>;

    device@1000 {
        reg = <0x1000 0x100>;  // 起始地址 0x1000,长度 0x100
    };
};

这里 #address-cells = 1 表示地址用 1 个 32 位整数表示,#size-cells = 1 表示长度也用 1 个 32 位整数。所以 reg 里每两个数一组:地址 + 长度。

我曾经踩过的坑:写 SPI 设备节点时,忘了设置 #address-cells 和 #size-cells,结果内核解析 reg 属性时直接报错。记住:这两个属性必须在父节点中定义,子节点才能正确使用 reg。

2.3 标签(Label):给节点起个名字

标签就是节点的别名。用 标签名: 放在节点前面,其他地方就能通过 &标签名 来引用这个节点。

uart0: serial@1000 {
    compatible = "ns16550";
    reg = <0x1000 0x100>;
    interrupts = <33>;
};

这里 uart0 就是标签。后面想引用这个串口节点,直接写 &uart0 就行。

我建议每个重要的外设都加上标签。为什么?因为设备树经常需要覆盖或追加属性,没有标签你根本没法引用。比如你要在板级文件中修改串口的波特率:

&uart0 {
    clock-frequency = <115200>;
};

没有标签的话,你得把整个节点重新写一遍,那多麻烦。

2.4 引用(Reference):连接节点的桥梁

引用有两种形式:一种是上面说的 &标签名,另一种是 &{/路径}。前者更常用,后者适合临时引用没有标签的节点。

// 方式一:通过标签引用(推荐)
&uart0 {
    status = "okay";
};

// 方式二:通过路径引用(不常用)
&{/soc/serial@1000} {
    status = "okay";
};

引用最常见的用途是覆盖属性。比如 SoC 的 .dtsi 文件里把某个外设的 status 设为 "disabled",板级 .dts 文件里通过引用把它改成 "okay"。这就是设备树的分层设计思想。

小技巧:编译设备树时,如果引用了一个不存在的标签,编译器会报错。所以写引用之前,先确认标签确实存在。我习惯在 .dtsi 文件里把所有外设都加上标签,这样板级文件引用起来很方便。

2.5 包含文件(#include):复用代码的利器

#include 和 C 语言的预处理指令一样,用来包含其他文件。设备树里主要包含两种文件:.dtsi(设备树包含文件)和 .h(头文件)。

// 包含 .dtsi 文件
#include "imx6ull.dtsi"

// 包含 .h 头文件(里面定义宏)
#include "imx6ull-pinfunc.h"

/ {
    model = "My i.MX6ULL Board";
    compatible = "my,imx6ull-board", "fsl,imx6ull";
};

.dtsi 文件通常放 SoC 的公共定义,比如 CPU、中断控制器、时钟等。板级 .dts 文件通过 #include 引入 .dtsi,然后覆盖或追加板级特有的配置。

头文件里一般放宏定义,比如 GPIO 号、引脚复用配置。举个例子:

// imx6ull-pinfunc.h
#define MX6UL_PAD_UART1_TX__UART1_DCE_TX  0x0084 0x0310 5 0 0
#define MX6UL_PAD_UART1_RX__UART1_DCE_RX  0x0088 0x0314 5 0 0

然后在设备树里直接用宏:

&uart1 {
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

&iomuxc {
    pinctrl_uart1: uart1grp {
        fsl,pins = <
            MX6UL_PAD_UART1_TX__UART1_DCE_TX  0x1b0b1
            MX6UL_PAD_UART1_RX__UART1_DCE_RX  0x1b0b1
        >;
    };
};

你看,用宏代替那些冗长的十六进制数,代码可读性一下子提升了不少。

注意:#include 和 /include/ 是两回事。#include 是 C 预处理器的指令,支持宏展开;/include/ 是设备树编译器(DTC)自己的包含指令,不支持宏。我建议统一用 #include,因为更灵活。

2.6 知识体系总览

说了这么多,咱们用一张图把设备树语法的核心逻辑串起来:

设备树语法核心要素 设备树源文件 (.dts) 节点 (Node) 属性 (Property) 标签 (Label) 引用 (Reference) 包含文件 (#include) 各要素的作用 • 节点:描述硬件设备或总线,形成树形结构 • 属性:描述节点的特征,如地址、中断号、兼容性 • 标签:给节点起别名,方便其他地方引用 • 引用:通过 &标签 或 &{/路径} 覆盖或追加属性 • 包含文件:复用 SoC 公共定义,支持宏展开

2.7 实战建议

学完这些语法,你可能会问:实际开发中怎么用?我分享几个经验:

  • 先读后写:拿到一个新板子,先看它的 .dts 和 .dtsi 文件,理解节点和属性的组织方式。我每次接手新项目,第一件事就是通读设备树。
  • 善用标签:所有外设节点都加上标签,哪怕当前不需要覆盖。因为你永远不知道后面会不会改。
  • 分层设计:SoC 公共部分放 .dtsi,板级特有部分放 .dts,通过 #include 组合。这样代码复用率高,维护也方便。
  • 编译验证:写完设备树,用 dtc -I dts -O dtb -o test.dtb test.dts 编译一下。如果有语法错误,编译器会告诉你具体位置。

我的习惯:在 .dtsi 文件开头加一段注释,写明这个文件定义了哪些标签、哪些节点。这样别人接手时,一眼就能知道文件的结构。

设备树语法其实不复杂,难的是把硬件手册里的信息准确翻译成节点和属性。多写几次,你就能找到感觉了。