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@地址,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)——连接节点与节点
引用用 &标签 表示。它有两种常见用法:
- 追加属性:在另一个文件里给已有节点加东西
- 引用 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 知识体系总览
下面这张图把设备树语法的核心关系串起来了。你看一遍应该就能记住:
2.7 避坑指南
最后分享几个我踩过的坑:
- 标签不能重复:同一个 dts 编译单元里,标签名必须唯一。我曾经在两个
.dtsi文件里都定义了uart0,编译报错找了半天。 #include路径问题:Buildroot 编译时,#include <>的搜索路径是内核头文件路径 + 设备树目录。如果找不到文件,检查一下ARCH和SRCARCH环境变量。- 引用覆盖要小心:用
&uart0 { ... };追加属性时,如果原节点没有这个属性,它是新增;如果有,它是覆盖。我见过有人想追加pinctrl-0,结果把原来的覆盖掉了,导致引脚配置丢失。 - 分号不能丢:每个属性后面都要加分号,每个节点后面也要加分号。漏一个分号,DTC 报错能让你看到怀疑人生。