第3章 设备树语法详解:节点、属性、标签、引用、包含文件、宏定义

设备树的语法,说白了就是一套描述硬件资源的“语言”。你写对了,内核就能自动识别你的板子;写错了,系统可能连启动都起不来。我刚开始接触设备树时,觉得这东西不就是个配置文件嘛,后来踩了几个坑才明白——它的语法细节远比想象中多。

这一章,咱们把设备树的核心语法拆开揉碎了讲。你掌握了这些,后面写RK3568的设备树配置就会顺手很多。

3.1 节点(Node)——设备树的基本单元

设备树里的每个硬件设备、每个总线、每个控制器,都是一个节点。节点用花括号 {} 包裹,节点名通常写成 名称@地址 的形式。

/ {
    cpu {
        compatible = "arm,cortex-a55";
        reg = <0x0>;
    };

    memory@100000 {
        device_type = "memory";
        reg = <0x0100000 0x2000000>;
    };
};

根节点 / 是所有节点的祖先。每个节点内部可以继续嵌套子节点,形成树形结构。我在项目中见过有人把节点名写成了 cpu@0x0,多了个 0x 前缀,结果解析失败——嗯,这里要注意,地址部分默认就是十六进制,不用加 0x。

节点命名规则:
  • 节点名不能以数字开头
  • 地址部分只写数值,不加 0x
  • 同一层级下节点名必须唯一

3.2 属性(Property)——描述节点的特征

属性就是键值对,用来描述节点的各种特征。常见的属性类型有字符串、整数、数组、布尔值等。

属性类型 写法示例 说明
字符串 compatible = "rockchip,rk3568"; 用于匹配驱动
32位整数 reg = <0x100000 0x20000>; 地址和大小
64位整数 reg = <0x0 0x100000 0x0 0x20000>; 高32位+低32位
布尔值 status = "okay"; 存在即真
字符串列表 interrupt-names = "uart0", "uart1"; 逗号分隔

我个人习惯把 compatible 属性放在每个节点的第一行,这样一眼就能看出这个节点对应什么驱动。曾经有个同事把 compatible 写成了 compatiable,少了个 i,内核直接忽略了这个节点——拼写错误是设备树里最常见的坑。

3.3 标签(Label)——给节点起个“外号”

标签就是节点的别名。你可以在节点名前加一个标签,后面通过 &标签名 来引用它。

uart0: serial@ff180000 {
    compatible = "rockchip,rk3568-uart";
    reg = <0xff180000 0x100>;
    interrupts = <0 56 4>;
};

这里 uart0 就是标签。后面你想修改这个节点时,不用重新写整个节点,直接引用标签就行:

&uart0 {
    status = "okay";
    clock-frequency = <24000000>;
};
标签的作用:
  • 方便在其他地方引用和修改节点
  • 避免重复定义整个节点
  • 让设备树文件更模块化

我在RK3568的项目里,习惯把所有外设节点都加上标签,比如 uart0、i2c1、spi2。这样板级文件里只需要引用标签,修改状态或添加子节点就行,不用动核心的 .dtsi 文件。

3.4 引用(Reference)——连接节点的“桥梁”

引用就是通过 &标签名 来访问或扩展一个节点。你可以在多个地方引用同一个节点,编译器会把它们合并成一个节点。

// 在核心文件中定义
i2c1: i2c@ff3f0000 {
    compatible = "rockchip,rk3568-i2c";
    reg = <0xff3f0000 0x1000>;
    status = "disabled";
};

// 在板级文件中引用并扩展
&i2c1 {
    status = "okay";
    clock-frequency = <400000>;

    touch@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
    };
};

你想想看,如果没有引用机制,每个板子都得复制粘贴整个 i2c1 节点,那维护起来得多痛苦?引用让设备树有了“继承”和“扩展”的能力。

引用时的注意事项:
  • 同一个属性在多个引用中重复定义,后面的会覆盖前面的
  • 子节点不会覆盖,而是合并
  • 引用顺序不影响最终结果,编译器会统一处理

3.5 包含文件(Include)——代码复用的利器

设备树支持 #include 预处理指令,用法和C语言一样。你可以把通用的定义放在 .dtsi 文件中,板级文件直接包含进来。

// rk3568.dtsi
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/rk3568-cru.h>

/ {
    compatible = "rockchip,rk3568";
    #address-cells = <2>;
    #size-cells = <2>;

    cpu0: cpu@0 {
        compatible = "arm,cortex-a55";
        reg = <0x0 0x0>;
    };
};

// my-board.dts
#include "rk3568.dtsi"

/ {
    model = "My RK3568 Board";
    memory@200000 {
        device_type = "memory";
        reg = <0x0 0x200000 0x0 0x8000000>;
    };
};

我曾经在项目里见过有人把整个 rk3568.dtsi 的内容直接复制到板级文件里,结果后续芯片升级时,所有板子都得手动改——这就是没有用好包含文件。正确的做法是:芯片级定义放 .dtsi,板级差异放 .dts。

文件类型 扩展名 用途
芯片级定义 .dtsi SoC内部外设、时钟、中断等
板级配置 .dts 板子特有的内存、外设、引脚等
头文件 .h 宏定义、常量、绑定信息

3.6 宏定义(Macro)——让设备树更“智能”

设备树支持C语言的宏定义,你可以用 #define 定义常量,然后在设备树里直接使用。这在中断号、时钟ID、GPIO编号等场景下特别有用。

// dt-bindings/interrupt-controller/arm-gic.h
#define GIC_SPI 0
#define GIC_PPI 1

// dt-bindings/clock/rk3568-cru.h
#define CLK_UART0 100
#define CLK_UART1 101

// 设备树中使用
uart0: serial@ff180000 {
    interrupts = <GIC_SPI 56 4>;
    clocks = <&cru CLK_UART0>;
};

你看,用了宏定义之后,代码的可读性明显提升了。否则你看到 <0 56 4> 和 <100>,还得去查手册才知道是什么意思。

宏定义的使用建议:
  • 中断类型、时钟ID、复位ID等常量一定要用宏
  • GPIO的引脚号和复用功能也可以用宏
  • 宏定义放在 dt-bindings/ 目录下的头文件中

我记得有一次调试RK3568的UART,怎么都收不到数据。折腾了半天,发现是时钟ID写错了——直接写了个数字 100,但实际UART0的时钟ID是 101。从那以后,我强制团队所有设备树文件必须用宏定义,不准出现裸数字。

3.7 知识结构总览

下面这张图把设备树语法的核心知识点串了起来,你可以对照着复习:

设备树语法知识结构图 设备树语法核心知识结构 设备树语法 节点 + 属性 节点名@地址 compatible / reg / status 标签 + 引用 uart0: serial@... &uart0 { ... } 包含 + 宏定义 #include "rk3568.dtsi" #define CLK_UART0 100 最终生成 .dtb 二进制文件 由内核解析并创建 platform_device

3.8 实战中的避坑指南

讲了这么多语法,最后分享几个我实际踩过的坑:

  • 标签重复: 两个 .dtsi 文件里定义了同名的标签,编译器会报错。我建议团队统一标签命名规范,比如 uart0、i2c1,不要用 uart、i2c 这种太通用的名字。
  • 属性覆盖: 引用时如果重复定义了同一个属性,后面的会覆盖前面的。我曾经在板级文件里写了 status = "disabled",结果覆盖了核心文件里的 "okay",导致外设没启动。
  • 宏定义未包含: 用了 GIC_SPI 但忘了 #include 对应的头文件,编译直接报错。这个错误很常见,尤其是从别的项目复制代码时。
  • 地址写错: reg 属性的地址和大小必须匹配 #address-cells 和 #size-cells。RK3568 默认是 <2>,所以每个地址要写两个32位数。
重要提醒: 设备树编译报错时,先检查语法。用 dtc -I dts -O dtb -o output.dtb input.dts 编译,错误信息会精确到行号。别问我怎么知道的——我曾在没有语法检查的情况下,手动排查了三个小时的设备树文件。

设备树语法看起来简单,但细节决定成败。你把这些基础打牢了,后面写RK3568的配置时就会游刃有余。