2. 设备树语法:节点、属性、标签、包含文件、宏定义
设备树(Device Tree)说白了就是一块板子的“硬件说明书”。它告诉内核:你有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么配。我刚开始接触时也觉得这东西玄乎,后来发现它其实就是一套树形结构——根节点是树干,子节点是树枝,属性就是树叶上的信息。
这一节我们就把设备树的几个核心语法掰开揉碎。你只要掌握了节点、属性、标签、包含文件和宏定义,写设备树就跟填表格一样简单。
2.1 节点(Node)—— 树的骨架
设备树里的每个硬件单元都是一个节点。根节点是 /,下面挂 CPU、内存、总线、外设。每个节点可以嵌套子节点,形成层级关系。
/ {
compatible = "acme,coyote's-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
节点命名有讲究:节点名@地址。地址部分通常写寄存器基址或单元编号。比如 cpu@0 表示第0个CPU核,memory@80000000 表示基址在0x80000000的内存。
2.2 属性(Property)—— 节点的血肉
属性就是节点的具体信息。常见的属性类型有四种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "arm,cortex-a9"; |
用于匹配驱动,格式为"厂商,型号" |
| 32位整数 | reg = <0x10000000 0x1000>; |
地址和长度,用尖括号包裹 |
| 布尔值 | status = "disabled"; |
用字符串"okay"或"disabled"表示 |
| 数组 | interrupts = <0 31 4>; |
多个值用空格分隔 |
最关键的属性是 compatible。内核驱动就是靠它来匹配设备的。我见过有人把厂商名写错,结果驱动死活加载不上——查了两天才发现是拼写问题。
2.3 标签(Label)—— 给节点起个外号
标签就是节点的别名。用 标签名: 节点名@地址 的格式定义。有了标签,其他地方引用这个节点就方便多了。
uart1: serial@10001000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x10001000 0x1000>;
interrupts = <0 26 4>;
status = "disabled";
};
&uart1 {
status = "okay";
pinctrl-0 = <&pinctrl_uart1>;
};
你看,先定义 uart1 标签,后面用 &uart1 引用它来修改属性。这种写法在设备树里非常常见,尤其是做板级适配时。
2.4 包含文件(Include)—— 代码复用
设备树支持 #include 预处理指令,跟C语言一样。通常用来引入芯片级 .dtsi 文件、头文件或者宏定义。
// 在 .dts 文件中
#include "imx6q.dtsi"
#include "imx6qdl.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
.dtsi 文件是芯片厂商提供的“公版”设备树,定义了芯片内部所有外设的默认配置。板级 .dts 文件通过 #include 引入它,然后只修改需要差异化的部分。
我记得第一次做项目时,厂商给了个 .dtsi 文件,里面定义了上百个节点。我直接在 .dts 里重新写了一遍——后来被导师骂了一顿。正确的做法是:#include 进来,然后用 &标签 覆盖修改。
2.5 宏定义(Macro)—— 让数字变得可读
设备树里经常出现一堆数字,比如中断号、GPIO号、时钟频率。直接用数字写,别人看不懂,你自己过两周也忘了。宏定义就是用来解决这个问题的。
// 在头文件中定义
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_LEVEL_HIGH 4
// 在设备树中使用
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
led-red {
compatible = "gpio-leds";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
interrupt-parent = <&intc>;
interrupts = <0 31 IRQ_TYPE_LEVEL_HIGH>;
};
你看,GPIO_ACTIVE_LOW 比直接写 1 好理解多了。内核在 dt-bindings/ 目录下提供了大量现成的宏定义,直接用就行。
2.6 知识结构总览
下面这张图把设备树语法的核心要素串起来了。你可以把它当作速查表:
2.7 实战:写一个完整的设备树片段
把上面这些知识点串起来,写一个实际的例子。假设我们要描述一个I2C控制器,上面挂了一个温度传感器:
// 引入芯片级定义和宏
#include "imx6q.dtsi"
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/gpio.h>
/ {
model = "Acme i.MX6Q Board";
compatible = "acme,imx6q-board", "fsl,imx6q";
// I2C控制器节点,带标签
i2c1: i2c@021a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_I2C1>;
status = "okay";
clock-frequency = <100000>;
// 子节点:温度传感器
temp_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
status = "okay";
};
};
};
// 板级覆盖:使能I2C1
&i2c1 {
status = "okay";
};
这个例子包含了:根节点、子节点、标签、属性、包含文件、宏定义。你仔细看,IRQ_TYPE_LEVEL_HIGH 是宏,IMX6QDL_CLK_I2C1 也是宏,&i2c1 是标签引用,#include 引入了 .dtsi 和头文件。
2.8 常见错误与避坑
- compatible 拼写错误——驱动匹配不上,设备不工作。我曾经把 "fsl,imx6q-uart" 写成 "fsl,imx6q-uart1",多了一个数字,查了两天。
- reg 地址写错——外设寄存器基址不对,内核访问时直接挂死。一定要跟芯片手册核对。
- 中断号对不上——中断控制器编号从0开始还是从1开始?不同芯片不一样。我建议先看内核里其他板子的写法。
- 标签重复——同一个标签名不能出现两次。编译时会有警告,但不会报错,运行时行为不可预期。
- 忘记 #include——宏定义没引入,编译报错。这个最基础,但新手经常犯。
make dtbs 或者 dtc -I dts -O dtb -o xxx.dtb xxx.dts。如果报错,先检查语法。我习惯用 dtc -@ 开启 overlay 支持,方便后续扩展。
嗯,设备树语法其实不复杂。你只要记住:节点是骨架,属性是血肉,标签是别名,包含文件是复用,宏定义是让数字可读。把这五样东西用好,写设备树就跟搭积木一样。