2. DTS语法详解:节点、属性、标签、包含文件的完整规则

好,咱们今天来啃DTS语法这块硬骨头。说实话,我刚接触设备树那会儿,看着那些花花绿绿的括号和分号,头都大了。但后来我发现,DTS的语法其实比C语言简单得多——它就是个结构化的描述语言,没有复杂的控制流,没有函数调用。

说白了,DTS就是用来描述硬件信息的。你告诉内核:我这儿有个I2C控制器,挂在哪个地址上,用哪个中断号。就这么简单。

2.1 节点(Node)——设备树的基本单元

节点是DTS的核心。每个节点代表一个设备、一个总线、或者一个控制器。我习惯把节点想象成硬件设备在软件世界里的身份证。

节点的基本语法长这样:

/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "vendor,myboard";

    cpu@0 {
        device_type = "cpu";
        reg = <0>;
        compatible = "arm,cortex-a9";
    };

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

看到没?每个节点用花括号包裹,以分号结尾。节点名后面可以跟一个@符号加地址——这个地址通常是寄存器基址或者设备编号。

节点命名规则:

  • 节点名必须小写字母开头
  • 可以包含数字、连字符和下划线
  • @后面的地址是十六进制数,不带0x前缀
  • 根节点用斜杠 / 表示

我在项目中遇到过一个问题:有人把节点名写成了大写开头,结果内核死活不认。嗯,DTS解析器对大小写是敏感的,这点要记住。

2.2 属性(Property)——描述节点的特征

属性就是节点的特征描述。你可以把它理解成C语言里的变量,只不过它的类型更丰富。

常见的属性类型有这些:

属性类型 语法示例 说明
字符串 compatible = "vendor,device"; 用双引号包裹
32位整数 reg = <0x10000000 0x1000>; 尖括号包裹,空格分隔
64位整数 reg = <0x00000000 0x80000000>; 用两个32位表示
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号包裹,十六进制
布尔值 status = "disabled"; 用字符串表示
混合类型 foo = <0x1234>, "string"; 逗号分隔不同类型

我最常用的是compatible属性。它就像设备的身份证号,内核通过它来匹配驱动。格式是"厂商,设备型号",比如"ti,am335x-uart"。

避坑指南:我曾经在reg属性里少写了一个地址单元,结果驱动读到的地址完全不对。调试了两天才发现是地址长度写错了。所以写reg的时候,一定要确认好地址和长度各占几个cell。

2.3 标签(Label)——给节点起个外号

标签就是节点的别名。你想想看,如果一个节点名特别长,比如"spi@48030000",每次引用它都要写全名,多麻烦。标签就是解决这个问题的。

// 定义标签
uart0: serial@44e09000 {
    compatible = "ti,am335x-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

// 引用标签
&uart0 {
    status = "okay";
    clock-frequency = <48000000>;
};

看到&uart0了吗?这就是通过标签引用节点。我习惯在写DTS的时候,给所有重要的设备都加上标签。这样后面要修改或者引用的时候,方便得很。

标签的命名规则:

  • 以字母或下划线开头
  • 可以包含字母、数字、下划线
  • 不能是关键字(比如compatible、reg这些)
  • 在整个设备树中必须唯一

注意:标签和节点名是两回事。标签只是引用方式,不会影响节点在设备树中的路径。比如上面的uart0,它的完整路径还是/serial@44e09000。

2.4 包含文件(Include)——模块化你的设备树

一个复杂的SoC可能有几十个外设,如果全部写在一个文件里,那简直是一场灾难。include指令就是用来解决这个问题的。

// 主文件:myboard.dts
#include "soc.dtsi"
#include "myboard.dtsi"

/ {
    model = "MyBoard";
    compatible = "vendor,myboard";
};

// soc.dtsi 文件
/ {
    cpu@0 {
        compatible = "arm,cortex-a9";
        reg = <0>;
    };

    uart0: serial@44e09000 {
        compatible = "ti,am335x-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        status = "disabled";
    };
};

include指令有两种写法:

  • #include "file.dtsi" —— 标准C预处理器语法
  • /include/ "file.dtsi" —— DTS原生语法

我个人习惯用#include,因为大多数DTS文件都会经过C预处理器处理,这样还能用宏定义。

说到dtsi文件,它和dts文件有什么区别?dtsi是设备树包含文件,通常用来描述SoC内部的通用硬件。dts文件才是具体的板级文件。一个SoC的dtsi可以被多个板子的dts文件包含。

文件组织建议:

  • soc.dtsi —— SoC通用外设定义
  • soc-pinmux.dtsi —— 引脚复用配置
  • myboard.dts —— 具体板级配置
  • myboard-xxx.dtsi —— 板级扩展配置

2.5 节点引用与覆盖

这是DTS最强大的特性之一。你可以在一个地方定义节点,在另一个地方修改它。我经常用这个特性来覆盖dtsi文件里的默认配置。

// soc.dtsi 中定义
uart0: serial@44e09000 {
    compatible = "ti,am335x-uart";
    reg = <0x44e09000 0x2000>;
    status = "disabled";
};

// myboard.dts 中覆盖
&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
};

看到没?在myboard.dts里,我通过&uart0引用了之前定义的节点,然后只修改了status和pinctrl属性。其他属性保持不变。这就是设备树的继承和覆盖机制。

我曾经在调试一个I2C设备的时候,发现设备树里明明配了中断,但驱动就是收不到中断。后来发现是dtsi文件里把中断号写错了,而板级文件里没有覆盖这个属性。嗯,从那以后我每次都会检查所有覆盖是否正确。

2.6 特殊属性详解

有几个属性特别重要,我单独拿出来说说:

compatible: 驱动匹配的关键。内核通过它找到对应的驱动。格式是"厂商,设备型号"。比如"ti,am335x-uart"、"fsl,imx6q-uart"。

reg: 描述设备地址空间。格式是<地址1 长度1 地址2 长度2 ...>。每个地址和长度占几个32位单元,由父节点的#address-cells和#size-cells决定。

interrupts: 描述中断信息。格式取决于中断控制器的配置。通常是一个中断号加一个触发类型。

status: 设备状态。可选值有"okay"、"disabled"、"reserved"等。我习惯在dtsi里把所有外设都设为disabled,然后在板级文件里按需开启。

小技巧:写compatible属性的时候,我建议把最具体的型号写在前面,通用的写在后面。比如"ti,am335x-uart"、"ns16550a"。这样内核匹配的时候,会优先匹配最具体的驱动。

2.7 实战:一个完整的DTS片段

说了这么多,咱们来看一个完整的例子。这是我之前做的一个项目里的实际代码:

// 包含SoC通用定义
#include "am335x.dtsi"

/ {
    model = "TI AM335x MyBoard";
    compatible = "ti,am335x-myboard", "ti,am335x";

    // 内存配置
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB
    };

    // 板级LED
    leds {
        compatible = "gpio-leds";
        led0 {
            label = "heartbeat";
            gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

// 覆盖SoC默认配置
&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

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

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

这个例子涵盖了节点、属性、标签、包含文件、节点覆盖等所有知识点。你可以看到,整个设备树结构清晰,层次分明。

2.8 知识体系图

下面这张图总结了DTS语法的核心知识体系:

DTS语法知识体系 DTS语法 节点 (Node) 属性 (Property) 标签 (Label) 包含文件 根节点 / 子节点 节点名 + @地址 花括号 + 分号 compatible / reg interrupts / status 字符串 / 整数 / 数组 别名引用 &标签名 语法 节点覆盖 #include 指令 dtsi 文件 模块化组织 掌握这四部分,DTS语法就通了

嗯,DTS语法其实不复杂。你只要记住:节点是骨架,属性是血肉,标签是别名,包含文件是模块化工具。把这四样东西搞明白了,写设备树就跟搭积木一样简单。

我刚开始学的时候,总觉得设备树很神秘。后来发现,它就是个结构化的硬件描述语言。你只要按照规则把硬件信息写清楚,内核就能自动帮你匹配驱动、分配资源。

好了,这一章的内容就到这里。记住我强调的几个关键点:节点命名规则、属性类型、标签引用、文件组织。这些都是实际开发中天天要用到的东西。

专注资料整理