2. 设备树语法:节点、属性、标签、包含文件、宏定义

设备树(Device Tree)说白了就是一块板子的“硬件说明书”。它告诉内核:你有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么配。我刚开始接触时也觉得这东西玄乎,后来发现它其实就是一套树形结构——根节点是树干,子节点是树枝,属性就是树叶上的信息。

这一节我们就把设备树的几个核心语法掰开揉碎。你只要掌握了节点、属性、标签、包含文件和宏定义,写设备树就跟填表格一样简单。

核心要点:设备树语法 = 描述硬件的结构化语言,不是C语言,也不是脚本,它有自己的编译器和语法规则。

2.1 节点(Node)—— 树的骨架

设备树里的每个硬件单元都是一个节点。根节点是 /,下面挂 CPU、内存、总线、外设。每个节点可以嵌套子节点,形成层级关系。

/ {
    compatible = "acme,coyote's-revenge";

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };

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

节点命名有讲究:节点名@地址。地址部分通常写寄存器基址或单元编号。比如 cpu@0 表示第0个CPU核,memory@80000000 表示基址在0x80000000的内存。

我的习惯:节点名尽量跟芯片手册保持一致。比如I2C控制器就叫 i2c@地址,UART就叫 serial@地址。这样查手册时一眼就能对上号。

2.2 属性(Property)—— 节点的血肉

属性就是节点的具体信息。常见的属性类型有四种:

属性类型 写法示例 说明
字符串 compatible = "arm,cortex-a9"; 用于匹配驱动,格式为"厂商,型号"
32位整数 reg = <0x10000000 0x1000>; 地址和长度,用尖括号包裹
布尔值 status = "disabled"; 用字符串"okay"或"disabled"表示
数组 interrupts = <0 31 4>; 多个值用空格分隔

最关键的属性是 compatible。内核驱动就是靠它来匹配设备的。我见过有人把厂商名写错,结果驱动死活加载不上——查了两天才发现是拼写问题。

避坑指南:compatible 字符串必须严格遵循"厂商,设备型号"格式,且与驱动中的 of_match_table 完全一致。大小写、逗号、空格都不能错。

2.3 标签(Label)—— 给节点起个外号

标签就是节点的别名。用 标签名: 节点名@地址 的格式定义。有了标签,其他地方引用这个节点就方便多了。

uart1: serial@10001000 {
    compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x10001000 0x1000>;
    interrupts = <0 26 4>;
    status = "disabled";
};

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

你看,先定义 uart1 标签,后面用 &uart1 引用它来修改属性。这种写法在设备树里非常常见,尤其是做板级适配时。

我建议:每个外设节点都加上标签。哪怕当前不需要引用,后续做 overlay 或者板级差异化时,有标签会省很多事。

2.4 包含文件(Include)—— 代码复用

设备树支持 #include 预处理指令,跟C语言一样。通常用来引入芯片级 .dtsi 文件、头文件或者宏定义。

// 在 .dts 文件中
#include "imx6q.dtsi"
#include "imx6qdl.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

.dtsi 文件是芯片厂商提供的“公版”设备树,定义了芯片内部所有外设的默认配置。板级 .dts 文件通过 #include 引入它,然后只修改需要差异化的部分。

我记得第一次做项目时,厂商给了个 .dtsi 文件,里面定义了上百个节点。我直接在 .dts 里重新写了一遍——后来被导师骂了一顿。正确的做法是:#include 进来,然后用 &标签 覆盖修改。

最佳实践:芯片级通用配置放 .dtsi,板级差异化放 .dts。不要复制粘贴,用包含+覆盖的方式。

2.5 宏定义(Macro)—— 让数字变得可读

设备树里经常出现一堆数字,比如中断号、GPIO号、时钟频率。直接用数字写,别人看不懂,你自己过两周也忘了。宏定义就是用来解决这个问题的。

// 在头文件中定义
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW  1

#define IRQ_TYPE_EDGE_RISING  1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_LEVEL_HIGH   4

// 在设备树中使用
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

led-red {
    compatible = "gpio-leds";
    gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
    interrupt-parent = <&intc>;
    interrupts = <0 31 IRQ_TYPE_LEVEL_HIGH>;
};

你看,GPIO_ACTIVE_LOW 比直接写 1 好理解多了。内核在 dt-bindings/ 目录下提供了大量现成的宏定义,直接用就行。

注意:宏定义只在编译预处理阶段生效。编译后的 .dtb 文件里,宏已经被替换成数字了。所以调试时看反汇编是看不到宏名字的。

2.6 知识结构总览

下面这张图把设备树语法的核心要素串起来了。你可以把它当作速查表:

设备树语法核心要素 根节点 / 子节点 cpu@0 子节点 memory 子节点 uart1 属性 compatible "厂商,设备型号" 属性 reg <地址 长度> 属性 interrupts <类型 号 触发> 标签 uart1: 引用时用 &uart1 #include 引入 .dtsi / 头文件 宏定义 #define GPIO_ACTIVE_LOW 1 核心流程 .dts + .dtsi + 头文件 → 预处理 → 编译 → .dtb → 内核解析

2.7 实战:写一个完整的设备树片段

把上面这些知识点串起来,写一个实际的例子。假设我们要描述一个I2C控制器,上面挂了一个温度传感器:

// 引入芯片级定义和宏
#include "imx6q.dtsi"
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/gpio.h>

/ {
    model = "Acme i.MX6Q Board";
    compatible = "acme,imx6q-board", "fsl,imx6q";

    // I2C控制器节点,带标签
    i2c1: i2c@021a0000 {
        compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
        reg = <0x021a0000 0x4000>;
        interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&clks IMX6QDL_CLK_I2C1>;
        status = "okay";
        clock-frequency = <100000>;

        // 子节点:温度传感器
        temp_sensor: lm75@48 {
            compatible = "national,lm75";
            reg = <0x48>;
            status = "okay";
        };
    };
};

// 板级覆盖:使能I2C1
&i2c1 {
    status = "okay";
};

这个例子包含了:根节点、子节点、标签、属性、包含文件、宏定义。你仔细看,IRQ_TYPE_LEVEL_HIGH 是宏,IMX6QDL_CLK_I2C1 也是宏,&i2c1 是标签引用,#include 引入了 .dtsi 和头文件。

我的经验:写设备树时,先找芯片厂商的 .dtsi 文件看看。里面已经把大部分外设节点定义好了,你只需要做板级差异化。别从头造轮子。

2.8 常见错误与避坑

  • compatible 拼写错误——驱动匹配不上,设备不工作。我曾经把 "fsl,imx6q-uart" 写成 "fsl,imx6q-uart1",多了一个数字,查了两天。
  • reg 地址写错——外设寄存器基址不对,内核访问时直接挂死。一定要跟芯片手册核对。
  • 中断号对不上——中断控制器编号从0开始还是从1开始?不同芯片不一样。我建议先看内核里其他板子的写法。
  • 标签重复——同一个标签名不能出现两次。编译时会有警告,但不会报错,运行时行为不可预期。
  • 忘记 #include——宏定义没引入,编译报错。这个最基础,但新手经常犯。
避坑指南:编译设备树时,用 make dtbs 或者 dtc -I dts -O dtb -o xxx.dtb xxx.dts。如果报错,先检查语法。我习惯用 dtc -@ 开启 overlay 支持,方便后续扩展。

嗯,设备树语法其实不复杂。你只要记住:节点是骨架,属性是血肉,标签是别名,包含文件是复用,宏定义是让数字可读。把这五样东西用好,写设备树就跟搭积木一样。