2、设备树基础:DTS语法、节点与属性、reg与interrupts属性详解
设备树(Device Tree)是什么?说白了,它就是一块「硬件描述板」。Linux 内核通过它来知道板子上接了哪些外设、地址是多少、中断怎么连。没有设备树之前,内核里全是板级文件,改个 GPIO 都要重新编译内核,那叫一个痛苦。
我个人习惯把设备树比作硬件的「身份证」。内核启动时读这张身份证,就知道该给谁装驱动、该给谁分配资源。今天我们就来拆解这张身份证的写法。
核心认知:设备树是描述硬件的结构化文本,不是代码。它不执行逻辑,只提供数据。内核解析它,然后根据数据初始化驱动。
2.1 DTS 文件长什么样?
DTS(Device Tree Source)是设备树的源文件。编译后变成 DTB(Device Tree Blob),内核才能吃。我们先看一个最简单的例子:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
leds {
compatible = "gpio-leds";
led0 {
label = "beaglebone:green:usr0";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
嗯,这里要注意几个点:
/dts-v1/;是版本声明,必须写在第一行。/是根节点,整棵树从这里开始。- 每个节点用花括号
{ }包裹,最后用分号结束。 - 属性就是
key = value;的形式。
我在项目中遇到过有人把分号漏了,编译报错找了半天。DTC 编译器报错信息有时候不太友好,养成检查分号的好习惯能省很多时间。
2.2 节点与属性:树的骨架
设备树的每个节点代表一个硬件设备或总线。节点可以嵌套,形成树形结构。节点名一般写成 名称@地址 的形式,比如 uart@44e09000。地址部分不是必须的,但如果有 reg 属性,建议加上,方便阅读。
属性就是节点的「特征描述」。常见的属性类型有:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | model = "TI AM335x"; |
用双引号包裹 |
| 32位整数 | reg = <0x80000000 0x10000000>; |
尖括号内,空格分隔 |
| 64位整数 | reg = <0x00000000 0x80000000 0x00000000 0x10000000>; |
两个 32 位拼一个 64 位 |
| 布尔值 | status = "disabled"; |
用字符串模拟,常见 "okay" 和 "disabled" |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号内,十六进制 |
| 字符串列表 | compatible = "ti,am335x-bone", "ti,am335x"; |
逗号分隔多个字符串 |
| 引用(phandle) | &gpio1 |
& 符号引用其他节点 |
你想想看,为什么 compatible 属性要写多个值?因为内核匹配驱动时,会从左到右尝试。第一个最精确,后面的作为 fallback。我调试过一块板子,驱动没加载,最后发现是 compatible 字符串写反了顺序,内核匹配到了错误的驱动。
个人经验:写 compatible 时,遵循「厂商,型号」的命名规范。比如 "ti,am335x-uart"。这样既避免冲突,也方便别人一眼看出是谁家的设备。
2.3 reg 属性:地址与大小
reg 属性描述设备的地址空间。格式是 reg = <地址1 大小1 地址2 大小2 ...>;。每个地址-大小对称为一个「region」。对于 32 位系统,一个 region 占两个 32 位整数;对于 64 位系统,占四个。
举个例子:
uart0: serial@44e09000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这里 reg 表示 UART 模块的寄存器基地址是 0x44e09000,地址范围是 0x2000 字节。内核驱动通过 platform_get_resource() 拿到这个地址,然后 ioremap() 映射到虚拟地址空间。
我曾经在移植驱动时,把 reg 的大小写错了。本来寄存器只占 0x1000,我写了 0x10000。结果 ioremap 映射了过大的空间,导致后续其他设备映射失败。这种 bug 很难查,因为系统不会立刻崩溃,而是等到访问其他外设时才出问题。
对于 SPI 或 I2C 设备,reg 可能表示片选号或设备地址:
&spi0 {
status = "okay";
spidev@0 {
compatible = "spidev";
reg = <0>; /* 片选 CS0 */
spi-max-frequency = <1000000>;
};
};
这里 reg = <0> 表示该设备挂在 SPI 总线的片选 0 上。内核 SPI 子系统会解析这个值来配置片选信号。
2.4 interrupts 属性:中断连接
中断属性描述设备如何向 CPU 发中断。格式取决于中断控制器。最常见的是 interrupts = <中断号 触发类型>;。
触发类型常用的宏定义:
| 宏 | 值 | 含义 |
|---|---|---|
IRQ_TYPE_EDGE_RISING |
1 | 上升沿触发 |
IRQ_TYPE_EDGE_FALLING |
2 | 下降沿触发 |
IRQ_TYPE_EDGE_BOTH |
3 | 双边沿触发 |
IRQ_TYPE_LEVEL_HIGH |
4 | 高电平触发 |
IRQ_TYPE_LEVEL_LOW |
8 | 低电平触发 |
看一个实际例子:
gpio0: gpio@44e07000 {
compatible = "ti,omap4-gpio";
reg = <0x44e07000 0x1000>;
interrupts = <96 IRQ_TYPE_LEVEL_HIGH>;
#gpio-cells = <2>;
gpio-controller;
#interrupt-cells = <2>;
interrupt-controller;
};
这里 GPIO 控制器本身也是一个中断控制器。它用 interrupts 告诉 CPU:我自己的中断线是 96 号,高电平触发。同时它声明了 interrupt-controller 和 #interrupt-cells = <2>,表示它可以接收其他设备的中断,每个中断描述需要 2 个 cell(通常是 GPIO 引脚号和触发类型)。
那么其他设备怎么用这个 GPIO 的中断呢?
button@0 {
compatible = "gpio-keys";
label = "user_button";
gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
interrupts-extended = <&gpio0 7 IRQ_TYPE_EDGE_FALLING>;
};
这里 interrupts-extended 明确指定了中断控制器是 &gpio0,引脚是 7,下降沿触发。内核会把这个中断映射到 GPIO 控制器的中断处理链上。
避坑指南:我曾经在项目中把 interrupts 和 interrupts-extended 混用。如果一个设备有多个中断源,或者中断控制器不是父节点,必须用 interrupts-extended。否则内核解析时会出错,中断根本注册不上。
2.5 设备树的核心结构图
下面这张图展示了设备树从源文件到内核使用的完整链路,以及节点、属性、reg、interrupts 之间的关系。
从图中可以看到,设备树的根节点下挂载了多个子节点,每个子节点携带自己的属性。reg 描述地址资源,interrupts 描述中断连接。最终 DTS 通过 DTC 编译器生成 DTB,内核在启动时解析 DTB,匹配驱动并初始化硬件。
2.6 常用技巧与避坑
- 引用节点:用
&节点标签的方式引用其他节点。比如&uart0可以在任何地方修改 uart0 的属性。我习惯把板级特有的修改放在.dts文件里,SoC 通用定义放在.dtsi里,这样复用性高。 - 覆盖属性:同一个属性在
.dtsi和.dts中都出现时,后面的覆盖前面的。利用这个特性,可以在.dts中只修改需要改的部分。 - status 属性:默认设为
"disabled",在板级文件中改为"okay"。这样 SoC 的.dtsi可以通用,每个板子按需开启外设。 - 编译检查:用
dtc -I dts -O dtb -o output.dtb input.dts编译。加上-@选项可以生成符号表,方便调试。
调试建议:内核启动时加上 earlycon 和 dtoc 参数,可以打印设备树解析过程。如果某个设备没注册,先看内核日志里有没有 "not found" 或 "failed to map" 之类的信息。我遇到过很多次,问题出在 reg 地址写错了,或者中断号对不上芯片手册。
设备树看起来语法简单,但实际项目中坑不少。多读芯片厂商提供的 .dtsi 文件,那是最好的学习材料。把 reg 和 interrupts 这两个属性吃透,设备树就算入门了。