3. 设备树基础语法:节点、属性、标签、包含文件(#include)、节点引用与覆盖

设备树(Device Tree,DT)是一种描述硬件资源的数据结构,它使用树形层次结构将系统中的设备、总线、中断、时钟、GPIO 等信息以节点和属性的形式组织起来。对于 SPI 设备配置而言,掌握设备树的基础语法是编写正确设备树文件的前提。本章将系统讲解设备树的五大核心语法要素:节点、属性、标签、包含文件以及节点引用与覆盖。

学习提示:设备树语法与 C 语言结构体类似,但更强调层次化和可复用性。建议读者在阅读本章时,对照实际开发板(如 i.MX6ULL、STM32MP1)的 .dts 文件进行验证。

3.1 节点(Node):设备树的骨架

节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线、控制器或功能模块。节点通过路径(path)唯一标识,路径从根节点(/)开始,类似文件系统的目录结构。

3.1.1 节点定义语法

// 节点定义格式
[label:] node-name[@unit-address] {
    [property-definitions]
    [child-node-definitions]
};
  • node-name:节点名称,描述设备类型,如 spi、gpio、serial。
  • @unit-address:可选,通常为设备在总线上的地址(如 SPI 片选号、寄存器基地址)。
  • label:可选,节点标签,用于在其他位置引用该节点。
  • 花括号 {} 和分号 ;:节点体必须用花括号包裹,并以分号结束。

3.1.2 根节点与子节点

每个设备树文件有且只有一个根节点(/),所有其他节点都是根节点的后代。SPI 控制器节点通常挂接在 SoC 内部总线节点(如 soc、ahb、apb)下,而 SPI 外设节点则作为 SPI 控制器的子节点。

/ {
    model = "Embedded SPI Demo Board";
    compatible = "vendor,spi-board";

    soc {
        spi@ff010000 {
            compatible = "vendor,spi-controller";
            reg = <0xff010000 0x1000>;
            #address-cells = <1>;
            #size-cells = <0>;

            // SPI 外设子节点
            spi-device@0 {
                compatible = "vendor,spi-sensor";
                reg = <0>;  // 片选号 0
                spi-max-frequency = <10000000>;
            };
        };
    };
};
关键点:SPI 外设节点必须作为 SPI 控制器节点的子节点,且通过 reg 属性指定片选号(CS)。控制器通过 #address-cells 和 #size-cells 定义子节点地址的编码格式。
设备树节点层次结构(SPI 场景) 根节点 / soc 节点 spi@ff010000 compatible, reg, #address-cells spi-device@0 reg=<0>, spi-max-frequency spi-device@1 reg=<1>, spi-max-frequency SPI 外设节点必须作为 SPI 控制器节点的子节点,通过 reg 属性指定片选号

3.2 属性(Property):描述节点的特征

属性是附着在节点上的键值对,用于描述设备的硬件特性、配置参数和连接关系。属性值可以是字符串、整数、字节数组、字符串列表或二进制数据。

3.2.1 常用属性类型

属性类型 语法格式 示例 说明
字符串 property = "string"; compatible = "vendor,spi-device"; 设备兼容性标识,驱动匹配的关键
32 位整数 property = <value>; spi-max-frequency = <5000000>; SPI 最大时钟频率,单位 Hz
64 位整数 property = <0x00000000 0xvalue>; reg = <0x00000000 0xff010000>; 地址范围,高 32 位 + 低 32 位
字符串列表 property = "str1", "str2"; compatible = "vendor,spi-a", "vendor,spi-b"; 多个兼容驱动,按优先级排列
字节数组 property = [hex bytes]; local-mac-address = [00 11 22 33 44 55]; MAC 地址、UUID 等二进制数据
布尔值 property;(无值) spi-cpha; spi-cpol; 存在即表示 true,用于使能特性

3.2.2 SPI 设备关键属性

在配置 SPI 外设节点时,以下属性是驱动正确工作的基础:

  • compatible:驱动匹配字符串,格式为 "厂商,设备型号",如 "maxim,ds1307"。
  • reg:SPI 片选号(CS),对于 SPI 总线,#address-cells = <1> 时,reg = <0> 表示 CS0。
  • spi-max-frequency:设备支持的最大 SPI 时钟频率,单位 Hz。
  • spi-cpha / spi-cpol:布尔属性,配置 SPI 模式(CPHA/CPOL)。
  • spi-cs-high:布尔属性,片选信号高电平有效(默认低电平)。
  • interrupts:中断号,需配合中断控制器使用。
// 完整的 SPI 外设节点属性示例
&spi1 {
    status = "okay";

    temperature-sensor@0 {
        compatible = "ti,lm70";
        reg = <0>;                    // 片选 CS0
        spi-max-frequency = <10000000>; // 10 MHz
        spi-cpha;                      // CPHA = 1
        spi-cpol;                      // CPOL = 1
        // 等效于 SPI 模式 3
        interrupts = <5 2>;           // 中断号 5,触发类型 2(下降沿)
    };
};
常见错误:忘记设置 spi-max-frequency 或设置值超过控制器/设备上限,会导致 SPI 通信时序异常。建议根据设备数据手册设置安全值,通常从较低频率开始调试。

3.3 标签(Label):节点的别名

标签是节点的别名,用于在设备树的其他位置快速引用该节点。标签定义在节点名称之前,使用冒号分隔。标签的主要用途包括:

  • 简化引用:避免书写冗长的节点路径。
  • 节点覆盖:通过标签引用节点并修改其属性。
  • 驱动绑定:驱动代码中通过标签查找节点。
// 定义标签
spi1: spi@ff010000 {
    compatible = "vendor,spi-controller";
    reg = <0xff010000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;
};

// 在其他位置通过 &spi1 引用
&spi1 {
    status = "okay";
    pinctrl-0 = <&pinctrl_spi1>;
};

标签的命名规范:

  • 使用小写字母、数字和下划线。
  • 避免使用关键字(如 compatible、reg)。
  • 建议使用设备功能缩写,如 spi1、i2c2、uart3。

3.4 包含文件(#include):复用与模块化

设备树支持 C 语言预处理器指令 #include,用于包含头文件(.h)或其他设备树源文件(.dtsi)。这是实现设备树模块化和复用性的核心机制。

3.4.1 包含文件的作用

文件类型 扩展名 用途 示例
设备树头文件 .h 定义宏、常量、GPIO 编号、时钟 ID #include "imx6ull-pinfunc.h"
设备树包含文件 .dtsi SoC 公共设备树,包含所有外设控制器节点 #include "imx6ull.dtsi"
板级设备树 .dts 具体板卡的设备树,包含 .dtsi 并覆盖配置 #include "my-board.dts"

3.4.2 包含文件实战

// my-board.dts
#include "soc-common.dtsi"       // 包含 SoC 公共定义
#include "pinfuncs.h"            // 包含引脚复用宏

/ {
    model = "My SPI Board";
    compatible = "vendor,my-board";
};

// 引用并覆盖 soc-common.dtsi 中定义的 spi1 节点
&spi1 {
    status = "okay";
    pinctrl-0 = <&pinctrl_spi1>;

    adc@0 {
        compatible = "ti,ads1256";
        reg = <0>;
        spi-max-frequency = <19200000>;
        // 使用头文件中定义的宏
        clocks = <&clks IMX6UL_CLK_SPI1>;
    };
};
最佳实践:将 SoC 公共定义放在 .dtsi 文件中,板级差异放在 .dts 文件中。头文件(.h)用于定义宏常量,避免在设备树中硬编码魔法数字。
设备树包含文件与编译流程 my-board.dts 板级设备树 soc-common.dtsi SoC 公共定义 pinfuncs.h 引脚宏定义 C 预处理器 展开 #include 替换宏定义 生成完整 .dts 合并节点覆盖 DTC 编译器 语法检查 生成二进制 .dtb 添加 phandle 生成符号表 my-board.dtb

3.5 节点引用与覆盖(Reference & Overlay)

节点引用与覆盖是设备树实现“增量修改”的核心机制。通过标签引用一个已存在的节点,可以在不重新定义整个节点的情况下,添加或修改其属性和子节点。

3.5.1 引用语法

// 引用格式:<label> { ... };
&spi1 {
    status = "okay";
    // 添加新属性或覆盖已有属性
    pinctrl-0 = <&pinctrl_spi1>;
};

3.5.2 覆盖规则

操作 语法 效果
添加属性 &node { new-prop = <value>; }; 节点新增属性,不影响已有属性
覆盖属性 &node { existing-prop = <new-value>; }; 用新值替换已有属性的值
添加子节点 &node { child@addr { ... }; }; 在节点下新增子节点
删除属性 &node { /delete-property/ prop-name; }; 删除指定属性(DTC 支持)
删除子节点 &node { /delete-node/ child@addr; }; 删除指定子节点(DTC 支持)

3.5.3 覆盖实战:SPI 设备配置

// soc-common.dtsi 中定义 SPI 控制器骨架
spi1: spi@ff010000 {
    compatible = "vendor,spi-controller";
    reg = <0xff010000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";  // 默认关闭
};

// my-board.dts 中覆盖配置
#include "soc-common.dtsi"

&spi1 {
    status = "okay";                    // 覆盖为启用
    pinctrl-0 = <&pinctrl_spi1>;        // 添加引脚配置
    cs-gpios = <&gpio1 0 GPIO_ACTIVE_LOW>; // 添加片选 GPIO

    // 添加 SPI 外设子节点
    spi-nor@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
        m25p,fast-read;
    };

    spi-touch@1 {
        compatible = "vendor,touch-controller";
        reg = <1>;
        spi-max-frequency = <10000000>;
        interrupt-parent = <&gpio2>;
        interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
    };
};
覆盖机制的优势:SoC 厂商提供 .dtsi 文件定义所有外设的默认配置,板卡厂商只需通过引用覆盖修改少量属性(如 status、pinctrl、SPI 外设),无需复制整个节点定义,大大减少了代码冗余和出错概率。

3.6 综合示例:完整的 SPI 设备树配置

以下是一个完整的板级设备树文件,综合运用了本章所学的所有语法要素:

// my-spi-board.dts
#include "soc-imx6ull.dtsi"       // SoC 公共定义
#include "imx6ull-pinfunc.h"      // 引脚复用宏

/ {
    model = "My SPI Sensor Board";
    compatible = "vendor,my-spi-board";

    chosen {
        stdout-path = &uart1;
    };

    // 板级 GPIO 按键
    gpio-keys {
        compatible = "gpio-keys";
        button@0 {
            label = "USER-BUTTON";
            gpios = &gpio5 1 GPIO_ACTIVE_LOW;
            linux,code = <KEY_ENTER>;
        };
    };
};

// 引用并覆盖 SPI 控制器
&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_spi1>;
    cs-gpios = <&gpio4 11 GPIO_ACTIVE_LOW>;

    // SPI 温度传感器
    temp-sensor@0 {
        compatible = "maxim,ds18b20-spi";
        reg = <0>;
        spi-max-frequency = <4000000>;
        spi-cpha;
        spi-cpol;
        resolution = <12>;
    };

    // SPI ADC
    adc@1 {
        compatible = "ti,ads1256";
        reg = <1>;
        spi-max-frequency = <19200000>;
        interrupts = <6 IRQ_TYPE_EDGE_RISING>;
        vref-supply = <®_vref>;
    };
};

// 引脚复用配置
&iomuxc {
    pinctrl_spi1: spi1grp {
        fsl,pins = <
            MX6UL_PAD_CSI_DATA00__GPIO4_IO11  0x10b0  // CS
            MX6UL_PAD_CSI_DATA01__ECSPI1_MISO 0x10b0  // MISO
            MX6UL_PAD_CSI_DATA02__ECSPI1_MOSI 0x10b0  // MOSI
            MX6UL_PAD_CSI_DATA03__ECSPI1_SCLK 0x10b0  // SCLK
        >;
    };
};

// 电源节点
®_vref {
    regulator-name = "vref-3v3";
    regulator-min-microvolt = <3300000>;
    regulator-max-microvolt = <3300000>;
};