2、设备树语法入门:节点、属性、标签、引用、包含文件的基本语法

设备树(Device Tree)说白了就是一块「硬件描述板」。你告诉内核:我板子上有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么接。内核拿到这个描述,就能自动把驱动绑上去。

我刚开始接触设备树时,觉得它跟 XML 差不多——一堆尖括号、花括号。但用久了你会发现,它的设计其实很精巧。今天我就带你把这套语法拆开揉碎,一个一个过。

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

设备树里的一切都是节点。一个节点代表一个设备、一个总线桥、或者一个芯片内部的功能模块。

节点的写法很简单:

/dts-v1/;

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard-v1", "vendor,myboard";

    cpu@0 {
        device_type = "cpu";
        reg = <0>;
    };

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

根节点是 /,所有设备都挂在它下面。每个节点用 节点名@地址 来标识——@ 后面跟的是寄存器基地址或单元号。比如 cpu@0 表示第 0 个 CPU 核,memory@80000000 表示起始地址为 0x80000000 的内存。

我的习惯:节点名尽量跟芯片手册保持一致。比如 I2C 控制器就叫 i2c@地址,UART 就叫 serial@地址。这样别人看你的 dts 文件,一眼就知道对应哪个外设。

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

属性就是键值对,告诉内核这个设备「长什么样」。常用的属性类型有几种:

属性类型 写法示例 说明
字符串 compatible = "vendor,device"; 驱动匹配的关键字
32 位整数 reg = <0x1000 0x100>; 地址 + 长度,用尖括号包起来
64 位整数 reg = <0x0 0x80000000 0x0 0x40000000>; 两个 32 位拼一个 64 位
布尔值 status = "disabled"; 用字符串模拟布尔值
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号包十六进制字节
字符串列表 compatible = "a", "b", "c"; 逗号分隔多个字符串

这里最核心的属性是 compatible。内核驱动通过它来匹配设备。我见过不少新手把 compatible 写错一个字母,结果驱动死活不 probe——嗯,这种坑我踩过不止一次。

注意:compatible 的命名规范是 "厂商,设备型号"。比如 "ti,am335x-uart"。不要随便起名字,否则内核找不到对应的驱动。

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

节点名有时候很长,比如 serial@44e09000。每次引用都要写全名,太累。标签就是给节点起个短外号。

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

这里的 uart0 就是标签。后面你想引用这个节点,直接写 &uart0 就行。

我个人习惯给所有关键外设都加上标签:uart0、i2c1、mmc0、usb0。这样在板级 dts 文件里覆盖或引用时,代码干净很多。

2.4 引用(Reference)——连接节点与节点

引用用 &标签 表示。它有两种常见用法:

  1. 追加属性:在另一个文件里给已有节点加东西
  2. 引用 phandle:一个节点指向另一个节点

先看追加属性的例子:

/* 在 soc.dtsi 中定义 */
uart0: serial@44e09000 {
    compatible = "ti,am335x-uart";
    status = "disabled";
};

/* 在 board.dts 中启用 */
&uart0 {
    status = "okay";
    clock-frequency = <48000000>;
};

这种写法在 Buildroot 的 Linux 内核里非常常见。SoC 厂商提供 .dtsi 文件,板级 .dts 文件通过 &标签 来覆盖或补充属性。

再看 phandle 引用:

gpio0: gpio@44e07000 {
    compatible = "ti,am335x-gpio";
    #gpio-cells = <2>;
    gpio-controller;
};

led {
    compatible = "gpio-leds";
    led-red {
        gpios = <&gpio0 23 GPIO_ACTIVE_LOW>;
    };
};

这里 &gpio0 就是 phandle 引用。它告诉内核:这个 LED 用的是 gpio0 控制器,第 23 号引脚。

关键区别:

  • &uart0 { ... }; —— 追加/覆盖属性,用在 dts 文件里
  • &gpio0 —— 作为值嵌入到另一个属性中,用在属性值里

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

设备树支持 #include 预处理指令。它跟 C 语言的 #include 一样,由 DTC(Device Tree Compiler)的预处理器处理。

#include "soc.dtsi"
#include "pinmux.dtsi"
#include <dt-bindings/gpio/gpio.h>

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard-v1", "vendor,myboard";
};

尖括号 <> 表示搜索系统头文件路径,双引号 "" 表示搜索当前目录。这个规则跟 C 编译器一模一样。

我在项目中通常这样组织文件:

文件 内容 谁维护
soc.dtsi SoC 内部外设定义(UART、I2C、SPI、GPIO 等) 芯片厂商
pinmux.dtsi 引脚复用配置 硬件工程师
board.dts 板级特有设备(LED、按键、外部外设) BSP 工程师
board-common.dtsi 同一系列板卡的公共配置 BSP 工程师

这样分层之后,换一个板子只需要改 board.dts,SoC 部分完全不用动。我在做 AM335x 系列板卡时,三个板子共用同一个 soc.dtsi,只换了 board.dts 和 pinmux.dtsi,维护成本低了很多。

2.6 知识体系总览

下面这张图把设备树语法的核心关系串起来了。你看一遍应该就能记住:

设备树语法知识体系 设备树语法体系 节点 (Node) 属性 (Property) 标签 (Label) 根节点 / 子节点 cpu@0 子节点 memory@80000000 compatible = "vendor,device" reg = <0x1000 0x100> interrupts = <72> uart0: serial@44e09000 i2c1: i2c@4802a000 引用 &标签 包含 #include 核心流程 节点定义设备 → 属性描述特征 → 标签方便引用 引用实现覆盖/连接 → 包含实现模块化

2.7 避坑指南

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

  • 标签不能重复:同一个 dts 编译单元里,标签名必须唯一。我曾经在两个 .dtsi 文件里都定义了 uart0,编译报错找了半天。
  • #include 路径问题:Buildroot 编译时,#include <> 的搜索路径是内核头文件路径 + 设备树目录。如果找不到文件,检查一下 ARCH 和 SRCARCH 环境变量。
  • 引用覆盖要小心:用 &uart0 { ... }; 追加属性时,如果原节点没有这个属性,它是新增;如果有,它是覆盖。我见过有人想追加 pinctrl-0,结果把原来的覆盖掉了,导致引脚配置丢失。
  • 分号不能丢:每个属性后面都要加分号,每个节点后面也要加分号。漏一个分号,DTC 报错能让你看到怀疑人生。