3、设备树文件结构:DTS文件的语法结构、节点(node)、属性(property)、值(value)的规则与写法

设备树(Device Tree)本质上是一种描述硬件拓扑与资源配置的树形数据结构,其源文件(DTS)采用类似 C 语言的声明式语法。理解 DTS 的语法结构、节点(node)、属性(property)与值(value)的规则,是编写、修改和排查设备树问题的基本功。本章将从语法骨架出发,逐层拆解节点与属性的组织方式,并结合实际案例说明如何正确书写和校验设备树片段。

/ (根节点) cpus memory@0 soc cpu@0 cpu@1 i2c@1000 uart@2000 eeprom@50 属性 (property) compatible = "vendor,dev"; reg = <0x1000 0x100>; interrupts = <0 5 4>; 值 (value) 类型 字符串: "string" 32位整数: <0x1 0x2> 字节串: [0x01 0x02] 节点命名规则 node-name@unit-address 如: i2c@1000, cpu@0 标签 (label) &i2c0 { ... };

3.1 DTS 语法骨架与词法规则

DTS 文件采用树形嵌套的括号结构,整体以分号(;)作为语句结束符,以花括号({})界定节点范围。一个最基本的 DTS 文件由根节点“/”开始,内部包含若干子节点,每个子节点又可以继续嵌套。语法上,DTS 与 C 语言有相似之处,支持 /* */ 块注释和 // 行注释,也支持宏定义与 #include 预处理指令(通常配合 .dtsi 头文件使用)。

核心语法骨架如下:

/ {
    model = "Vendor Board Rev2";
    compatible = "vendor,board-rev2", "vendor,board";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            device_type = "cpu";
            reg = <0x0>;
            clock-frequency = <1200000000>;
        };
    };

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x40000000>;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;

        i2c@1000 {
            compatible = "vendor,i2c";
            reg = <0x1000 0x100>;
            clock-frequency = <100000>;
        };
    };
};

词法层面需要特别注意:节点名和属性名区分大小写,且不能包含空格、逗号、括号等特殊字符;字符串值必须使用双引号包裹;32 位整数数组使用尖括号 <> 包裹,多个值用空格分隔;字节串使用方括号 [] 包裹,每个字节为两位十六进制数。DTS 编译器(DTC)对格式要求严格,任何括号不匹配、分号缺失或引号未闭合都会导致编译失败。

核心原理:DTS 的语法本质是“声明式”的——它只描述硬件“有什么、在哪里、怎么配置”,而不描述“如何操作”。操作系统内核通过解析 DTS 生成设备模型,驱动再根据 compatible 属性匹配对应的驱动代码。因此,DTS 的语法结构直接决定了内核看到的硬件拓扑。

3.2 节点(node)的规则与写法

节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线、控制器或抽象层次。节点可以嵌套,形成父子关系,反映硬件拓扑的从属结构。节点名称由两部分组成:节点名(node-name)和可选的单元地址(unit-address),格式为 node-name@unit-address。

节点命名规则如下:

  • 节点名:必须为 1~31 个字符,只能包含小写字母、数字、逗号、点、下划线和连字符,且必须以字母或数字开头。推荐使用小写字母和数字,避免使用下划线开头。
  • 单元地址:用于区分同一层级下同类型的不同实例,通常对应设备在总线上的地址(如 I2C 从机地址、寄存器基地址)。单元地址前必须加 @ 符号,且只能包含十六进制数字(0-9, a-f)。
  • 根节点:固定为 /,是整个设备树的起点,没有父节点。
  • 标签(label):节点前可以加标签,格式为 label: node-name@unit-address,标签用于在 DTS 其他位置引用该节点(通过 &label 语法)。

例如,一个 I2C 控制器节点可以写为:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
    };
};

这里 eeprom@50 表示该 EEPROM 设备在 I2C 总线上的从机地址为 0x50。节点名 eeprom 是通用设备名,@50 是单元地址。注意,同一父节点下,节点名+单元地址的组合必须唯一,否则 DTC 会报重复节点错误。

3.3 属性(property)与值(value)的规则

属性是节点的“键值对”,用于描述设备的特性、配置参数、资源映射等。属性名必须为 1~31 个字符,只能包含小写字母、数字、逗号、点、下划线和连字符,且必须以字母或数字开头。属性值可以是空(表示布尔真)、字符串、32 位无符号整数数组、字节串,或这些类型的组合。

常见的属性值类型及写法如下表:

值类型 语法格式 示例 典型用途
空值(布尔) 仅属性名,无值 status = "okay"; 或 disable-rtc; 表示功能开关或标志位
字符串 双引号包裹 compatible = "vendor,device"; 设备兼容性标识、型号名称
32 位整数数组 尖括号包裹,空格分隔 reg = <0x1000 0x100>; 寄存器地址、长度、中断号、时钟频率
字节串 方括号包裹,十六进制字节 local-mac-address = [00 11 22 33 44 55]; MAC 地址、序列号、固件版本
混合类型 使用逗号分隔不同值块 gpios = <&gpio1 2 0>, <&gpio2 5 0>; GPIO 描述、多资源引用

其中,compatible 属性是最关键的属性之一,它定义了设备与驱动的匹配关系。其值通常为“厂商,型号”格式的字符串列表,内核会按顺序与驱动注册的 compatible 表进行匹配。例如:

compatible = "vendor,ethernet", "simple-bus";

这表示该设备首先尝试匹配 vendor,ethernet 驱动,若失败则尝试 simple-bus 驱动。字符串列表按优先级从高到低排列。

3.4 标准属性与地址编码规则

设备树规范定义了一些标准属性,用于描述设备资源映射和中断信息。最核心的是 reg 属性,它配合父节点的 #address-cells 和 #size-cells 属性共同解释地址和长度信息。

#address-cells 表示地址字段占用的 32 位整数个数,#size-cells 表示长度字段占用的 32 位整数个数。例如:

soc {
    #address-cells = <1>;
    #size-cells = <1>;

    uart@2000 {
        reg = <0x2000 0x100>;
        // 地址 = 0x2000,长度 = 0x100
    };
};

对于 64 位地址的系统,#address-cells 可以设为 2,此时 reg 中的地址字段需要两个 32 位整数拼接表示。例如:

memory@0 {
    device_type = "memory";
    reg = <0x0 0x0 0x80000000>;
    // 64位地址 = 0x00000000_00000000,长度 = 0x80000000
};

中断属性 interrupts 的格式由父节点的 #interrupt-cells 决定。对于 ARM GIC(通用中断控制器),通常 #interrupt-cells = <3>,三个字段分别表示中断类型(0=SPI 共享外设中断,1=PPI 私有外设中断)、中断号、触发类型(1=上升沿,4=高电平)。

经验提醒:在编写 reg 属性时,务必确认父节点的 #address-cells 和 #size-cells 值。如果父节点未定义这两个属性,默认值为 1。地址和长度字段的个数不匹配,会导致 DTC 编译警告,且内核解析时可能得到错误的资源地址。

3.5 案例拆解:从零编写一个 I2C EEPROM 节点

下面通过一个完整案例,演示如何在实际项目中编写和校验设备树节点。假设某嵌入式板卡上有一颗 I2C EEPROM(型号 AT24C02),挂接在 SoC 的 I2C0 控制器上,I2C0 的寄存器基地址为 0x1000,地址空间长度 0x100。系统使用 ARM GIC 中断控制器,I2C0 使用 SPI 中断号 25,高电平触发。

初始现象与目标:内核启动后无法识别 EEPROM 设备,i2cdetect 工具扫描不到 0x50 地址。需要编写正确的设备树节点,使内核 I2C 子系统能够枚举到该设备。

关键输入参数:

  • SoC 数据手册:I2C0 基地址 0x1000,中断号 25(SPI),触发类型高电平(4)
  • EEPROM 数据手册:I2C 从机地址 0x50(7 位地址),页大小 8 字节
  • 原理图:EEPROM 的 WP 引脚接 GND(写保护禁用),A0/A1/A2 接地(地址 0x50)

分析判断过程:首先确认 I2C0 控制器节点是否已存在于 SoC 的 .dtsi 文件中。通常 SoC 厂商会提供基础 .dtsi,其中已定义 i2c0 节点及其寄存器、中断属性。我们只需在板级 .dts 文件中通过 &i2c0 引用并添加子节点即可。

实际操作步骤:

// 在板级 .dts 文件中
&i2c0 {
    status = "okay";
    clock-frequency = <400000>;  // 提升到 400kHz 快速模式

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
        read-only;  // 如果 WP 引脚接高电平,则添加此属性
    };
};

编译设备树后,内核启动日志中应能看到类似 i2c 0x50: at24c02 detected 的信息。使用 i2cdetect -y 0 命令,应能在 0x50 位置看到设备编号(如 50 或 UU)。

结果对比与复盘:如果仍然无法识别,需要检查以下几点:

  1. 确认 status = "okay" 是否已设置(默认可能为 "disabled")
  2. 确认 I2C 控制器节点本身是否使能,以及引脚复用(pinmux)是否正确配置
  3. 使用 i2cdetect -r -y 0 强制扫描,排除设备 NACK 问题
  4. 检查 EEPROM 的地址引脚是否与原理图一致,0x50 对应 A0=A1=A2=0

风险提示:在修改设备树时,不要随意更改 SoC 厂商 .dtsi 中已定义的标准节点属性(如 reg、interrupts),除非你完全理解其含义。错误修改可能导致整个外设控制器无法工作,甚至引发内核启动崩溃。正确的做法是在板级 .dts 中通过 &label 引用节点,仅覆盖需要修改的属性。

3.6 常见误区与总结复盘

在编写 DTS 文件时,初学者常犯以下几类错误,需要特别注意:

误区类型 错误示例 正确做法 后果
节点名大小写混用 I2C@1000 i2c@1000 DTC 编译报错,或与驱动匹配失败
reg 字段个数与父节点不匹配 父节点 #address-cells=<2>,子节点 reg = <0x1000> reg = <0x0 0x1000> 地址解析错误,设备资源申请失败
字符串值未加引号 compatible = vendor,device; compatible = "vendor,device"; 语法错误,编译失败
整数数组使用逗号分隔 reg = <0x1000, 0x100>; reg = <0x1000 0x100>; DTC 解析异常,值被截断
忘记设置 status 属性 节点未写 status 显式设置 status = "okay" 设备默认禁用,无法工作

总结复盘时,建议遵循以下编写流程:

  1. 查阅 SoC 数据手册,确认外设的寄存器基地址、中断号、时钟频率等硬件参数。
  2. 查看厂商 .dtsi,了解已有节点定义,避免重复定义或冲突。
  3. 在板级 .dts 中引用并覆盖,使用 &label 语法添加子节点或修改属性。
  4. 编译并检查警告,使用 make dtbs 编译,仔细阅读 DTC 输出的 warning 信息。
  5. 运行时验证,通过 /proc/device-tree 或 dtc -I fs -O dts /proc/device-tree 导出实际生效的设备树,确认节点和属性与预期一致。

掌握 DTS 的语法结构、节点与属性的规则,是设备树开发的基础。下一阶段的学习应聚焦于 compatible 匹配机制、中断映射、GPIO 描述等进阶主题,但所有进阶内容都建立在本章所讲的语法骨架之上。建议读者在编写每个节点时,都对照本章的规则表逐项检查,形成严谨的编写习惯。