2、DTS语法入门:节点、属性、值的表示方法,以及include机制

好,咱们正式开始啃DTS语法这块硬骨头。

说实话,我刚开始接触设备树时,看着那一堆花括号和分号,心里直犯嘀咕:这不就是个配置文件吗?怎么搞得这么复杂?后来被硬件折腾了几回,才明白——设备树其实是内核与硬件之间的「翻译官」。你写对了,内核就认识你的板子;写错了,系统直接罢工给你看。

2.1 节点:设备树的骨架

节点是DTS最基本的组成单元。你可以把它想象成一个文件夹,每个节点代表一个硬件设备或总线。

节点长这样:

/dts-v1/;

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

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

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

看到没?根节点是 /,里面挂着cpu和memory两个子节点。每个节点用花括号包裹,末尾加分号。

节点命名规则:

  • 格式:节点名@地址
  • 节点名不能有空格、特殊字符
  • 地址部分通常用十六进制,比如 cpu@0i2c@1c00000

我个人习惯把节点名写得尽量短,但别短到看不懂。比如 uart0 就比 serial@44e09000 好记,但后者更精确。看项目要求吧。

2.2 属性:描述设备的「形容词」

属性就是节点的特征描述。每个属性都有名字和值,用等号连接,分号结尾。

常见的属性类型:

属性名 含义 示例
compatible 设备兼容性标识 "vendor,device"
reg 寄存器地址/长度 <0x1000 0x100>
interrupts 中断号 <0 29 4>
status 设备状态 "okay" 或 "disabled"

嗯,这里要注意:compatible 是内核匹配驱动的关键。我见过有人把厂商名写反了,结果驱动死活加载不上。排查了半天,最后发现是 "ti,am335x" 写成了 "am335x,ti"——这种低级错误,犯一次就记住了。

2.3 值的表示方法:五花八门但都有规矩

DTS里的值类型挺多的,我列个表给你看:

类型 写法 说明
字符串 "hello" 双引号包裹
32位整数 <0x12345678> 尖括号包裹
64位整数 <0x00000000 0x80000000> 两个32位拼起来
字节数组 [01 02 03 04] 方括号包裹
字符串列表 "str1", "str2" 逗号分隔
混合值 <0x1000> "info" 空格分隔不同类型

举个例子,一个完整的I2C控制器节点:

i2c@44e0b000 {
    compatible = "ti,omap4-i2c";
    reg = <0x44e0b000 0x1000>;
    interrupts = <70>;
    status = "okay";
    clock-frequency = <100000>;
};

你看,reg 用了两个32位整数表示基地址和长度,interrupts 用一个整数表示中断号,clock-frequency 直接给数值。每种值都有自己的一套写法,别搞混了。

小技巧:64位地址一定要用两个32位整数表示。我曾经在项目里写 reg = <0x800000000>,编译直接报错。后来才知道,DTC编译器默认32位,超过范围的地址得拆开写。

2.4 include机制:代码复用的利器

一个复杂的板子,设备树可能有几千行。全写在一个文件里?那维护起来简直是噩梦。

DTS支持 #include 预处理指令,用法和C语言一模一样:

// myboard.dts
#include "soc.dtsi"
#include "myboard-pinmux.dtsi"

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

    &uart0 {
        status = "okay";
        pinctrl-0 = &uart0_pins;
    };
};

这里 soc.dtsi 定义了芯片内部的外设,myboard-pinmux.dtsi 定义了引脚复用。主文件只需要引用并覆盖需要的部分就行。

为什么会用 .dtsi 后缀?说白了,这是设备树的头文件。就像C语言的 .h 文件一样,.dtsi 专门放公共定义,.dts 放板级定制内容。

避坑指南:我曾经在一个项目里,把 #include 写成了 /include/,这是老式语法,新版本DTC已经不支持了。编译报错时我还纳闷:明明语法没错啊?后来查文档才发现,从2015年起,DTC就只认 #include 了。

2.5 节点引用与覆盖

include进来之后,怎么修改已有的节点?用 & 引用:

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

// myboard.dts 中覆盖
&uart0 {
    status = "okay";
    clock-frequency = <48000000>;
};

注意看,uart0: 是标签(label),后面用 &uart0 引用。这样就不用把整个节点重新写一遍,只改需要改的属性就行。

我个人觉得,这种设计很优雅。就像面向对象里的继承——基类定义通用属性,子类只覆盖差异部分。内核开发者们确实用心了。

2.6 实战:一个完整的DTS文件长啥样

把上面这些知识点串起来,一个典型的设备树文件大概是这样:

/dts-v1/;

#include "am335x.dtsi"
#include "am335x-bone-common.dtsi"

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";

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

    leds {
        pinctrl-names = "default";
        pinctrl-0 = <&user_led_pins>;

        led@2 {
            label = "beaglebone:green:heartbeat";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

&uart0 {
    status = "okay";
};

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

你看,根节点定义板级信息,memory节点描述内存,leds节点描述板载LED,最后用引用打开UART和I2C。结构清晰,层次分明。

核心要点总结:

  • 节点用花括号,属性用等号,分号结尾不能忘
  • compatible 是驱动匹配的关键,写错就认不出硬件
  • 64位地址要拆成两个32位整数
  • #include 复用公共定义,&引用覆盖节点
  • .dtsi 是头文件,.dts 是板级文件

嗯,DTS语法其实不难,难的是把硬件手册里的寄存器地址、中断号、引脚配置准确翻译成设备树。我刚开始写的时候,经常对着几百页的datasheet一个一个数地址,眼睛都快看花了。但熟练之后,你会发现设备树其实就是硬件的「地图」——节点是地标,属性是路标,include就是地图的图例。把这张图画好了,内核就能在你的板子上跑得顺风顺水。

DTS语法知识体系 DTS文件 节点 (Node) 属性 (Property) 值 (Value) #include 根节点 / 子节点 cpu@0 标签引用 &uart0 compatible reg status / interrupts 字符串 "okay" 整数 <0x1000> 字节数组 [01 02] soc.dtsi board.dtsi 代码复用 核心流程 1. 用 #include 引入公共定义 (.dtsi) 2. 定义根节点和子节点,描述硬件结构 3. 用属性填充设备细节(地址、中断、状态) 4. 用 & 引用覆盖默认值,完成板级定制

这张图把DTS语法的四个核心要素串起来了。你写设备树时,就按这个流程走:先include公共头文件,再定义节点,填充属性,最后用引用做定制。顺序对了,思路就清晰了。

专注资料整理