2. 设备树基础语法:节点、属性、标签、包含与宏定义

设备树(Device Tree)说白了就是一块板子的“硬件清单”。你想想看,Linux 内核要跑起来,总得知道 CPU 是哪个、内存多大、外设挂在哪条总线上吧?以前这些东西都硬编码在 arch/arm/ 目录下的 C 文件里,换一块板子就得重新编译内核。后来大家觉得这样太折腾了,于是设备树就诞生了。

我个人习惯把设备树比作“硬件说明书”。内核启动时读这份说明书,就知道该怎么初始化硬件了。今天我们就来拆解这份说明书的语法。

2.1 节点(node)—— 设备树的骨架

设备树里的每个硬件单元都是一个节点。节点用花括号 { } 包裹,可以嵌套。根节点是 /,所有设备都挂在它下面。

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

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

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

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };
};

看到没?cpus 是根节点下的子节点,cpu@0 又是 cpus 的子节点。节点名后面可以跟 @地址,用来区分同类型设备。比如两个串口,可以写成 uart@44e09000 和 uart@48022000。

我的习惯:节点名尽量用通用名(如 serial、mmc、i2c),不要用芯片型号。这样设备树复用性更好。

2.2 属性(property)—— 节点的“身份证”

属性就是键值对,描述节点的特征。常见的属性类型有四种:

属性类型 写法示例 说明
字符串 compatible = "ti,am335x-bone"; 用于匹配驱动,格式为"厂商,型号"
32位整数 reg = <0x80000000 0x20000000>; 地址或长度,尖括号包裹
64位整数 clock-frequency = <0x0 0x5f5e100>; 两个32位拼一个64位
布尔值 status = "disabled"; 用字符串"okay"或"disabled"表示

这里有个坑,我踩过好几次:reg 属性的地址和大小是配合 #address-cells 和 #size-cells 来解析的。如果父节点设了 #address-cells = <2>,那 reg 里的地址就得写两个32位数。

我曾经犯过的错:在写 SPI 控制器节点时,忘了检查父节点的 #address-cells,结果 reg 地址解析错了,SPI 设备死活不工作。查了两天才发现是地址位数不对。

2.3 标签(label)—— 给节点起个“外号”

标签就是节点的别名,方便在其他地方引用。语法很简单:在节点名前加个名字,后面跟冒号。

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

这里的 &uart0 就是引用。标签定义时写在节点前面:

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

有了标签,你就可以在别的 .dtsi 文件里通过 &uart0 来追加属性。这个特性在板级适配时特别有用——你不需要改原始 .dtsi,只需要在自己的 .dts 里引用标签并修改。

2.4 包含(include)—— 像 C 语言一样复用

设备树用 #include 来引入其他文件。这和 C 语言的预处理指令一模一样,因为设备树编译器(DTC)确实会先经过 C 预处理器。

#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/am33xx.h>

用双引号 "" 包含本地文件,用尖括号 <> 包含系统路径下的头文件。这些头文件里定义了很多宏,比如 GPIO_ACTIVE_HIGH、PIN_OUTPUT 之类的常量。

项目经验:我在做 i.MX6 平台时,发现不同板子共用同一个 SoC 的 .dtsi,但外设引脚不同。我的做法是:SoC 级 .dtsi 只定义控制器节点,板级 .dts 通过 #include 引入后,再用 &标签 覆盖引脚配置。这样 SoC 级文件完全不用动。

2.5 宏定义 —— 让设备树更“可读”

设备树里可以用宏,因为 DTC 会调用 C 预处理器。常见的用法是定义引脚功能、中断类型、GPIO 标志等。

// 在 am33xx.h 中定义
#define PIN_OUTPUT    0x7
#define PIN_INPUT     0x27
#define MUX_MODE0     0
#define MUX_MODE1     1

// 在 .dts 中使用
uart0_pins: pinmux_uart0_pins {
    pinctrl-single,pins = <
        AM33XX_IOPAD(0x970, PIN_OUTPUT | MUX_MODE0)  /* uart0_txd */
        AM33XX_IOPAD(0x974, PIN_INPUT  | MUX_MODE0)  /* uart0_rxd */
    >;
};

你看,PIN_OUTPUT | MUX_MODE0 这种写法比直接写 0x7 好理解多了。宏定义通常放在 include/dt-bindings/ 目录下,按功能分类:

  • gpio/gpio.h —— GPIO 标志(高有效、低有效、开漏等)
  • interrupt-controller/irq.h —— 中断类型(上升沿、下降沿、电平触发)
  • pinctrl/ —— 各平台的引脚复用定义
  • clock/ —— 时钟相关常量
我建议:不要自己造宏。优先使用内核已经定义好的 dt-bindings 头文件。如果确实需要自定义,放在板级 .dts 文件头部,加个注释说明用途。

2.6 知识结构总览

下面这张图把设备树语法的核心要素串起来了。你可以把它当作“语法速查表”。

设备树基础语法知识结构 根节点 / cpus 节点 memory 节点 soc 节点 属性示例 compatible = "arm,cortex-a8"; reg = <0x0>; 属性示例 device_type = "memory"; reg = <0x80000000 0x20000000>; 属性示例 #address-cells = <1>; #size-cells = <1>; 标签(label) uart0: serial@44e09000 引用(&label) &uart0 { status = "okay"; }; 包含(include) #include "am33xx.dtsi" 宏定义(#define) #define PIN_OUTPUT 0x7 → 在 dt-bindings 中定义,提升可读性

2.7 实战:一个完整的 .dts 文件长什么样

我们把今天学的知识点串起来,写一个完整的设备树文件。假设我们有一块基于 Allwinner H3 的开发板:

// SPDX-License-Identifier: GPL-2.0
/dts-v1/;

#include "sun8i-h3.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "MyCustom H3 Board";
    compatible = "mycompany,h3-board", "allwinner,sun8i-h3";

    aliases {
        serial0 = &uart0;
    };

    chosen {
        stdout-path = "serial0:115200n8";
    };

    leds {
        compatible = "gpio-leds";

        status_led {
            label = "status";
            gpios = <&pio 0 10 GPIO_ACTIVE_HIGH>;  /* PA10 */
            linux,default-trigger = "heartbeat";
        };
    };
};

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

&mmc0 {
    pinctrl-names = "default";
    pinctrl-0 = <&mmc0_pins>;
    vmmc-supply = <&reg_vcc3v3>;
    cd-gpios = <&pio 5 6 GPIO_ACTIVE_LOW>;  /* PF6 */
    status = "okay";
};

这个文件里包含了:根节点、子节点、标签引用、宏定义、包含指令。你可以看到,gpios 属性里用了 GPIO_ACTIVE_HIGH 宏,stdout-path 引用了 serial0 别名。这些都是实际项目中每天都在用的写法。

注意:设备树里不要写中文注释!DTC 对编码支持不太好,中文注释可能导致编译失败。老老实实用英文注释吧。

2.8 避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 逗号与分号别搞混:属性值后面用分号 ;,多个值用逗号 , 分隔。我见过有人把 compatible 里的逗号写成冒号,结果内核匹配不到驱动。
  • 标签名不能重复:整个设备树里标签必须唯一。如果你在两个 .dtsi 里定义了同名的标签,编译时会报错。
  • #include 路径问题:用 #include "file.dtsi" 时,DTC 会在当前目录和 -I 指定的路径里找。建议把公共 .dtsi 放在 arch/arm/boot/dts/ 下,用相对路径引用。
  • 宏定义只在预处理阶段生效:如果你在 .dts 里 #define 了一个宏,它只在当前文件有效。想全局共享,放到 dt-bindings 头文件里。