第2章:设备树基础语法:DTS文件结构、节点与属性、标签与引用、include机制

好,咱们接着往下聊。上一章我带你看了设备树到底长什么样,这一章咱们就深入进去,把它的语法规则掰开揉碎了讲清楚。说实话,我刚接触设备树那会儿,看着一堆花括号和分号,心里直犯嘀咕——这不就是个配置文件吗?后来踩的坑多了,才明白它背后那套严谨的语法设计。

2.1 DTS文件的基本骨架

一个标准的DTS文件,说白了就是一棵树。根节点是唯一的,下面挂着一堆子节点。每个节点描述一个硬件设备或者总线。我习惯把DTS文件想象成公司组织架构图——CEO是根节点,各部门经理是子节点,每个员工就是具体的属性。

来看个最简单的例子:

/dts-v1/;

/ {
    compatible = "acme,coyote";
    #address-cells = <1>;
    #size-cells = <1>;

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

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

注意第一行 /dts-v1/;,这个版本声明不能少。我见过有人抄代码漏掉这行,结果编译直接报错。嗯,这里要记住:每个DTS文件必须以这个开头。

根节点用 / 表示,里面可以嵌套任意深度的子节点。节点名后面可以跟 @地址,这个地址通常是设备在总线上的基地址。比如 cpu@0 表示CPU的起始地址是0。

2.2 节点(node)的命名规则

节点命名其实有讲究。格式是 node-name@unit-address,其中 @unit-address 是可选的。但如果你有多个同类型设备,建议加上地址区分。

举个例子:

uart0: serial@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x1000>;
    interrupts = <33>;
};

uart1: serial@10001000 {
    compatible = "ns16550";
    reg = <0x10001000 0x1000>;
    interrupts = <34>;
};

这里 uart0 是标签(label),后面会讲。节点名是 serial,地址分别是 0x100000000x10001000。这样一看就知道是两个不同的串口。

小技巧:节点名尽量用标准名称,比如 seriali2cspi。Linux内核里有很多匹配规则是基于节点名的。我习惯参考内核源码里的 Documentation/devicetree/bindings/ 目录,那里有官方命名规范。

2.3 属性(property)的写法

属性就是节点的配置信息。常见的属性类型有几种:

属性类型 写法示例 说明
字符串 compatible = "arm,cortex-a9"; 用双引号括起来
32位整数 reg = <0x10000000 0x1000>; 尖括号括起来,空格分隔
64位整数 reg = <0x00000000 0x80000000>; 用两个32位表示
字符串列表 compatible = "ti,am335x-uart", "ns16550"; 逗号分隔多个字符串
二进制数据 mac-address = [00 11 22 33 44 55]; 方括号括起来,十六进制
混合类型 mixed = <0x1234 0x5678>, "hello"; 逗号分隔不同类型

这里我要特别强调 compatible 属性。它是设备树里最重要的属性之一,Linux内核就是靠它来匹配驱动。格式是 "厂商,设备型号"。比如 "ti,am335x-uart" 表示TI公司的AM335x系列UART。

注意:我曾经在项目里把 compatible 写成了 compatiable,少了个i。结果内核死活不认这个设备,查了两天才发现是拼写错误。这种低级错误最坑人,一定要仔细检查。

2.4 标签(label)与引用

标签是设备树里非常实用的功能。你可以给节点起个外号,然后在其他地方通过 &标签 来引用它。这有点像C语言里的指针。

看个例子:

/ {
    i2c0: i2c@44e0b000 {
        compatible = "ti,omap4-i2c";
        #address-cells = <1>;
        #size-cells = <0>;
        reg = <0x44e0b000 0x1000>;
        interrupts = <70>;

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

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

这里 i2c0: 就是标签。在文件后面,我用 &i2c0 引用了这个节点,给它添加了 statusclock-frequency 属性。这种写法在设备树里非常常见,特别是当你需要覆盖或扩展某个节点时。

我个人习惯把标签写得简短有意义。比如 uart0i2c1mmc0,一看就知道是什么设备。别用 abc 这种,时间长了你自己都记不住。

2.5 include机制

设备树支持 #include 预处理指令,这跟C语言一模一样。你可以把公共的部分抽出来放到头文件里,然后在多个DTS文件中引用。

举个例子:

// am335x.dtsi
/ {
    ocp {
        uart0: serial@44e09000 {
            compatible = "ti,am335x-uart";
            reg = <0x44e09000 0x2000>;
            interrupts = <72>;
            status = "disabled";
        };
    };
};
// myboard.dts
#include "am335x.dtsi"

&uart0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
};

你看,我把AM335x芯片的公共定义放在 .dtsi 文件里,板级文件只需要 #include 进来,然后覆盖需要的部分。这样既减少了重复代码,又方便维护。

核心思想:设备树的include机制实现了SoC级定义和板级定义的分离。SoC厂商提供 .dtsi 文件,板卡厂商只需要写自己的 .dts 文件,引用并修改少量配置即可。这大大降低了工作量。

我记得第一次做设备树移植时,面对几百行的DTS文件完全不知道从哪下手。后来发现大部分内容都在 .dtsi 里定义好了,我只需要关注板级特有的部分——GPIO复用、电源管理、外设使能这些。嗯,想通了这一点,事情就简单多了。

2.6 实战中的常见陷阱

最后分享几个我踩过的坑:

  • 分号不能少:每个属性声明和节点结束都要加分号。漏一个分号,编译就报错,而且错误位置往往不准确。
  • 地址和大小要匹配:#address-cells#size-cells 决定了 reg 属性里每个地址和大小占几个32位。比如 #address-cells = <2> 表示地址用两个32位表示,也就是64位地址。
  • 标签不能重复:同一个文件里标签必须唯一。我曾经在两个不同的 .dtsi 文件里定义了同名的 gpio0 标签,结果编译时冲突了。
  • 路径引用要小心:&标签 引用时,确保标签在之前已经定义过。虽然编译器会检查,但错误信息有时候不太直观。

好了,这一章的内容就到这里。设备树语法其实不复杂,但细节很多。我建议你动手写几个简单的DTS文件,用 dtc 工具编译一下,看看报错信息是什么样的。实践出真知嘛。