2. 设备树语法详解:节点、属性、标签、包含与宏定义

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,它使用树形节点和键值对属性来描述系统中所有可寻址设备。本章将深入解析设备树源文件(.dts / .dtsi)的核心语法要素,包括节点、属性、标签、包含机制以及宏定义的使用技巧。掌握这些语法是编写正确、可维护的设备树文件的基础。

2.1 节点(Node)

节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线或功能块。节点通过路径(path)在树中唯一标识,路径由根节点(/)开始,使用斜杠分隔。

2.1.1 节点定义格式

node-name@unit-address {
    property-name = <value>;
    child-node {
        ...
    };
};
  • node-name:节点名称,通常反映设备类型(如 cpu、memory、uart)。
  • unit-address:可选,表示设备在总线上的地址(如寄存器基址、片选号),用于区分同名节点。
  • 属性:节点内部定义的键值对,描述设备特性。
  • 子节点:节点可以嵌套,形成层次结构。

2.1.2 节点命名规则

规则 说明 示例
通用名称 使用标准设备类别名,如 serial、gpio、spi serial@ff000000
地址后缀 当同一总线有多个同类设备时,必须加地址区分 i2c@0、i2c@1
特殊节点 / 为根节点,aliases、chosen 为系统节点 / { ... };
最佳实践:节点名称应尽量使用 device-type@address 格式,避免使用过于具体的芯片型号作为节点名,以增强设备树的可移植性。

2.2 属性(Property)

属性是节点中存储设备信息的键值对。设备树规范定义了多种标准属性,同时也允许厂商自定义属性。

2.2.1 常用标准属性

属性名 值类型 用途
compatible 字符串列表 设备驱动匹配的关键标识,格式为 "厂商,型号"
reg 32位整数对列表 描述设备地址空间(基址 + 长度)
interrupts 32位整数列表 描述中断号及触发类型
clocks phandle + 整数 引用时钟控制器及输出索引
status 字符串 设备状态:"okay"、"disabled"、"reserved"
#address-cells 32位整数 子节点 reg 属性中地址字段的单元数
#size-cells 32位整数 子节点 reg 属性中长度字段的单元数

2.2.2 属性值编码格式

// 字符串
compatible = "vendor,device";

// 32位整数(一个或多个)
reg = <0x10000000 0x1000>;

// 64位整数(使用两个32位单元)
reg = <0x00000000 0x80000000 0x00000000 0x10000000>;

// 字节数组
local-mac-address = [00 11 22 33 44 55];

// 布尔属性(存在即为真)
interrupt-controller;

// 混合类型
interrupts = <0 59 4>;

2.3 标签(Label)

标签是附加在节点或属性上的符号名称,用于在设备树的其他位置引用该节点。标签在编译时被解析为 phandle(指针句柄),是设备树实现节点间引用的核心机制。

2.3.1 标签定义与引用

// 定义标签
uart0: serial@ff000000 {
    compatible = "vendor,uart";
    reg = <0xff000000 0x1000>;
    interrupts = <0 33 4>;
};

// 在其他节点中引用标签
amba-bus {
    uart-phandle = <&uart0>;  // 通过 & 引用标签
};

2.3.2 标签的典型用途

  • 驱动绑定:在 chosen 节点或板级文件中引用外设节点。
  • 时钟/中断/GPIO 引用:通过 &label 获取控制器节点句柄。
  • 覆盖(overlay):在设备树覆盖文件中通过标签定位要修改的节点。
注意:标签名在全局范围内必须唯一。不同 .dtsi 文件中不能定义同名标签,否则会导致编译冲突。

2.4 包含(Include)

设备树源文件支持 #include 预处理指令,用于将公共定义、SoC 描述文件或头文件包含到当前文件中。这是实现设备树模块化和复用的关键机制。

2.4.1 包含机制的使用

// 包含 SoC 公共定义
#include "soc.dtsi"

// 包含头文件(用于宏定义)
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

// 包含板级特定配置
#include "board-common.dtsi"

2.4.2 包含文件的层次结构

文件类型 扩展名 内容 复用范围
SoC 描述文件 .dtsi CPU、内存控制器、外设总线、通用 IP 核 同一 SoC 系列所有板卡
板级公共文件 .dtsi 板级外设(如以太网 PHY、音频编解码器) 同一参考设计的不同变体
板级最终文件 .dts 板级特有配置、GPIO 复用、设备使能 单一具体板卡
头文件 .h 宏定义、常量、位掩码 所有设备树文件

2.5 宏定义

设备树源文件通过 C 预处理器支持宏定义,这极大地提高了代码的可读性和可维护性。宏定义通常放在 .h 头文件中,通过 #include 引入。

2.5.1 常用宏定义示例

// gpio.h
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW  1

// 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

// 在 dts 中使用宏
gpio-keys {
    compatible = "gpio-keys";
    button@0 {
        label = "Power";
        gpios = <&gpio0 3 GPIO_ACTIVE_LOW>;
        interrupts = <IRQ_TYPE_EDGE_FALLING>;
    };
};

2.5.2 宏定义的优势

  • 消除魔法数字:用有意义的名称替代裸数字,如 GPIO_ACTIVE_LOW 代替 0。
  • 统一管理:所有板卡共享同一套宏定义,避免重复定义和笔误。
  • 与内核驱动对齐:设备树中的宏定义与内核头文件中的定义保持一致,确保匹配。
实战技巧:在编写设备树时,优先使用 dt-bindings/ 目录下内核提供的标准宏定义。对于厂商私有属性,可以在板级 .h 文件中自定义宏,但需在驱动中同步维护。

2.6 设备树语法结构总览

下图展示了设备树源文件的完整语法结构,从根节点到子节点、属性、标签以及包含关系的整体视图。

设备树源文件语法结构总览 / (根节点) #address-cells = <1> #size-cells = <1> model = "Board Name" compatible = "vendor,board" cpu@0 (标签: cpu0) compatible = "arm,cortex-a7" reg = <0x0> serial@ff000000 (标签: uart0) compatible = "vendor,uart" reg = <0xff000000 0x1000> interrupts = <0 33 4> gpio@ff020000 (标签: gpio0) compatible = "vendor,gpio" reg = <0xff020000 0x1000> gpio-controller; #include 机制 soc.dtsi → 定义 SoC 级节点和标签 | board.dts → 通过 &label 引用并填充板级属性 宏定义(#define) dt-bindings/gpio/gpio.h → GPIO_ACTIVE_HIGH / GPIO_ACTIVE_LOW | 消除魔法数字,增强可读性

2.7 节点引用与覆盖

设备树支持通过标签引用节点,并在后续文件中覆盖或追加属性。这种机制使得 SoC 公共描述与板级特定配置可以分离,极大提高了复用性。

2.7.1 引用与覆盖语法

// soc.dtsi - SoC 公共定义
uart0: serial@ff000000 {
    compatible = "vendor,uart";
    reg = <0xff000000 0x1000>;
    status = "disabled";  // 默认关闭
};

// board.dts - 板级文件
#include "soc.dtsi"

&uart0 {
    status = "okay";      // 覆盖 status 属性
    clock-frequency = <24000000>;  // 追加新属性
};

2.7.2 覆盖规则

操作 语法 效果
覆盖属性值 &label { prop = <new>; }; 用新值替换原属性值
追加属性 &label { new-prop = <val>; }; 在节点中增加新属性
追加子节点 &label { child { ... }; }; 在节点下增加子节点
删除属性 /delete-property/ prop; 从节点中移除指定属性
删除节点 /delete-node/ &label; 从树中移除整个节点
设计模式:推荐在 .dtsi 文件中将所有外设节点的 status 默认设为 "disabled",然后在板级 .dts 文件中通过 &label 引用并设置为 "okay"。这样可以避免未使用的外设占用资源。

2.8 语法检查与编译

设备树源文件需要通过 dtc(Device Tree Compiler)编译为二进制 .dtb 文件。编译过程中会进行语法检查、标签解析和宏展开。

2.8.1 编译命令

# 编译 dts 为 dtb
dtc -I dts -O dtb -o board.dtb board.dts

# 反编译 dtb 为 dts(用于调试)
dtc -I dtb -O dts -o board_decompiled.dts board.dtb

# 检查语法错误
dtc -I dts -O dtb -o /dev/null board.dts 2>&1

2.8.2 常见编译错误

  • 标签重复:多个文件定义了同名标签。
  • 属性值类型不匹配:如字符串属性被赋值为整数。
  • 地址单元不匹配:子节点 reg 的单元数与父节点 #address-cells / #size-cells 不一致。
  • phandle 引用无效:&label 引用了不存在的标签。
调试建议:使用 dtc -O dts -o /dev/stdout board.dtb 可以查看编译后的设备树内容,确认属性值是否被正确解析。如果内核启动时设备树解析失败,可以通过 CONFIG_OF_UNITTEST 启用设备树单元测试。

2.9 本章小结

设备树语法是嵌入式 Linux 驱动开发的基础技能。节点定义硬件拓扑,属性描述设备特性,标签实现跨文件引用,包含机制促进代码复用,宏定义提升可读性。掌握这些语法要素后,开发者可以编写出结构清晰、易于维护的设备树文件,为后续的驱动开发和内核集成打下坚实基础。