第2章 设备树语法入门:节点、属性、标签、包含文件
好,咱们今天聊聊设备树最基础的东西。很多刚入行的朋友觉得设备树就是一堆看不懂的文本,其实没那么玄乎。说白了,它就是描述硬件的一张「地图」——告诉内核,你的板子上有哪些外设、它们怎么连接的、地址是多少。
我个人习惯把设备树比作「硬件的简历」。每个外设就是一份简历,上面写着它的名字、地址、中断号、时钟频率等等。内核拿到这份简历,就知道怎么跟它打交道了。
2.1 节点(Node)—— 硬件的「身份证」
节点是设备树最基本的单元。每个节点代表一个硬件设备,或者一个总线、一个控制器。节点用一对花括号 { } 包裹,里面可以放属性和子节点。
来看个最简单的例子:
/ {
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
这里 serial@10000000 就是一个节点。节点名由两部分组成:serial 是设备类型,10000000 是它的基地址。嗯,这里要注意——节点名里的地址只是给人看的,真正起作用的还是 reg 属性里的值。
2.2 属性(Property)—— 硬件的「详细信息」
属性就是节点的具体参数。常见的属性类型有:
| 属性类型 | 格式 | 示例 | 说明 |
|---|---|---|---|
| 字符串 | "string" | compatible = "ns16550"; | 用于驱动匹配 |
| 32位整数 | <value> | reg = <0x10000000 0x1000>; | 地址、大小等 |
| 64位整数 | <0x00000000 0x10000000> | reg = <0x00000000 0x10000000 0x0 0x1000>; | 大地址空间 |
| 布尔值 | 无值 | status = "disabled"; | 存在即为真 |
| 字节数组 | [value ...] | local-mac-address = [00 11 22 33 44 55]; | MAC地址等 |
最关键的属性是 compatible。它就像设备的「身份证号」,内核通过它找到对应的驱动。格式一般是「厂商,设备型号」,比如 "ti,am335x-uart"、"fsl,imx6q-uart"。
2.3 标签(Label)—— 给节点起个「外号」
标签就是节点的别名。你看上面例子里的 uart0:,它就是标签。有了标签,其他地方引用这个节点就方便多了。
为什么要用标签?举个例子:
&uart0 {
status = "okay";
clock-frequency = <115200>;
};
这里 &uart0 就是引用了上面那个串口节点。如果没有标签,你就得写完整的路径 &{/serial@10000000},又长又容易写错。
我个人习惯给所有重要的节点都加上标签。比如:
uart0、uart1给串口i2c0、i2c1给 I2C 总线mmc0、mmc1给 SD/MMC 控制器eth0、eth1给以太网
标签命名也有讲究。我建议用「设备类型+序号」的格式,这样一看就知道是什么设备。别用 my_device_1 这种名字,三个月后你自己都看不懂。
&uart0 最终会变成 /serial@10000000。这个替换是在编译阶段完成的,不影响运行效率。
2.4 包含文件(Include)—— 代码复用的利器
设备树也支持 #include 预处理指令。这个跟 C 语言的 #include 是一个道理——把别的文件的内容包含进来。
最常见的用法是包含 SoC 的通用设备树文件:
#include "imx6q.dtsi"
/ {
model = "My Custom Board";
compatible = "my,imx6q-board", "fsl,imx6q";
};
.dtsi 文件是 SoC 厂商提供的通用描述文件,里面定义了芯片内部的所有外设。你只需要在自己的板级文件(.dts)里引用它,然后修改或启用你需要的外设就行了。
这样做的好处很明显:
- 不用重复写 SoC 的通用配置
- SoC 升级时只需要更新
.dtsi文件 - 不同板子可以共享同一个
.dtsi
除了 .dtsi,还可以包含头文件(.h)。头文件里通常定义了一些宏,比如中断号、时钟索引等:
#include "imx6q-pinfunc.h"
#include "irq.h"
&uart0 {
pinctrl-0 = <&pinctrl_uart0>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
};
这里 GIC_SPI 和 IRQ_TYPE_LEVEL_HIGH 就是头文件里定义的宏。用宏的好处是代码可读性更强,而且不容易写错数字。
.dts 文件里,把 SoC 通用的配置放在 .dtsi 文件里。如果某个外设的配置特别复杂,我还会单独建一个 .dtsi 文件来包含它。比如显示控制器(Display Controller)的配置通常很复杂,我就单独放一个 display.dtsi,然后在主文件里 #include 它。
2.5 实战:一个完整的设备树片段
咱们把上面学的东西串起来,写一个完整的例子:
#include "imx6q.dtsi"
#include "imx6q-pinfunc.h"
/ {
model = "My i.MX6Q Board";
compatible = "my,imx6q-board", "fsl,imx6q";
memory@10000000 {
device_type = "memory";
reg = <0x10000000 0x40000000>;
};
leds {
compatible = "gpio-leds";
led0: led@0 {
label = "heartbeat";
gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
&uart0 {
status = "okay";
pinctrl-0 = <&pinctrl_uart0>;
};
&i2c0 {
status = "okay";
clock-frequency = <100000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
这个例子包含了:
- 根节点
/,定义了板子型号和兼容性 - 内存节点,告诉内核内存的起始地址和大小
- LED 节点,用 GPIO 控制心跳灯
- 通过标签引用
uart0和i2c0,启用它们并配置引脚 - 在 I2C 总线下挂载了一个 EEPROM 子节点
reg 属性含义跟父节点有关。比如 I2C 设备子节点的 reg 表示设备地址(7位地址),而内存节点的 reg 表示地址范围。这个区别很容易搞混,我刚开始学的时候就犯过这个错——把 I2C 设备的地址写成了 32 位,结果内核报地址越界。
2.6 避坑总结
最后,我把自己这些年踩过的坑总结一下:
- 节点名不要用特殊字符——只能用字母、数字、连字符和下划线。我见过有人用空格,编译直接报错。
- compatible 字符串要跟驱动完全一致——大小写、下划线都不能错。最好直接从驱动源码里复制。
- 标签不要重复——整个设备树里标签必须唯一。如果两个节点用了同一个标签,编译会报错。
- 包含文件路径要正确——
#include的搜索路径跟编译器有关。建议用相对路径,或者把.dtsi文件放在内核源码的arch/arm/boot/dts/目录下。 - 别忘了
status = "okay";——很多 SoC 的.dtsi文件里默认把外设status = "disabled";,你需要在自己板级文件里显式启用它。
嗯,设备树的基础语法就这些。说白了就是四个东西:节点描述硬件、属性描述参数、标签方便引用、包含文件实现复用。掌握了这些,你就能看懂大部分设备树文件了。下一章咱们聊聊更进阶的内容——如何调试设备树,当内核启动时设备树加载失败该怎么办。