第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写对了,系统跑起来很顺。写错了,问题千奇百怪。我总结了一套调试流程。

  1. 编译阶段检查:用make dtbs编译,看有没有语法错误。DTC编译器会报出大部分低级错误。
  2. 查看实际加载的DTS:系统启动后,执行dtc -I fs -O dts /sys/firmware/devicetree/base,把内核实际使用的DTS反编译出来。对比你写的源文件,看有没有被覆盖或修改。
  3. 检查驱动probe:在驱动入口加dev_info打印,看of_match_table是否匹配成功。如果没进probe,八成是compatible写错了。
  4. 使用of_*API调试:在驱动里调用of_property_read_u32等函数,打印读取到的属性值。确认DTS里的值被正确解析。

下面这张图是我自己总结的DTS调试流程,你可以参考:

DTS调试流程图 DTS调试流程图 编写DTS源文件 make dtbs 编译 有语法错误? 修改 加载到内核启动 dtc反编译检查 驱动probe成功? 完成 排查compatible/reg 提示:反编译DTS是定位问题的利器。对比源文件和实际加载的版本,能发现很多隐藏问题。

3.5 实战案例:一个McASP的DTS调试过程

最后,我分享一个真实案例。某项目需要启用McASP0做音频输出。DTS写好后,音频一直没声音。

排查步骤:

  1. 反编译DTS:发现assigned-clock-rates的值是0,说明时钟配置没生效。
  2. 检查时钟树:在/sys/kernel/debug/clk下查看,发现McASP的父时钟是main_pll1,但频率被设成了19.2MHz。
  3. 修改DTS:在assigned-clock-parents里指定正确的PLL输出,并显式设置频率。
  4. 验证:再次反编译,确认频率变成24.576MHz。音频正常输出。

这个案例说明:DTS里的时钟配置,光写clocks不够,必须配合assigned-clocks系列属性才能生效。

最后提醒:DTS的坑,90%出在compatible、reg、interrupts和clocks这四个属性上。写的时候多检查一遍,能省下半天调试时间。