3. 设备树常用属性详解:compatible、reg、interrupts、status、clocks

设备树里的属性,说白了就是描述硬件资源的「标签」和「参数」。我刚开始接触设备树时,总觉得这些属性名看着眼熟,但真要自己写一个节点,又拿不准该放哪些、怎么写。后来调过几块板子,踩过几次坑,才慢慢摸清门道。

这一章,咱们就把最常用的五个属性掰开揉碎讲清楚。你想想看,一个外设节点,内核最关心什么?无非是:你是谁(compatible)、你在哪(reg)、你怎么通知我(interrupts)、你现在能不能用(status)、以及你的时钟怎么配(clocks)。把这五个搞明白,大部分设备树节点你都能自己写了。

核心要点:这五个属性覆盖了 90% 以上外设节点的描述需求。调优的第一步,就是确保它们写对了。

3.1 compatible —— 驱动匹配的「身份证」

compatible 是设备树里最重要的属性,没有之一。内核通过它来找到对应的驱动程序。格式很简单:"厂商,型号",比如 "ti,am335x-uart"。

我个人习惯,至少写两个值:第一个是精确的芯片型号,第二个是兼容的通用型号。举个例子:

compatible = "ti,am335x-uart", "ns16550";

这样写的好处是:如果内核里没有针对 AM335x 的专有驱动,它还能 fallback 到通用的 16550 驱动。我在项目中遇到过一块板子,UART 死活不工作,查了半天发现 compatible 只写了一个厂商型号,而内核里根本没有对应的驱动。加上 "ns16550" 后,问题立刻解决。

小技巧:查看内核里有哪些 compatible 字符串被支持,可以 grep 内核源码的 of_match_table。比如:grep -r "ns16550" drivers/tty/serial/

3.2 reg —— 描述地址空间的「门牌号」

reg 属性告诉内核:这个外设的寄存器在哪个地址范围。格式是 <地址 长度> 的数组。对于 32 位系统,每个单元是 32 位;64 位系统则用两个单元表示一个 64 位地址。

reg = <0x48020000 0x1000>;

这表示外设的寄存器基地址是 0x48020000,占用 0x1000(4KB)的空间。我见过有人把长度写错,比如只写 0x100,结果内核访问寄存器时直接挂掉——因为超出了实际映射范围。

如果外设有多段地址空间,可以这样写:

reg = <0x48020000 0x1000>,
      <0x48021000 0x100>;

嗯,这里要注意:reg 的顺序必须和驱动里读取的顺序一致。驱动里 platform_get_resource(pdev, IORESOURCE_MEM, 0) 拿到的就是第一段,index 1 是第二段。

系统位数 地址单元大小 长度单元大小 示例
32 位 1 个 cell(32 位) 1 个 cell(32 位) reg = <0x48020000 0x1000>;
64 位 2 个 cell(64 位) 2 个 cell(64 位) reg = <0x00 0x48020000 0x00 0x1000>;

3.3 interrupts —— 外设的「敲门砖」

外设需要通知 CPU 时,就靠中断。interrupts 属性描述了中断号、触发类型等信息。格式依赖于中断控制器,但最常见的是:

interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>;

这里 GIC_SPI 表示共享外设中断(Shared Peripheral Interrupt),42 是中断号,IRQ_TYPE_EDGE_RISING 是上升沿触发。

我曾经在一个项目里,GPIO 中断老是丢失。排查到最后,发现是触发类型写错了——设备树里写的是电平触发,但芯片手册明确要求边沿触发。改过来之后,一切正常。

避坑指南:中断号不是随便写的!它依赖于中断控制器(interrupt-parent)的映射关系。同一个物理中断,在不同 SoC 上的编号可能完全不同。一定要查芯片手册或参考 BSP 代码。

如果外设使用多个中断,可以这样:

interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>,
             <GIC_SPI 43 IRQ_TYPE_LEVEL_HIGH>;

驱动里通过 platform_get_irq(pdev, 0) 和 platform_get_irq(pdev, 1) 分别获取。

3.4 status —— 设备的「开关」

status 属性控制设备是否启用。最常用的两个值是 "okay" 和 "disabled"。默认情况下,很多 SoC 的 dtsi 文件里会把外设设为 status = "disabled",然后在板级 dts 里再打开。

&uart0 {
    status = "okay";
};

为什么这么做?因为同一个 SoC 可能用在多种板卡上,有些外设可能没被引出。比如某个 I2C 控制器在芯片内部存在,但板子上没接任何设备,那就保持 disabled。

我见过有人把 status 拼写成 "ok" 或 "enable",结果内核根本不认。记住,只有 "okay" 和 "disabled" 是标准值。其他值如 "reserved" 或 "fail" 虽然在某些内核版本里被支持,但最好别用——移植性太差。

调试技巧:如果某个外设不工作,先检查 /proc/device-tree 下对应节点的 status 属性。用 cat /proc/device-tree/soc/uart@48020000/status 就能看到。

3.5 clocks —— 外设的「心跳」

clocks 属性指定外设使用的时钟源。格式是 <时钟控制器 时钟ID>。比如:

clocks = <&clkctrl CLKCTRL_UART0_CLK>;

这里 &clkctrl 是时钟控制器的 phandle,CLKCTRL_UART0_CLK 是时钟 ID。驱动通过 clk_get(dev, NULL) 或 devm_clk_get(dev, "name") 来获取时钟。

如果外设需要多个时钟,可以带名字:

clocks = <&clkctrl CLKCTRL_UART0_CLK>,
         <&clkctrl CLKCTRL_UART0_FCLK>;
clock-names = "clk", "fclk";

clock-names 属性给每个时钟起了名字,驱动里就可以用 devm_clk_get(dev, "fclk") 来获取。我个人强烈建议加上 clock-names,这样代码可读性高得多。

我在调试一个 USB 控制器时,发现它枚举设备特别慢。用示波器一量,时钟频率只有 12MHz,但设备树里配的是 48MHz。查了半天,原来是 clocks 属性里引用的时钟 ID 错了,指向了一个分频后的时钟。改回正确的时钟源后,速度立刻正常。

注意:时钟 ID 的定义通常在 include/dt-bindings/clock/ 目录下。不同 SoC 的时钟 ID 完全不同,不要想当然地复制粘贴。

3.6 知识体系总览

下面这张图把这五个属性的关系串起来了。你可以看到,一个外设节点如何通过这五个属性与内核的驱动框架、中断系统、时钟框架交互。

设备树常用属性与内核子系统交互关系 外设节点 compatible reg interrupts status clocks 驱动匹配框架 内存映射子系统 中断子系统 设备启用/禁用 时钟框架 每个属性对应一个内核子系统,属性值写错会导致对应子系统无法正确配置外设

3.7 实战:一个完整的 UART 节点

把上面五个属性组合起来,就是一个完整的外设节点。下面是一个实际的 UART 节点示例:

uart0: serial@48020000 {
    compatible = "ti,am335x-uart", "ns16550";
    reg = <0x48020000 0x1000>;
    interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>;
    status = "disabled";
    clocks = <&clkctrl CLKCTRL_UART0_CLK>;
    clock-names = "clk";
};

然后在板级 dts 里打开它:

&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
};

你看,调优的过程其实就是根据硬件手册,把这几个属性填对。我刚开始做的时候,总觉得设备树很神秘,后来发现——只要把这五个属性搞明白,再配合 pinctrl 和 dma 等辅助属性,大部分外设都能配起来。

最后提醒一句:写设备树时,多翻芯片手册,多参考内核里已有的 dts 文件。别自己瞎猜属性值——我吃过这个亏,你也别重蹈覆辙。