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),并结合实际案例说明其工程应用。

/ (根节点) soc chosen memory@0 leds compatible = "simple-bus" #address-cells = <1> #size-cells = <1> bootargs = "console=ttyS0" stdout-path = &uart0 (引用示例) device_type = "memory" reg = <0x0 0x40000000> (值类型: 32位数组) label = "led0" gpios = <&gpio1 5 0> (标签+引用) #include 包含机制 #include "imx6ull.dtsi" → SoC级定义(CPU、中断控制器、GPIO控制器) #include "my-board.dtsi" → 板级定义(外设、引脚复用、电源) /dts-v1/; → 指定DTS版本,必须位于文件头部 根节点唯一

上图展示了设备树的核心语法要素:根节点“/”下挂载多个子节点,每个节点可包含属性(键值对),属性值支持字符串、32位整数数组、64位整数数组、二进制数据等类型。标签(如label = "led0")用于给节点起别名,引用(如&gpio1)用于在其它位置访问已定义的节点。包含文件(#include)则实现了SoC级与板级定义的分离,这是大型BSP工程中常用的组织方式。

4.1 节点(Node)与属性(Property)

核心原理:设备树的基本组成单元是节点,每个节点代表一个设备、总线、控制器或抽象功能块。节点名称通常采用“名称@地址”的格式,其中地址部分用于区分同类型设备,例如uart0@02020000表示基地址为0x02020000的UART0控制器。节点内部通过属性描述其硬件特征,属性是“键值对”,键为字符串,值可以是空、字符串、整数数组或二进制数据。

判断方法:在编写或阅读DTS时,应遵循以下步骤:

  1. 确定设备属于哪个总线(如platform、i2c、spi),从而决定其父节点位置。
  2. 为设备节点命名,格式为“设备类型@寄存器基地址”,地址必须与reg属性中的首地址一致。
  3. 添加compatible属性,格式为“厂商,型号”,内核通过该字符串匹配驱动。
  4. 添加reg属性,描述设备占用的寄存器地址范围或片选编号。
  5. 根据设备特性添加中断、时钟、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结尾的ASCIIcompatible、name
32位整数<0x021E8000>4字节大端reg地址、中断号
64位整数<0x0 0x40000000>两个32位cell大内存基址
字节数组[0x00 0x01 0x02]原始字节MAC地址、校准数据
节点引用&gpio132位phandleGPIO、时钟、中断控制器
字符串列表"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中的默认配置。

判断方法:引用有两种典型用法:

  1. 属性追加/覆盖:在板级DTS中,通过&uart3 { ... };向已有节点添加新属性或修改现有属性。
  2. 作为属性值:在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无相关错误。

分析过程:

  1. 检查I2C2控制器节点是否使能:&i2c2 { status = "okay"; };
  2. 确认引脚复用是否正确:需配置I2C2_SCL和I2C2_SDA的pinctrl。
  3. 添加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 20x48处显示UU或设备号
4. 验证驱动绑定ls -l /sys/bus/i2c/devices/2-0048/driver指向tmp102驱动

通过上述完整链路,可以看到设备树语法不仅是文本编写,更与内核驱动模型、硬件电气特性紧密关联。掌握节点、属性、值类型、标签、引用和包含文件的正确用法,是进行嵌入式Linux驱动开发的基础能力。