4、设备树语法详解:节点、属性、值类型、标签、引用、包含文件等基本语法
设备树(Device Tree,DT)是嵌入式Linux系统中描述硬件拓扑、资源分布与驱动绑定关系的核心数据结构。它并非可执行代码,而是一种树形文本描述语言,由DTS(Device Tree Source)源文件编写,经DTC(Device Tree Compiler)编译为DTB(Device Tree Blob)二进制,最终由内核在启动阶段解析并生成platform_device、i2c_client、spi_device等设备模型。理解设备树语法,是进行内核驱动配置、板级适配与BSP移植的前提。本章将系统拆解设备树的基本语法单元:节点(node)、属性(property)、值类型(value type)、标签(label)、引用(&)与包含文件(#include),并结合实际案例说明其工程应用。
上图展示了设备树的核心语法要素:根节点“/”下挂载多个子节点,每个节点可包含属性(键值对),属性值支持字符串、32位整数数组、64位整数数组、二进制数据等类型。标签(如label = "led0")用于给节点起别名,引用(如&gpio1)用于在其它位置访问已定义的节点。包含文件(#include)则实现了SoC级与板级定义的分离,这是大型BSP工程中常用的组织方式。
4.1 节点(Node)与属性(Property)
核心原理:设备树的基本组成单元是节点,每个节点代表一个设备、总线、控制器或抽象功能块。节点名称通常采用“名称@地址”的格式,其中地址部分用于区分同类型设备,例如uart0@02020000表示基地址为0x02020000的UART0控制器。节点内部通过属性描述其硬件特征,属性是“键值对”,键为字符串,值可以是空、字符串、整数数组或二进制数据。
判断方法:在编写或阅读DTS时,应遵循以下步骤:
- 确定设备属于哪个总线(如platform、i2c、spi),从而决定其父节点位置。
- 为设备节点命名,格式为“设备类型@寄存器基地址”,地址必须与reg属性中的首地址一致。
- 添加
compatible属性,格式为“厂商,型号”,内核通过该字符串匹配驱动。 - 添加
reg属性,描述设备占用的寄存器地址范围或片选编号。 - 根据设备特性添加中断、时钟、GPIO、电源等资源属性。
案例拆解:以i.MX6ULL平台的UART3节点为例,其DTS定义如下:
uart3: serial@021E8000 {
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x021E8000 0x4000>;
interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART3_IPG>,
<&clks IMX6UL_CLK_UART3_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
dma-names = "rx", "tx";
status = "okay";
};
该节点中,compatible提供了三级兼容字符串,内核依次尝试匹配;reg定义了寄存器基址和长度;interrupts使用GIC宏定义中断号;clocks和clock-names关联时钟框架;dmas和dma-names配置DMA通道。这些属性共同构成了驱动所需的完整硬件资源描述。
常见误区:初学者常将compatible写成单一字符串,导致驱动匹配失败。正确做法是提供从具体到通用的多级兼容性,例如“fsl,imx6ul-uart”优先匹配专用驱动,“fsl,imx21-uart”作为兜底。另外,reg中的地址必须与节点名称中的地址一致,否则DTC编译会报错。
4.2 值类型(Value Types)与编码规则
核心原理:设备树属性值支持四种基本类型:字符串(string)、32位无符号整数(cell)、64位整数(由两个cell组成)、二进制数据(byte array)。此外还有<...>表示的整数数组、[...]表示的字节序列、&label表示的节点引用(编译为32位句柄)。理解这些类型的编码规则,是正确解析设备树的关键。
| 值类型 | DTS写法 | DTB编码 | 典型用途 |
|---|---|---|---|
| 字符串 | "fsl,imx6ul-uart" | 以\0结尾的ASCII | compatible、name |
| 32位整数 | <0x021E8000> | 4字节大端 | reg地址、中断号 |
| 64位整数 | <0x0 0x40000000> | 两个32位cell | 大内存基址 |
| 字节数组 | [0x00 0x01 0x02] | 原始字节 | MAC地址、校准数据 |
| 节点引用 | &gpio1 | 32位phandle | GPIO、时钟、中断控制器 |
| 字符串列表 | "a", "b", "c" | 多个\0结尾字符串 | clock-names、dma-names |
操作流程:在编写属性值时,需注意以下规则:
- 整数默认按32位处理,超过32位的值需拆分为两个cell,并配合
#address-cells和#size-cells解释。 - 字符串列表用逗号分隔,编译后连续存放,以\0分隔。
- 节点引用
&label在编译时自动替换为对应节点的phandle值。 - 空属性(如
status = "disabled")表示存在但值为空,常用于布尔标志。
实战场景:在配置DDR控制器时,reg = <0x0 0x80000000 0x0 0x40000000>表示基址0x80000000、大小1GB。由于地址和大小都超过32位,必须使用两个cell表示一个值,同时父节点需设置#address-cells = <2>和#size-cells = <2>。若忘记设置,DTC会按默认值1解析,导致地址错误。
4.3 标签(Label)与引用(Reference)
核心原理:标签是节点的别名,用于在DTS文件的其他位置引用该节点。标签定义在节点名称之前,格式为“标签名: 节点名@地址”。引用通过&标签名实现,可用于追加属性、修改属性值或作为属性值(如GPIO句柄)。引用机制是设备树实现“增量修改”的基础,允许板级DTS覆盖SoC级DTSI中的默认配置。
判断方法:引用有两种典型用法:
- 属性追加/覆盖:在板级DTS中,通过
&uart3 { ... };向已有节点添加新属性或修改现有属性。 - 作为属性值:在
gpios = <&gpio1 5 GPIO_ACTIVE_LOW>中,&gpio1被编译为GPIO控制器的phandle,驱动通过该句柄找到对应的gpio_chip。
案例拆解:假设SoC级DTSI中定义了UART3节点,但默认状态为status = "disabled"。板级DTS中启用该串口并修改波特率:
// 文件: imx6ull.dtsi (SoC级)
uart3: serial@021E8000 {
compatible = "fsl,imx6ul-uart";
reg = <0x021E8000 0x4000>;
status = "disabled";
};
// 文件: my-board.dts (板级)
#include "imx6ull.dtsi"
&uart3 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart3>;
};
编译时,DTC将两个定义合并,最终节点包含所有属性。若板级DTS中未引用&uart3,则节点保持disabled状态,驱动不会注册。这种机制使得同一SoC可以灵活适配多种板卡。
常见误区:标签名不能与节点名重复,且引用必须在标签定义之后(或通过头文件包含)。若在DTSI中定义标签,板级DTS通过#include引入后即可引用。另外,&label只能用于节点引用,不能用于属性引用。
4.4 包含文件(#include)与DTSI组织
核心原理:设备树源文件支持C预处理器指令,包括#include、#define、#ifdef等。通常将SoC级公共定义放在.dtsi文件中,板级差异放在.dts文件中,通过#include组合。这种分层设计极大提高了代码复用率,避免了重复定义。
操作流程:典型的文件组织方式如下:
// imx6ull.dtsi (SoC级)
#include "skeleton.dtsi"
#include "imx6ull-pinfunc.h"
/ {
compatible = "fsl,imx6ull";
soc {
#address-cells = <1>;
#size-cells = <1>;
gpio1: gpio@0209C000 {
compatible = "fsl,imx6ul-gpio";
reg = <0x0209C000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>;
};
};
};
// my-board.dts (板级)
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "My i.MX6ULL Board";
compatible = "my,imx6ull-board", "fsl,imx6ull";
leds {
compatible = "gpio-leds";
led0: led-0 {
label = "heartbeat";
gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
};
};
};
实战场景:当需要支持多款板卡时,可创建my-board-a.dts和my-board-b.dts,两者都包含imx6ull.dtsi,但各自定义不同的板级外设。编译时分别生成对应的DTB,内核启动时根据DTB中的compatible字符串匹配板级驱动。
经验总结:建议将引脚复用(pinctrl)、时钟配置、电源管理等通用定义放在DTSI中,将具体外设的使能、参数调整放在DTS中。同时,善用#define定义宏常量(如GPIO编号、中断触发方式),提高可读性。但需注意,宏定义仅在预处理阶段生效,不会出现在DTB中。
风险提示:过度使用#include可能导致依赖关系混乱。建议遵循“单向依赖”原则:DTS包含DTSI,DTSI不反向包含DTS。若多个DTSI之间存在共享定义,可提取公共部分到更底层的DTSI中,避免循环包含。
4.5 综合案例:从DTS到驱动匹配的完整链路
案例背景:某嵌入式项目需要在一颗i.MX6ULL处理器上扩展一个I2C接口的温度传感器(型号TMP102),并要求通过设备树配置驱动。硬件连接为:TMP102挂载在I2C2总线上,地址0x48,中断引脚连接到GPIO1_IO14。
初始现象:内核启动后,i2c-dev接口无法检测到设备,dmesg无相关错误。
分析过程:
- 检查I2C2控制器节点是否使能:
&i2c2 { status = "okay"; }; - 确认引脚复用是否正确:需配置I2C2_SCL和I2C2_SDA的pinctrl。
- 添加TMP102子节点,指定compatible、reg、interrupts属性。
实际操作:
// 板级DTS追加内容
&i2c2 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
status = "okay";
tmp102: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
};
};
// 引脚复用定义
&iomuxc {
pinctrl_i2c2: i2c2grp {
fsl,pins = <
MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
>;
};
};
结果对比:修改后重新编译DTB,内核启动后i2cdetect -y 2可检测到0x48设备,dmesg显示tmp102 2-0048: Temperature sensor registered。驱动成功匹配的关键在于:compatible字符串与内核驱动(drivers/hwmon/tmp102.c)中的of_match_table一致,且中断资源正确解析。
风险与代价:若引脚复用配置错误,I2C总线无法通信;若中断号或触发方式错误,可能导致中断风暴。建议在调试时先屏蔽中断属性,确认I2C通信正常后再逐步添加。
复盘经验:设备树调试应遵循“由底向上”原则:先确认总线控制器使能,再验证引脚复用,最后检查设备子节点。使用dtc -I dtb -O dts反编译DTB可验证编译结果是否符合预期。
| 调试步骤 | 命令/方法 | 预期结果 |
|---|---|---|
| 1. 检查DTB是否包含节点 | dtc -I dtb -O dts my-board.dtb | grep tmp102 | 显示tmp102节点定义 |
| 2. 确认I2C控制器状态 | cat /sys/bus/i2c/devices/i2c-2/name | 输出"i2c"或控制器名称 |
| 3. 扫描总线设备 | i2cdetect -y 2 | 0x48处显示UU或设备号 |
| 4. 验证驱动绑定 | ls -l /sys/bus/i2c/devices/2-0048/driver | 指向tmp102驱动 |
通过上述完整链路,可以看到设备树语法不仅是文本编写,更与内核驱动模型、硬件电气特性紧密关联。掌握节点、属性、值类型、标签、引用和包含文件的正确用法,是进行嵌入式Linux驱动开发的基础能力。