第3章:设备树(DTS)实战:语法详解、绑定文档解读与Jacinto平台避坑指南
设备树,圈里人常叫它DTS。说白了,它就是一块“硬件描述板”。你告诉内核:我这款板子上有哪些外设,挂在哪个地址,中断怎么接。内核拿到这个描述,就能自动把驱动绑上去。
我在Jacinto平台上折腾了十几年,见过太多因为DTS写错导致系统起不来的案例。有些问题藏得很深,光看log根本看不出。这一章,我就把实战中积累的经验摊开来聊。
核心观点:设备树不是配置文件,它是内核与硬件之间的“契约”。写错了,驱动再完美也白搭。
3.1 DTS语法速览——你只需要记住这些
DTS的语法其实不复杂。我习惯把它拆成三块:节点、属性、引用。
- 节点(node):用
{ }包起来,代表一个硬件设备或总线。 - 属性(property):键值对,描述节点的特性。
- 引用(phandle):用
&符号连接其他节点。
举个例子,一个典型的I2C控制器节点长这样:
i2c0: i2c@2000000 {
compatible = "ti,j721e-i2c", "ti,omap4-i2c";
reg = <0x00 0x2000000 0x00 0x100>;
interrupts = <GIC_SPI 200 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&k3_clks 102 0>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
这里有几个关键点:
compatible是驱动匹配的“身份证”,顺序很重要。内核会按列表顺序尝试匹配。reg描述地址空间,#address-cells和#size-cells决定了地址和长度的编码位数。status = "okay"表示启用,"disabled"就是禁用。我见过有人写成"ok",结果设备死活不工作。
我的习惯:写DTS时,先把compatible和reg写对,其他属性可以慢慢补。这两个错了,驱动根本不会probe。
3.2 绑定文档——驱动开发者的“圣经”
绑定文档(Binding Document)是内核社区维护的官方说明。它告诉你每个compatible字符串对应的属性该怎么写。
路径一般在:Documentation/devicetree/bindings/。我每次移植一个新外设,第一件事就是去翻这个目录。
举个例子,TI的McASP(多通道音频串行端口)绑定文档里会写明:
| 属性 | 类型 | 说明 |
|---|---|---|
compatible |
string list | 必须包含"ti,j721e-mcasp" |
reg |
array | 寄存器基地址和长度 |
interrupts |
array | TX/RX中断号 |
tx-num-evt |
u32 | 发送DMA事件数,默认32 |
rx-num-evt |
u32 | 接收DMA事件数,默认32 |
绑定文档里还会给出一个完整示例。我建议你直接复制示例,然后改地址和中断号。这样最不容易出错。
注意:不同内核版本的绑定文档可能有差异。比如Linux 5.10和6.1对某些TI外设的属性要求就不一样。一定要以你实际使用的内核版本为准。
3.3 Jacinto平台上的常见坑——我踩过的那些雷
在Jacinto平台上写DTS,有几个坑特别容易踩。我一个个说。
3.3.1 坑一:interrupt-parent没指定
Jacinto平台使用GIC(通用中断控制器)。每个设备节点必须显式指定interrupt-parent,或者从父节点继承。
我曾经遇到一个案子:GPIO中断一直不触发。查了两天,最后发现子节点忘了写interrupt-parent = &gic500。内核默认用父节点的中断控制器,但父节点是总线,根本没连GIC。
// 错误写法
gpio0: gpio@600000 {
compatible = "ti,j721e-gpio";
reg = <0x00 0x600000 0x00 0x100>;
interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
// 缺少 interrupt-parent
};
// 正确写法
gpio0: gpio@600000 {
compatible = "ti,j721e-gpio";
reg = <0x00 0x600000 0x00 0x100>;
interrupt-parent = <&gic500>;
interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
};
3.3.2 坑二:clocks和assigned-clocks不匹配
Jacinto平台的时钟树很复杂。每个外设需要先定义clocks引用,再用assigned-clocks设置初始频率。
我见过有人只写了clocks,没写assigned-clocks。结果外设启动时时钟频率是默认值,根本不对。比如McASP需要24.576MHz,默认可能是19.2MHz,音频直接变调。
// 正确做法:同时指定时钟源和频率
mcasp0: mcasp@2b00000 {
compatible = "ti,j721e-mcasp";
reg = <0x00 0x2b00000 0x00 0x2000>;
clocks = <&k3_clks 174 0>;
assigned-clocks = <&k3_clks 174 0>;
assigned-clock-parents = <&k3_clks 174 1>;
assigned-clock-rates = <24576000>;
};
3.3.3 坑三:pinctrl复用冲突
Jacinto的引脚复用(PinMux)是通过pinctrl子系统管理的。两个外设不能同时占用同一个引脚。
我记得有一次调试UART,怎么都收不到数据。最后发现UART的TX引脚被I2C占用了。DTS里两个节点都引用了同一个pinctrl-0,但内核不会报错,只会默默让后加载的那个生效。
排查方法:在/sys/kernel/debug/pinctrl/目录下查看引脚复用状态。如果发现某个引脚被多个设备占用,那就是冲突了。
3.4 调试技巧——让DTS问题无处遁形
DTS写对了,系统跑起来很顺。写错了,问题千奇百怪。我总结了一套调试流程。
- 编译阶段检查:用
make dtbs编译,看有没有语法错误。DTC编译器会报出大部分低级错误。 - 查看实际加载的DTS:系统启动后,执行
dtc -I fs -O dts /sys/firmware/devicetree/base,把内核实际使用的DTS反编译出来。对比你写的源文件,看有没有被覆盖或修改。 - 检查驱动probe:在驱动入口加
dev_info打印,看of_match_table是否匹配成功。如果没进probe,八成是compatible写错了。 - 使用
of_*API调试:在驱动里调用of_property_read_u32等函数,打印读取到的属性值。确认DTS里的值被正确解析。
下面这张图是我自己总结的DTS调试流程,你可以参考:
3.5 实战案例:一个McASP的DTS调试过程
最后,我分享一个真实案例。某项目需要启用McASP0做音频输出。DTS写好后,音频一直没声音。
排查步骤:
- 反编译DTS:发现
assigned-clock-rates的值是0,说明时钟配置没生效。 - 检查时钟树:在
/sys/kernel/debug/clk下查看,发现McASP的父时钟是main_pll1,但频率被设成了19.2MHz。 - 修改DTS:在
assigned-clock-parents里指定正确的PLL输出,并显式设置频率。 - 验证:再次反编译,确认频率变成24.576MHz。音频正常输出。
这个案例说明:DTS里的时钟配置,光写clocks不够,必须配合assigned-clocks系列属性才能生效。
最后提醒:DTS的坑,90%出在compatible、reg、interrupts和clocks这四个属性上。写的时候多检查一遍,能省下半天调试时间。