2. 设备树语法:节点、属性、标签、包含与宏
设备树(Device Tree,DT)是一种描述硬件拓扑与资源配置的树形数据结构,其语法由 节点(node)、属性(property)、标签(label)、包含(include) 与 宏(macro) 五个核心元素构成。理解这些语法单元是编写、调试与故障定位设备树的基础。
核心认知:设备树不是C语言代码,而是一种结构化数据描述语言。它通过节点表示硬件设备,通过属性描述设备参数,通过标签提供引用锚点,通过包含与宏实现模块化复用。
2.1 节点(node)—— 硬件拓扑的骨架
节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线、控制器或功能块。节点以 / 为根,形成树状层级结构。
/ {
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
serial@ff000000 {
compatible = "ns16550";
reg = <0xff000000 0x1000>;
interrupts = <0 33 4>;
};
};
| 节点元素 | 语法规则 | 说明 |
|---|---|---|
| 根节点 | / { ... }; |
设备树的唯一根,所有设备挂接在其下 |
| 普通节点 | node-name@unit-address { ... }; |
node-name 为通用名称,unit-address 为设备基地址(可选) |
| 子节点 | 在父节点 { } 内嵌套定义 |
表示总线与从设备、控制器与外设的层级关系 |
| 空节点 | node-name; |
仅声明引用,不包含属性和子节点 |
调试要点:节点名称中的
@unit-address 必须与 reg 属性的首地址一致,否则内核在解析时会因地址不匹配而跳过该节点,导致设备无法被驱动识别。
2.2 属性(property)—— 设备的参数清单
属性是附着在节点上的键值对,用于描述设备的硬件特征、配置参数和连接关系。属性值支持多种数据类型。
| 数据类型 | 语法示例 | 说明 |
|---|---|---|
| 空属性 | status = "disabled"; |
布尔标志,存在即为真 |
| 字符串 | compatible = "arm,cortex-a9"; |
双引号包裹,用于设备匹配 |
| 32位整数 | reg = <0x80000000 0x40000000>; |
尖括号包裹,多个值用空格分隔 |
| 64位整数 | reg = <0x00000000 0x80000000>; |
使用两个32位单元表示 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号包裹,十六进制字节 |
| 字符串列表 | compatible = "ti,am335x-uart", "ns16550"; |
逗号分隔多个字符串 |
| 混合值 | interrupts = <0 33 4>; |
可包含整数、&phandle 引用等 |
常见故障:
reg 属性中的地址与 #address-cells、#size-cells 不匹配,会导致内核解析出的地址空间错误。例如 #address-cells = <2> 时,reg 中的每个地址必须占用两个32位单元。
2.3 标签(label)—— 引用与覆盖的锚点
标签为节点提供一个全局唯一的引用名称,用于在设备树的其他位置通过 &label 语法引用该节点,实现属性覆盖或追加。
// 定义标签
uart0: serial@ff000000 {
compatible = "ns16550";
reg = <0xff000000 0x1000>;
interrupts = <0 33 4>;
};
// 在其他文件中通过标签引用并追加属性
&uart0 {
clock-frequency = <24000000>;
status = "okay";
};
| 标签操作 | 语法 | 效果 |
|---|---|---|
| 定义标签 | label: node-name@addr { ... }; |
为节点创建全局引用名 |
| 引用节点 | &label { ... }; |
通过标签定位节点,追加或覆盖属性 |
| 引用到属性值 | clocks = &uart0; |
在属性值中引用节点作为phandle |
调试技巧:使用
dtc -O dtb -o temp.dtb source.dts 编译后,再用 dtc -I dtb -O dts temp.dtb 反编译,可以查看标签引用是否被正确解析为 phandle 值。如果反编译结果中 &label 变成了数字 <0x00000001>,说明引用生效。
2.4 包含(include)—— 模块化与复用
设备树使用C预处理器(CPP)的 #include 指令实现文件包含,支持包含 .dtsi(设备树包含文件)、.h(头文件)等。这是实现SoC级复用、板级定制的关键机制。
// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include "am335x-boneblack-common.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
};
| 包含类型 | 文件后缀 | 用途 |
|---|---|---|
| SoC级包含 | .dtsi |
定义SoC内部IP核、总线、时钟等通用结构 |
| 板级包含 | .dtsi |
定义板级外设、GPIO复用、电源管理等 |
| 头文件包含 | .h |
引入宏定义、中断号、时钟索引等常量 |
最佳实践:将SoC通用部分放在
.dtsi 中,板级差异部分放在 .dts 中。通过 &label 覆盖 .dtsi 中的默认属性,避免直接修改公共文件。
2.5 宏(macro)—— 编译期常量与表达式
设备树源文件经过C预处理器(CPP)处理,因此支持 #define 宏定义、#ifdef 条件编译等预处理指令。宏常用于定义中断号、GPIO编号、时钟索引等硬件常量。
// am33xx.h
#define OMAP_INTC_BASE 0x48200000
#define OMAP_GPIO0_BASE 0x44E07000
#define OMAP_UART0_BASE 0x44E09000
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_LEVEL_HIGH 4
// am33xx.dtsi
#include "am33xx.h"
/ {
soc {
uart0: serial@44e09000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <OMAP_UART0_BASE 0x2000>;
interrupts = <IRQ_TYPE_LEVEL_HIGH 72 0>;
};
};
};
| 宏类型 | 示例 | 编译后效果 |
|---|---|---|
| 地址宏 | #define UART_BASE 0x44E09000 |
reg = <0x44E09000 0x2000>; |
| 中断宏 | #define IRQ_UART0 72 |
interrupts = <0 72 4>; |
| 条件编译 | #ifdef CONFIG_FEATURE_X |
根据内核配置决定是否包含某段设备树 |
| 宏表达式 | #define SIZE (0x1000 * 4) |
reg = <0x44E09000 0x4000>; |
调试陷阱:宏展开错误是设备树编译失败的常见原因。使用
dtc -E -o preprocessed.dts source.dts 可以查看预处理后的设备树源码,检查宏是否被正确展开。如果宏未定义,CPP会报错并终止编译。
2.6 语法综合示例与调试对照
以下是一个完整的设备树片段,综合展示了节点、属性、标签、包含与宏的协同使用,以及编译前后的对比。
// ========== soc.h ==========
#define UART0_BASE 0x44E09000
#define UART0_IRQ 72
#define GPIO0_BASE 0x44E07000
#define GPIO0_IRQ 96
// ========== am33xx.dtsi ==========
#include "soc.h"
/ {
#address-cells = <1>;
#size-cells = <1>;
ocp {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x44E00000 0x100000>;
uart0: serial@90000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <UART0_BASE 0x2000>;
interrupts = <UART0_IRQ>;
status = "disabled";
};
gpio0: gpio@70000 {
compatible = "ti,omap4-gpio";
reg = <GPIO0_BASE 0x1000>;
interrupts = <GPIO0_IRQ>;
status = "disabled";
};
};
};
// ========== board.dts ==========
/dts-v1/;
#include "am33xx.dtsi"
/ {
model = "Custom Board";
compatible = "custom,board", "ti,am33xx";
};
&uart0 {
status = "okay";
clock-frequency = <48000000>;
};
&gpio0 {
status = "okay";
};
| 语法元素 | 源文件位置 | 编译后效果 |
|---|---|---|
#include "soc.h" |
am33xx.dtsi |
UART0_BASE 展开为 0x44E09000 |
uart0: serial@90000 |
am33xx.dtsi |
节点路径为 /ocp/serial@90000,标签 uart0 可被引用 |
&uart0 { status = "okay"; }; |
board.dts |
在 /ocp/serial@90000 节点中追加 status = "okay" 属性 |
clock-frequency = <48000000>; |
board.dts |
追加新属性,不影响 .dtsi 中的原有属性 |
故障定位实战:如果UART驱动无法加载,执行以下调试步骤:
1.
2.
3. 确认
4. 检查
1.
dtc -E -o pre.dts board.dts 检查宏是否展开正确。2.
dtc -I dtb -O dts /sys/firmware/fdt 查看内核实际使用的设备树。3. 确认
status = "okay" 存在,且 compatible 字符串与驱动匹配。4. 检查
reg 地址是否与硬件手册一致,interrupts 号是否在中断控制器范围内。