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 问题时的路线图。

DTS 配置三大坑点关系图 DTS 配置三大坑点排查路线图 设备树 DTS 配置 节点命名冲突 属性类型错误 中断号映射偏差 同名节点覆盖 #include 顺序依赖 reg 地址格式错误 interrupts 宏未使用 SPI 起始偏移量未知 GIC 实现差异 反编译 .dtb 检查 对比绑定文档 读 GIC 寄存器 排查顺序:先查命名 → 再查属性 → 最后查中断

这张图从左到右,对应了排查时的优先级。我个人习惯是:先确认节点有没有被意外覆盖,再检查属性值的类型和格式,最后才去折腾中断号。因为中断号的问题往往需要看硬件手册甚至示波器,代价最大。

3.5 总结一下

DTS 配置的坑,说白了就是「编译器不帮你把关」。你写错了,它不报错,但内核跑起来就各种妖。

  • 节点命名冲突:多用标签引用,少写全路径。编译后反编译检查。
  • 属性类型错误:优先用宏,别手写数字。对着绑定文档一行一行对。
  • 中断号映射偏差:确认 GIC 的 SPI 起始偏移量。用 /proc/interrupts 验证。

嗯,这三个坑,我每个都至少栽过一次。希望你看完这篇,能少走几步弯路。