设备树语法详解:节点、属性与包含文件

设备树(Device Tree)说白了就是一块描述硬件的数据结构。我刚开始接触时也觉得它挺绕,后来发现它其实就是个树形结构——根节点下面挂子节点,子节点下面再挂子节点,每个节点上贴一堆标签(属性)。今天咱们就把这棵树的语法掰开揉碎了讲清楚。

核心思想:设备树不是代码,是数据。它告诉内核“我有哪些硬件,它们怎么连接,怎么访问”。

2.1 节点(Node)与属性(Property)

设备树的基本单位是节点。每个节点代表一个硬件设备或总线。节点可以嵌套,形成层级关系。

/dts-v1/;

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

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

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* 1GB RAM */
    };

    serial@101f1000 {
        compatible = "ns16550";
        reg = <0x101f1000 0x1000>;
        interrupts = <0x0 0x1 0x4>;
    };
};

看到这个结构了吗?/ 是根节点,下面挂了三个子节点。每个节点用 @ 后面的地址来区分同一层级的不同实例。比如 cpu@0 和 cpu@1 就是两个不同的 CPU 核心。

节点命名规则:

  • node-name@unit-address 格式
  • node-name 是通用名称,比如 serial、i2c、spi
  • unit-address 是设备在总线上的地址,通常与 reg 属性的第一个地址一致
  • 如果节点没有地址(比如根节点),可以省略 @ 部分

我的习惯:命名时尽量用标准名称,别自己发明。比如串口就叫 serial,别叫 uart_port_0。内核匹配驱动时靠的是 compatible 属性,不是节点名。

2.2 标准属性详解

属性就是键值对。值可以是整数、字符串、字节数组,或者它们的组合。下面这几个属性是设备树的“标配”,几乎每个节点都会用到。

2.2.1 compatible —— 驱动匹配的关键

compatible 是设备树里最重要的属性。它告诉内核:“这个设备是什么型号,可以用哪个驱动”。

compatible = "vendor,device", "generic,device";

内核匹配驱动时,会从左到右依次尝试。第一个匹配上了就用第一个,否则试第二个。我遇到过一个问题:某款芯片的 I2C 控制器兼容性字符串写反了,结果内核加载了通用驱动,导致高速模式打不开。排查了一整天才发现是字符串顺序问题。

避坑指南:compatible 字符串的格式是 "厂商,型号",厂商名用小写,型号用标准名称。不要随便写,内核源码的 Documentation/devicetree/bindings/ 目录下有规范,写之前先查一下。

2.2.2 reg —— 地址与大小

reg 描述设备的地址空间。格式是 <地址 长度> 的序列。

reg = <0x101f1000 0x1000>;  /* 基地址 0x101f1000,长度 4KB */

地址和长度的位数由父节点的 #address-cells 和 #size-cells 决定。比如:

#address-cells = <2>;  /* 地址用两个 32 位整数表示,即 64 位 */
#size-cells = <1>;     /* 长度用一个 32 位整数表示 */

那么 reg = <0x0 0x80000000 0x40000000> 就表示地址 0x80000000(高 32 位是 0),长度 0x40000000(1GB)。

属性 含义 示例
#address-cells 地址字段占几个 32 位整数 <1> 表示 32 位地址
#size-cells 长度字段占几个 32 位整数 <1> 表示 32 位长度
reg 地址空间列表 <0x1000 0x100>

2.2.3 interrupts —— 中断连接

描述设备使用哪个中断。格式取决于中断控制器。

interrupts = <0x0 0x1 0x4>;
interrupt-parent = <&gic>;

这里 interrupt-parent 指定中断控制器(通常是 GIC),interrupts 里的三个值分别是:中断类型(SPI=0,PPI=1)、中断号、触发方式(高电平=4,上升沿=1 等)。

我踩过的坑:中断号是硬件相关的,不同芯片的中断号映射可能不同。我曾经把 SPI 中断号写成了 PPI 的,结果中断一直触发不了。后来用 cat /proc/interrupts 一看,中断根本没注册上。

2.2.4 其他常用属性

  • status:设备状态。"okay" 表示启用,"disabled" 表示禁用。调试时很有用。
  • device_type:设备类型。早期设备树用得比较多,现在基本被 compatible 取代了。
  • label:给节点起个别名,方便在板级文件中引用。
  • clock-frequency:时钟频率,常用于串口、I2C 等外设。

2.3 设备树包含文件(.dtsi)

一个芯片可能有几十种外设,如果每个板子都写一遍,那得累死。所以设备树引入了 .dtsi 文件,相当于 C 语言的头文件。

典型结构:

// imx6ull.dtsi —— SoC 级定义
/ {
    soc {
        aips1: aips-bus@2000000 {
            uart1: serial@2020000 {
                compatible = "fsl,imx6ul-uart", "fsl,imx-uart";
                reg = <0x2020000 0x4000>;
                interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
                status = "disabled";
            };
        };
    };
};
// myboard.dts —— 板级文件
#include "imx6ull.dtsi"

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard", "fsl,imx6ull";
};

&uart1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
};

看到 &uart1 了吗?这是引用语法。在 .dtsi 里定义节点时加上标签(比如 uart1:),然后在板级文件里用 &uart1 引用它,就可以修改或追加属性。

我的建议:把 SoC 通用的外设定义放在 .dtsi 里,默认状态设为 disabled。板级文件里只启用实际用到的外设,并配置引脚复用。这样复用性最好。

2.4 知识体系结构图

下面这张图展示了设备树语法的核心结构,从根节点到属性,再到包含文件的组织方式:

设备树语法核心结构 设备树语法核心结构 根节点 / 子节点:cpu@0, serial@..., i2c@... compatible reg interrupts "vendor,device" <addr size> <type irq flags> .dtsi 包含文件 imx6ull.dtsi (SoC级) myboard.dts (板级) 节点定义硬件结构,属性描述硬件细节,.dtsi 实现复用 板级文件通过 #include 和 & 引用修改 SoC 级定义

2.5 实战:从零写一个设备树片段

假设我们要描述一个板子,上面有一颗 i.MX6ULL 芯片,带一个串口和一个 GPIO 按键。我会这样写:

// soc.dtsi —— SoC 通用定义
/ {
    #address-cells = <1>;
    #size-cells = <1>;

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

        uart1: serial@2020000 {
            compatible = "fsl,imx6ul-uart", "fsl,imx-uart";
            reg = <0x2020000 0x4000>;
            interrupts = <0 26 4>;
            status = "disabled";
        };

        gpio1: gpio@2090000 {
            compatible = "fsl,imx6ul-gpio", "fsl,imx-gpio";
            reg = <0x2090000 0x4000>;
            interrupts = <0 66 4>;
            gpio-controller;
            #gpio-cells = <2>;
            status = "disabled";
        };
    };
};
// myboard.dts —— 板级文件
/dts-v1/;
#include "soc.dtsi"

/ {
    model = "My i.MX6ULL Board";
    compatible = "mycompany,imx6ull-board", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

    gpio-keys {
        compatible = "gpio-keys";
        user-button {
            label = "User Button";
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
        };
    };
};

&uart1 {
    status = "okay";
};

&gpio1 {
    status = "okay";
};

关键点:

  • .dtsi 里外设默认 disabled,板级文件按需启用
  • chosen 节点设置内核启动参数,比如控制台串口
  • gpio-keys 是内核标准驱动,通过 gpios 属性绑定硬件引脚
  • &uart1 语法用于引用和修改已定义的节点