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。
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位数。
#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 之类的常量。
#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 知识结构总览
下面这张图把设备树语法的核心要素串起来了。你可以把它当作“语法速查表”。
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 = <®_vcc3v3>;
cd-gpios = <&pio 5 6 GPIO_ACTIVE_LOW>; /* PF6 */
status = "okay";
};
这个文件里包含了:根节点、子节点、标签引用、宏定义、包含指令。你可以看到,gpios 属性里用了 GPIO_ACTIVE_HIGH 宏,stdout-path 引用了 serial0 别名。这些都是实际项目中每天都在用的写法。
2.8 避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间:
- 逗号与分号别搞混:属性值后面用分号
;,多个值用逗号,分隔。我见过有人把compatible里的逗号写成冒号,结果内核匹配不到驱动。 - 标签名不能重复:整个设备树里标签必须唯一。如果你在两个 .dtsi 里定义了同名的标签,编译时会报错。
- #include 路径问题:用
#include "file.dtsi"时,DTC 会在当前目录和-I指定的路径里找。建议把公共 .dtsi 放在arch/arm/boot/dts/下,用相对路径引用。 - 宏定义只在预处理阶段生效:如果你在 .dts 里
#define了一个宏,它只在当前文件有效。想全局共享,放到dt-bindings头文件里。