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 属性的首地址一致,否则内核在解析时会因地址不匹配而跳过该节点,导致设备无法被驱动识别。
设备树节点层级结构示例 根节点 / amba@0 (总线) serial@ff000000 gpio@ff001000 compatible = "ns16550" reg = <0xff000000 0x1000> 每个节点可包含多个属性(property),属性描述设备的寄存器地址、中断号、时钟频率等参数

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会报错并终止编译。
设备树编译流程:包含与宏的处理 board.dts (板级描述) soc.dtsi (SoC通用描述) defines.h (宏定义头文件) C预处理器 (CPP) 展开 #include 展开 #define DTC编译器 (Device Tree Compiler) 解析节点/属性 生成 .dtb 二进制 board.dtb 调试命令:dtc -E -o pre.dts board.dts # 查看预处理后的设备树源码 dtc -I dtb -O dts -o decompiled.dts board.dtb # 反编译检查最终结果

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. dtc -E -o pre.dts board.dts 检查宏是否展开正确。
2. dtc -I dtb -O dts /sys/firmware/fdt 查看内核实际使用的设备树。
3. 确认 status = "okay" 存在,且 compatible 字符串与驱动匹配。
4. 检查 reg 地址是否与硬件手册一致,interrupts 号是否在中断控制器范围内。