3、Device Tree源文件(DTS)语法:文件结构、注释、宏定义、include机制、标签(label)与引用(&)的用法

Device Tree Source(DTS)是描述硬件拓扑与资源配置的文本源文件,经过 Device Tree Compiler(DTC)编译后生成二进制的 DTB(Device Tree Blob),由 Bootloader 传递给内核。DTS 的语法看似简单,但实际工程中,文件组织方式、宏定义的使用、include 的层级关系以及 label 与引用的配合,直接决定了设备树的可维护性、可复用性和可读性。本章将系统拆解 DTS 的语法要素,并结合多板卡复用、外设节点覆盖等真实场景,帮助读者建立一套规范的 DTS 编写与维护方法论。

DTS 文件组织与引用机制总览 根节点 "/" model / compatible soc 节点 cpu / i2c / uart pinctrl 节点 引脚复用配置 chosen 节点 bootargs include 机制 #include "soc.dtsi" #include "board-common.dtsi" #include "board-rev2.dtsi" 宏定义与头文件 #define GPIO_ACTIVE_LOW 1 #include "dt-bindings/gpio/gpio.h" #include "dt-bindings/clock/imx8mm-clock.h" label 与 & 引用 i2c1: i2c@30a20000 { ... } &i2c1 { status = "okay"; } &i2c1 { clock-frequency = <400000>; } DTS + DTSI 预处理 (cpp) DTC 编译 dtc -I dts -O dtb DTB 二进制 Bootloader 加载 关键语法要点 • 节点命名:node-name@unit-address,如 i2c@30a20000 • 属性类型:字符串、32位整数数组、字符串列表、二进制数据 • 覆盖规则:&label 可追加属性或子节点,同名属性后者覆盖前者

3.1 DTS 文件结构与节点层次

核心原理:从根节点到叶子节点的树形描述

DTS 文件采用树形结构描述硬件,唯一的根节点用 / 表示,所有设备节点均挂在根节点之下。每个节点由节点名(node-name)和可选的单元地址(unit-address)组成,格式为 node-name@unit-address。节点内部包含若干属性(property),属性由键值对构成,值可以是字符串、32 位无符号整数数组、字符串列表或二进制数据。例如:

/ {
    model = "ACME Board Rev2";
    compatible = "acme,board-rev2", "acme,board";
    
    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        
        uart0: serial@10000000 {
            compatible = "ns16550a";
            reg = <0x10000000 0x100>;
            interrupts = <0 20 4>;
            clock-frequency = <24000000>;
        };
    };
};

根节点下的 model 和 compatible 属性用于标识整块板卡。SoC 厂商通常将芯片内部外设(如 UART、I2C、SPI、DMA 控制器等)封装在 .dtsi 文件中,板级文件(.dts)通过 include 引入 SoC 描述,再添加板级特有的外设(如外部 PHY、Codec、Sensor 等)。这种分层设计使得同一颗 SoC 可以快速适配多种板卡。

判断方法:如何规划 DTS 文件结构

在实际项目中,建议按照以下层次组织文件:

  • SoC 级 .dtsi:描述芯片内部所有外设、时钟、中断控制器、引脚复用等,不包含板级差异。例如 imx8mm.dtsi。
  • 板卡公共 .dtsi:描述某系列板卡的公共部分,如 DDR 配置、PMIC 型号、启动方式。例如 imx8mm-evk-common.dtsi。
  • 具体板卡 .dts:最终编译的源文件,包含板卡独有的外设、GPIO 配置、设备使能状态。例如 imx8mm-evk-rev2.dts。

这种分层结构的好处在于:当 SoC 升级时只需修改 SoC 级 .dtsi;当板卡改版时只需新建或修改板级 .dts,并通过 & 引用覆盖差异部分。

案例拆解:多板卡复用场景

假设某公司基于同一颗 SoC 设计了标准版和精简版两款板卡。标准版带有 Wi-Fi 模块和 4 个 UART,精简版去掉 Wi-Fi 且仅引出 2 个 UART。文件组织如下:

// soc.dtsi(SoC 级)
/ {
    soc {
        uart0: serial@10000000 { /* ... */ };
        uart1: serial@10001000 { /* ... */ };
        uart2: serial@10002000 { /* ... */ };
        uart3: serial@10003000 { /* ... */ };
        wifi: wifi@0 { status = "disabled"; };
    };
};

// board-common.dtsi(板卡公共)
#include "soc.dtsi"
/ {
    model = "ACME SoC Board";
    compatible = "acme,soc-board";
};

// board-std.dts(标准版)
#include "board-common.dtsi"
&uart2 { status = "okay"; };
&uart3 { status = "okay"; };
&wifi { status = "okay"; };

// board-lite.dts(精简版)
#include "board-common.dtsi"
&uart2 { status = "disabled"; };
&uart3 { status = "disabled"; };

通过 & 引用,精简版无需复制整个 SoC 描述,只需将不需要的外设状态改为 disabled。内核在启动时不会初始化这些外设,从而节省资源。

常见误区与总结

误区 1:在 .dtsi 中直接写 status = "okay"。这会导致所有使用该 .dtsi 的板卡都强制使能该设备,无法灵活裁剪。正确做法是 .dtsi 中默认 disabled,板级文件中按需使能。
误区 2:节点命名未包含单元地址。当同一总线下有多个相同类型设备时,缺少 @unit-address 会导致 DTC 编译警告,且不利于地址映射。

3.2 注释、宏定义与 include 机制

核心原理:预处理在 DTC 之前完成

DTS 文件在编译前会经过 C 预处理器(cpp)处理。这意味着 DTS 支持 #include、#define、#ifdef 等预处理指令。这一特性极大地增强了设备树的表达能力。例如,可以定义 GPIO 电平标志、时钟 ID、中断类型等宏,避免在 DTS 中直接书写魔法数字。

#include "dt-bindings/gpio/gpio.h"
#include "dt-bindings/interrupt-controller/irq.h"
#include "dt-bindings/clock/imx8mm-clock.h"

/ {
    gpio-keys {
        compatible = "gpio-keys";
        power-key {
            label = "Power";
            gpios = <&gpio1 0 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_POWER>;
        };
    };
};

其中 GPIO_ACTIVE_LOW 定义在 gpio.h 中,值为 1;KEY_POWER 定义在 input-event-codes.h 中。使用宏定义后,代码可读性显著提升,且当内核 API 更新时只需修改头文件。

操作流程:include 的层级与覆盖规则

include 机制遵循以下规则:

  1. 文件查找路径:DTC 编译时通过 -i 参数指定头文件搜索路径,通常包括内核源码的 include/ 目录和板卡 BSP 目录。
  2. 节点合并:当多个文件定义同一路径的节点时,DTC 会将它们合并。例如 soc.dtsi 中定义了 i2c1 节点,板级文件中通过 &i2c1 追加属性,最终生成的 DTB 中 i2c1 节点包含两处定义的并集。
  3. 属性覆盖:如果同一属性在多个位置定义,后解析的定义覆盖先前的。因此,板级文件中的属性值优先于 .dtsi 中的默认值。
实战提示:在 Makefile 中,DTC 命令通常写作:
dtc -I dts -O dtb -o board.dtb -i arch/arm64/boot/dts/ -i include/dt-bindings/ board.dts
确保 -i 参数覆盖所有需要的头文件路径,否则预处理阶段会报“file not found”错误。

案例拆解:条件编译控制多版本

某项目需要同时支持 DDR3 和 DDR4 两种内存配置,且硬件设计上通过电阻选择。可以在 DTS 中使用条件编译:

#define DDR_TYPE_DDR3  0
#define DDR_TYPE_DDR4  1

#define CURRENT_DDR_TYPE  DDR_TYPE_DDR4

#if CURRENT_DDR_TYPE == DDR_TYPE_DDR4
/ {
    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x80000000>;  /* 2GB DDR4 */
    };
};
#else
/ {
    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x40000000>;  /* 1GB DDR3 */
    };
};
#endif

编译时只需修改 CURRENT_DDR_TYPE 宏即可切换内存配置,无需维护两份 DTS 文件。但需注意:条件编译的代码在 DTS 中不易调试,建议仅在确实需要时使用,且做好注释说明。

常见误区与总结

误区 1:在 .dtsi 中使用 #include 引入板级头文件,造成循环依赖。正确做法是头文件只放宏定义和常量,不放节点定义。
误区 2:滥用 #ifdef 导致 DTS 可读性急剧下降。建议优先使用 & 引用覆盖,而非条件编译。
误区 3:忽略 #address-cells 和 #size-cells 的继承关系。子节点 reg 属性的解析依赖父节点的这两个属性,错误设置会导致地址解析异常。

3.3 标签(label)与引用(&)的深度应用

核心原理:label 是节点的“锚点”

label 是节点的一个别名,定义在节点名前,格式为 label: node-name@unit-address。label 的作用是允许在其他位置通过 &label 引用该节点,从而追加属性、修改属性值或添加子节点。这种机制避免了在 DTS 中重复书写完整的节点路径,也使得板级文件可以精准地修改 SoC 级描述。

// soc.dtsi 中定义
i2c1: i2c@30a20000 {
    compatible = "fsl,imx8mm-i2c", "fsl,imx21-i2c";
    reg = <0x30a20000 0x10000>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";
};

// board.dts 中引用
&i2c1 {
    status = "okay";
    clock-frequency = <400000>;
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <16>;
    };
};

上述代码中,&i2c1 引用了 SoC 级定义的 I2C 控制器节点,追加了 status、clock-frequency 属性,并添加了 EEPROM 子节点。最终 DTB 中 i2c@30a20000 节点包含所有属性。

操作流程:引用覆盖的优先级与合并规则

当多个位置引用同一 label 时,遵循以下规则:

  • 属性合并:不同位置定义的属性全部保留,同名属性后者覆盖前者。
  • 子节点合并:不同位置添加的子节点全部保留,若子节点名相同则递归合并。
  • 覆盖顺序:以 DTC 解析文件的顺序为准,后解析的覆盖先解析的。通常 .dtsi 先被 include,板级 .dts 后解析,因此板级定义优先级最高。

这一机制在 BSP 升级时尤为重要。例如,SoC 厂商更新了 .dtsi 中的外设时钟配置,板级文件无需修改,因为板级只关心使能状态和板级特有属性。

案例拆解:多引用叠加实现复杂配置

考虑一个典型场景:某板卡使用 I2C1 连接触摸屏控制器和 EEPROM,同时 I2C1 的时钟频率需要根据外设要求调整。分步实现:

// 第一步:SoC 级定义(soc.dtsi)
i2c1: i2c@30a20000 {
    compatible = "fsl,imx8mm-i2c";
    reg = <0x30a20000 0x10000>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";
};

// 第二步:板卡公共配置(board-common.dtsi)
&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  // 默认 100kHz
};

// 第三步:具体板卡覆盖(board-rev2.dts)
#include "board-common.dtsi"
&i2c1 {
    clock-frequency = <400000>;  // 覆盖为 400kHz
    touch@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
    };
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

在这个案例中,board-rev2.dts 通过 include 引入了公共配置,再通过 &i2c1 覆盖时钟频率并添加两个子节点。最终 DTB 中 I2C1 以 400kHz 运行,挂载触摸屏和 EEPROM。这种分层覆盖方式使得 Rev1 和 Rev2 板卡只需维护差异部分。

常见误区与总结

误区 1:在 & 引用中重复定义已存在的子节点,导致 DTC 报“duplicate node name”错误。正确做法是使用 &label 引用子节点本身进行修改。
误区 2:忘记在 .dtsi 中为需要被引用的节点添加 label。没有 label 的节点只能通过完整路径引用,路径一旦变化所有引用都会失效。
误区 3:在 & 引用中修改 compatible 属性。这通常意味着硬件设计有误,应通过内核驱动匹配而非修改设备树来解决问题。

3.4 综合实战:从零构建一个板级 DTS

场景描述与目标

假设需要为一款基于 i.MX8MM 处理器的工业控制板编写设备树。硬件配置如下:

  • 2 个 UART(UART2 用于调试,UART3 用于 RS485 通信)
  • 1 个 I2C 总线连接 RTC 和 EEPROM
  • 1 个 SPI 总线连接 NOR Flash
  • 2 个 GPIO 控制 LED(一个电源指示,一个心跳指示)
  • 1 个以太网 PHY(通过 RGMII 接口)

实施步骤

第一步:创建 SoC 级 imx8mm-industrial.dtsi,描述芯片内部外设,所有外设默认 disabled:

/ {
    compatible = "fsl,imx8mm";
    
    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        
        uart2: serial@30890000 {
            compatible = "fsl,imx8mm-uart", "fsl,imx21-uart";
            reg = <0x30890000 0x10000>;
            interrupts = <0 28 IRQ_TYPE_LEVEL_HIGH>;
            status = "disabled";
        };
        
        uart3: serial@30880000 {
            compatible = "fsl,imx8mm-uart", "fsl,imx21-uart";
            reg = <0x30880000 0x10000>;
            interrupts = <0 27 IRQ_TYPE_LEVEL_HIGH>;
            status = "disabled";
        };
        
        i2c1: i2c@30a20000 {
            compatible = "fsl,imx8mm-i2c";
            reg = <0x30a20000 0x10000>;
            interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "disabled";
        };
        
        spi1: spi@30820000 {
            compatible = "fsl,imx8mm-ecspi";
            reg = <0x30820000 0x10000>;
            interrupts = <0 31 IRQ_TYPE_LEVEL_HIGH>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "disabled";
        };
        
        gpio1: gpio@30200000 {
            compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
            reg = <0x30200000 0x10000>;
            interrupts = <0 64 IRQ_TYPE_LEVEL_HIGH>,
                         <0 65 IRQ_TYPE_LEVEL_HIGH>;
            gpio-controller;
            #gpio-cells = <2>;
            status = "okay";
        };
        
        fec1: ethernet@30be0000 {
            compatible = "fsl,imx8mm-fec", "fsl,fec";
            reg = <0x30be0000 0x10000>;
            interrupts = <0 118 IRQ_TYPE_LEVEL_HIGH>,
                         <0 119 IRQ_TYPE_LEVEL_HIGH>;
            phy-mode = "rgmii-id";
            status = "disabled";
        };
    };
};

第二步:创建板级 imx8mm-industrial.dts,引入 SoC 描述并配置板级外设:

#include "imx8mm-industrial.dtsi"
#include "dt-bindings/gpio/gpio.h"
#include "dt-bindings/interrupt-controller/irq.h"

/ {
    model = "ACME i.MX8MM Industrial Board";
    compatible = "acme,imx8mm-industrial", "fsl,imx8mm";
    
    chosen {
        stdout-path = &uart2;
    };
    
    leds {
        compatible = "gpio-leds";
        power-led {
            label = "power";
            gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>;
            default-state = "on";
        };
        heartbeat-led {
            label = "heartbeat";
            gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

&uart2 {
    status = "okay";
};

&uart3 {
    status = "okay";
    linux,rs485-enabled-at-boot-time;
};

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;
    
    rtc@32 {
        compatible = "microcrystal,rv3028";
        reg = <0x32>;
    };
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <16>;
    };
};

&spi1 {
    status = "okay";
    
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

&fec1 {
    status = "okay";
    phy-handle = <&ethphy>;
    
    mdio {
        #address-cells = <1>;
        #size-cells = <0>;
        
        ethphy: ethernet-phy@0 {
            compatible = "ethernet-phy-ieee802.3-c22";
            reg = <0>;
        };
    };
};

第三步:编译验证。使用 DTC 编译并检查是否有警告:

dtc -I dts -O dtb -o imx8mm-industrial.dtb -i include/ imx8mm-industrial.dts
dtc -I dtb -O dts -o decompiled.dts imx8mm-industrial.dtb  # 反编译验证

反编译后检查 i2c@30a20000 节点是否包含 RTC 和 EEPROM 子节点,uart3 是否带有 RS485 标志。

结果与复盘

通过分层设计,该板级 DTS 仅约 80 行代码就完成了全部硬件配置。后续若 Rev2 板卡更换了 RTC 型号,只需修改 &i2c1 中的 RTC 节点;若增加 CAN 总线,只需在 SoC 级 .dtsi 中添加 CAN 控制器描述,并在板级文件中使能。这种可维护性正是设备树分层设计的核心价值。

经验总结:编写 DTS 时,始终遵循