第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 知识结构总览
下面这张图把设备树语法的核心知识点串了起来,你可以对照着复习:
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的配置时就会游刃有余。