2. 设备树语法入门:节点、属性、标签、包含与宏定义
设备树(Device Tree,DT)是一种描述硬件拓扑与资源配置的树形数据结构,由一系列节点(node)、属性(property)、标签(label)、包含(include)与宏定义组成。掌握这些基本语法要素,是编写与调试板级设备树文件(.dts / .dtsi)的前提。
💡 核心认知:设备树不是C语言代码,而是一种结构化描述语言,其语法风格类似JSON与C的混合体,但更简洁。所有硬件信息通过节点与属性的层级关系表达,内核在启动时解析DTB(Device Tree Blob)并据此初始化驱动。
2.1 节点(Node)—— 设备树的骨架
节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线、控制器或功能模块。节点通过路径(path)唯一标识,根节点为 /,子节点通过层级关系组织。
2.1.1 节点语法格式
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
device_type = "cpu";
reg = <0x00>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
};
节点命名规则:节点名@单元地址,其中@单元地址可选,用于区分同一父节点下地址不同的同类设备。
📌 节点关键规则:
- 每个节点以
节点名 { ... };定义,花括号内包含属性和子节点。 - 节点名应使用小写字母、数字和连字符,推荐使用通用名称(如
serial、mmc、i2c)。 - 同一父节点下,节点名必须唯一;若地址不同,可通过
@地址区分。 - 根节点
/是唯一没有父节点的节点,所有设备挂载其下。
2.1.2 节点层级关系 SVG 图
2.2 属性(Property)—— 描述硬件特征
属性是附着在节点上的键值对,用于描述硬件的兼容性、地址、中断、时钟、GPIO等特征。属性值可以是字符串、整数、字节数组、字符串列表或二进制数据。
2.2.1 属性类型与写法
| 属性类型 | 语法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ti,am335x-bone-black"; |
用于驱动匹配,格式为 厂商,型号 |
| 字符串列表 | compatible = "ti,am335x-bone-black", "ti,am33xx"; |
多个字符串用逗号分隔,内核按顺序匹配 |
| 32位整数 | reg = <0x80000000 0x20000000>; |
尖括号内为32位整数,可多个,空格分隔 |
| 64位整数 | reg = <0x00000000 0x80000000>; |
使用两个32位整数拼接表示64位地址 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号内为十六进制字节,空格分隔 |
| 布尔值 | status = "disabled"; |
通过字符串 "okay" / "disabled" 表示 |
| 混合类型 | gpios = <&gpio0 15 0>; |
可包含标签引用、整数等混合内容 |
2.2.2 标准属性速查表
| 属性名 | 用途 | 必选/可选 |
|---|---|---|
compatible |
驱动匹配字符串,格式 厂商,设备名 |
必选(根节点与设备节点) |
reg |
寄存器地址与大小,格式 <地址 长度> |
必选(有地址的设备) |
#address-cells |
子节点 reg 中地址字段占用的32位字数 |
必选(总线节点) |
#size-cells |
子节点 reg 中长度字段占用的32位字数 |
必选(总线节点) |
interrupt-parent |
指定中断控制器节点引用 | 可选(有中断的设备) |
interrupts |
中断号与触发类型 | 可选(有中断的设备) |
status |
设备状态:"okay" / "disabled" / "reserved" |
可选 |
device_type |
设备类型,如 "cpu"、"memory" |
可选(CPU/内存节点建议使用) |
⚠️ 常见错误:
reg属性中地址与长度的数量必须与父节点的#address-cells和#size-cells一致,否则内核解析会出错。compatible字符串顺序很重要:内核按列表顺序匹配驱动,最精确的匹配项应放在最前面。
2.3 标签(Label)—— 节点引用与复用
标签是设备树中用于引用节点的标识符,语法为 标签名: 节点名 { ... };。标签允许在设备树的其他位置通过 &标签名 引用该节点,实现节点属性的追加或覆盖。
2.3.1 标签定义与引用
// 定义标签
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "disabled";
};
// 在其他文件中引用标签,追加属性
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
2.3.2 标签的作用域与最佳实践
| 场景 | 说明 | 示例 |
|---|---|---|
| 板级覆盖 | 在 .dts 文件中引用 .dtsi 中定义的节点,修改其属性 |
&uart0 { status = "okay"; }; |
| 引脚复用 | 通过标签引用 pinctrl 节点,配置引脚功能 | pinctrl-0 = <&uart0_pins>; |
| GPIO/中断引用 | 在属性中引用其他节点作为句柄 | gpios = <&gpio2 4 GPIO_ACTIVE_LOW>; |
| 时钟/电源引用 | 引用时钟控制器或电源管理节点 | clocks = <&clk_uart0>; |
🔖 标签命名规范:
- 使用小写字母、数字和下划线,如
uart0、i2c1、mmc0_pins。 - 标签名应具有描述性,便于在多个文件中识别。
- 避免使用过于简短的标签(如
a、b),防止命名冲突。
2.4 包含(Include)—— 模块化与复用
设备树使用 #include 预处理指令引入其他文件,实现硬件描述的模块化。通常 SoC 厂商提供 .dtsi(设备树包含文件),板级 .dts 文件通过 #include 引入 SoC 定义,再覆盖或追加板级特有属性。
2.4.1 包含文件层级结构
// am335x-boneblack.dts
#include "am33xx.dtsi" // SoC 级定义
#include "am335x-bone-common.dtsi" // 板级公共定义
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
&uart0 {
status = "okay";
};
};
2.4.2 包含文件组织原则
| 文件类型 | 扩展名 | 内容范围 | 示例 |
|---|---|---|---|
| SoC 定义文件 | .dtsi |
CPU、中断控制器、时钟、外设控制器等 SoC 级硬件 | am33xx.dtsi |
| 板级公共文件 | .dtsi |
同一参考设计下多个板卡共享的硬件描述 | am335x-bone-common.dtsi |
| 板级最终文件 | .dts |
具体板卡的完整设备树,包含所有覆盖与追加 | am335x-boneblack.dts |
| 头文件 | .h |
宏定义、GPIO 常量、中断类型等 | dt-bindings/gpio/gpio.h |
📂 包含路径提示:设备树编译器(DTC)在解析
#include 时,默认搜索 -I 参数指定的路径。内核源码中,arch/arm/boot/dts/ 和 include/dt-bindings/ 是常用的包含路径。
2.5 宏定义 —— 增强可读性与可维护性
设备树支持 C 预处理器宏定义,通过 #define 定义常量,在属性值中直接使用。宏定义通常放在 .h 头文件中,配合 #include 引入,使设备树更易读、易维护。
2.5.1 宏定义示例
// dt-bindings/gpio/gpio.h
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
// dt-bindings/interrupt-controller/irq.h
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
// 在设备树中使用宏
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
&gpio0 {
led-red {
gpios = <&gpio2 4 GPIO_ACTIVE_LOW>;
interrupts = <IRQ_TYPE_EDGE_RISING>;
};
};
2.5.2 宏定义 vs 直接数值
| 对比项 | 使用宏定义 | 使用直接数值 |
|---|---|---|
| 可读性 | GPIO_ACTIVE_LOW 一目了然 |
1 需要查阅文档才能理解 |
| 可维护性 | 修改宏定义一处生效 | 需要逐个修改所有数值 |
| 跨平台兼容 | 头文件统一管理,不同 SoC 可复用 | 数值含义可能因 SoC 而异 |
| 编译检查 | 预处理器可检测宏未定义错误 | 数值错误只能在运行时发现 |
⚠️ 宏定义使用注意事项:
- 宏定义必须在
#include头文件之后才能使用,注意包含顺序。 - 不要重新定义内核头文件中已有的宏,避免冲突。
- 宏定义只支持整数常量,不支持字符串拼接或复杂表达式。
- 在
.dts文件中使用#define时,需确保 DTC 启用了预处理(默认启用)。
2.6 综合语法示例:完整节点结构
以下是一个综合了节点、属性、标签、包含与宏定义的完整设备树片段:
// am335x-boneblack.dts
/dts-v1/;
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
chosen {
stdout-path = &uart0;
};
leds {
pinctrl-names = "default";
pinctrl-0 = <&user_led_pins>;
led-red {
label = "beaglebone:red:usr0";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
led-green {
label = "beaglebone:green:usr1";
gpios = <&gpio1 22 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "mmc0";
};
};
};
&uart0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
};
&i2c0 {
status = "okay";
clock-frequency = <400000>;
eeprom@50 {
compatible = "atmel,24c32";
reg = <0x50>;
pagesize = <32>;
};
};
2.7 语法要素关系 SVG 图
2.8 本章小结
- 节点是设备树的基本单元,通过层级关系组织硬件拓扑。
- 属性以键值对形式描述硬件特征,
compatible和reg是最核心的属性。 - 标签提供节点引用机制,支持跨文件属性覆盖与追加。
- 包含实现设备树模块化,SoC 定义与板级定制分离。
- 宏定义提升代码可读性与可维护性,是设备树工程化的关键手段。
🔧 实践建议:在开始编写板级设备树前,先阅读对应 SoC 的
.dtsi 文件,熟悉已有的节点、标签和宏定义。遵循“包含 + 覆盖”的模式,避免直接修改 .dtsi 文件,保持板级定制的清晰与可追溯。