3、设备树(DTS)配置的坑:节点命名冲突、属性类型错误、中断号映射偏差
设备树,圈里人常叫 DTS。说白了,它就是一块硬件的「身份证」加「说明书」。内核启动时靠它来认识板子上接了哪些外设、怎么访问、中断怎么走。
但问题也出在这儿——这玩意儿是文本文件,编译器只检查语法,不检查语义。你写错了,它照样编译通过,但驱动跑起来就各种诡异。我这些年踩过的 DTS 坑,少说也有十几个。今天挑三个最典型的聊聊。
核心观点:DTS 配置错误不会导致编译失败,但会导致驱动行为异常。调试这类问题,往往比改代码本身更耗时。
3.1 节点命名冲突:同名不同爹,内核认谁?
先看一个我实际遇到过的场景。某款 SoC 有两个 SPI 控制器,一个接 Flash,一个接传感器。DTS 里我写了两个节点:
spi@ff110000 {
compatible = "vendor,spi-v1";
reg = <0x0 0xff110000 0x0 0x1000>;
status = "okay";
};
spi@ff120000 {
compatible = "vendor,spi-v1";
reg = <0x0 0xff120000 0x0 0x1000>;
status = "okay";
};
看起来没问题对吧?但问题出在——我同事在另一个 .dtsi 文件里也定义了一个 spi@ff110000,而且 status = "disabled"。两个文件被 #include 到一起后,内核的覆盖规则是:后解析的节点覆盖先解析的。结果我的 Flash 驱动死活 probe 不上。
为什么会这样?因为 DTS 编译时,同名节点(相同 reg 地址)会被合并,但属性值以最后一次赋值为准。你想想看,这种问题查起来多恶心——代码逻辑全对,就是设备树打架。
我的习惯:每个板级 .dts 文件开头加一段注释,列出所有被 #include 的 .dtsi 文件,并标注其中定义的节点地址范围。这样一眼就能看出有没有冲突。
避坑指南:
- 使用
&spi1这种标签引用方式,而不是直接写全路径节点名。标签引用不会产生覆盖歧义。 - 同一个 SoC 系列内,不同型号的
.dtsi文件不要同时#include。 - 编译后用
dtc -I dtb -O dts反编译生成的.dtb,检查最终合并结果。
3.2 属性类型错误:你以为你写对了,其实没有
DTS 的属性类型就那么几种:u32、u64、string、phandle、bytes。但就是这几种,我见过无数人写错。
举个例子,reg 属性。很多人以为 reg = <0xff110000 0x1000> 就完事了。但如果你用的 SoC 是 64 位地址总线,reg 的每个条目应该是 <地址高位 地址低位 长度> 或者 <地址 长度> 但地址是 64 位。写错了,驱动拿到的地址就是错的。
我记得有一次调试一个 PCIe 控制器,驱动总是访问到错误的寄存器地址。查了两天,最后发现是 reg 属性里少写了一个 0x0 占位符。就这一个字节的差距,整个控制器无法初始化。
| 属性 | 常见错误写法 | 正确写法 | 后果 |
|---|---|---|---|
reg |
<0xff110000 0x1000> |
<0x0 0xff110000 0x0 0x1000> |
地址解析错误,驱动访问到错误内存 |
interrupts |
<0 31 4> |
<GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH> |
中断号或触发类型错误 |
clocks |
<&clk 0> |
<&clk CLK_UART0> |
时钟索引错误,外设无法使能 |
gpios |
<&gpio0 7 0> |
<&gpio0 7 GPIO_ACTIVE_HIGH> |
GPIO 电平极性错误 |
警告:不要相信「看起来差不多」。DTS 编译器不会检查属性值的语义正确性。你写 interrupts = <0 999 4>,它照样编译通过,但内核会直接 panic。
我的建议是:写 DTS 时,优先使用头文件里定义的宏,而不是直接写数字。比如 GIC_SPI、IRQ_TYPE_LEVEL_HIGH 这些宏,既提高可读性,又避免手误。
3.3 中断号映射偏差:GIC 的坑,一个比一个深
中断号映射,这是 DTS 配置里最容易出问题的地方,没有之一。
ARM 的 GIC(通用中断控制器)有两种中断类型:PPI(私有外设中断,每个核独享)和 SPI(共享外设中断,所有核共享)。DTS 里写 interrupts = <0 31 4>,第一个数字 0 表示 SPI,31 是中断号,4 是触发类型。
但问题来了——这个 31 是硬件中断号还是软件中断号?不同厂商的 GIC 实现,偏移量可能不一样。有些厂商把 SPI 的起始号定为 32,有些定为 16。你按参考手册写的 31,实际硬件上可能是 47 或者 15。
我曾经接手过一个项目,触摸屏中断一直不触发。驱动层查了个遍,逻辑完全正确。最后拿示波器量了物理引脚,发现中断确实产生了。再查 GIC 寄存器,发现中断号被映射到了 63,而 DTS 里写的是 47。差了 16 个号——因为厂商的 GIC 实现里,SPI 起始号是 48,不是 32。
调试技巧:在 /proc/interrupts 里查看实际注册的中断号,跟 DTS 里写的对比。如果对不上,八成是映射偏差。另外,cat /proc/device-tree/<你的节点>/interrupts 可以查看 DTS 解析后的原始值。
避坑指南:
- 拿到新板子,先读
Documentation/devicetree/bindings/interrupt-controller/下对应 GIC 的绑定文档。 - 确认厂商的 SPI 起始偏移量。常见的有 16、32、48 三种。
- 用
devm_request_irq注册中断后,打印返回的中断号,跟硬件手册对比。 - 如果中断不触发,先查 GIC 的
ISPENDR寄存器,看中断是否确实到达了 GIC。
3.4 一张图看懂 DTS 配置的三大坑
下面这张图,我把三个坑点的关系画出来了。你可以把它当成排查 DTS 问题时的路线图。
这张图从左到右,对应了排查时的优先级。我个人习惯是:先确认节点有没有被意外覆盖,再检查属性值的类型和格式,最后才去折腾中断号。因为中断号的问题往往需要看硬件手册甚至示波器,代价最大。
3.5 总结一下
DTS 配置的坑,说白了就是「编译器不帮你把关」。你写错了,它不报错,但内核跑起来就各种妖。
- 节点命名冲突:多用标签引用,少写全路径。编译后反编译检查。
- 属性类型错误:优先用宏,别手写数字。对着绑定文档一行一行对。
- 中断号映射偏差:确认 GIC 的 SPI 起始偏移量。用
/proc/interrupts验证。
嗯,这三个坑,我每个都至少栽过一次。希望你看完这篇,能少走几步弯路。