4. 设备树基础概念

设备树里中断和时钟的配置,是让 Linux 内核真正“活”起来的关键。说白了,CPU 要响应外设的事件,就得靠中断线;外设要正常工作,就得有正确的时钟源。这两块配错了,内核要么收不到中断,要么外设跑飞了频率。

我个人习惯,在调试新板子时,总是先确认中断树和时钟树是否完整。因为这两个东西一旦出错,现象往往很诡异——比如网卡能探测到但 ping 不通,或者串口能打印但丢数据。嗯,咱们今天就把这两个核心属性彻底讲透。

4.1 中断控制器与中断树

RISC-V 架构里,中断控制器通常就是 CPU 核本身(CLINT 或 PLIC)。设备树里用 interrupt-controller 属性来标记一个节点是中断控制器,用 #interrupt-cells 指定中断描述符需要几个 cell。

核心概念:中断树是从设备到中断控制器的路径。每个设备通过 interrupt-parent 指向它的中断控制器,然后通过 interrupts 属性描述具体的中断号、触发类型等信息。

我曾经在一个项目里,因为忘记给 GPIO 控制器加 interrupt-controller 属性,导致所有挂在该 GPIO 下的子设备中断都不生效。排查了整整两天才发现是这里少了一个标记。

4.1.1 interrupt-controller 与 #interrupt-cells

这两个属性必须成对出现。看一个典型的 RISC-V PLIC 节点:

plic: interrupt-controller@c000000 {
    compatible = "riscv,plic0";
    reg = <0x0 0xc000000 0x0 0x4000000>;
    interrupts-extended = <&cpu0_intc 11 &cpu0_intc 9>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

这里 #interrupt-cells = <2> 表示每个中断描述符用两个 32 位整数:第一个是中断号,第二个是触发标志(1=上升沿,2=下降沿,4=高电平,8=低电平)。

属性 含义 是否必需
interrupt-controller 声明该节点是中断控制器 是
#interrupt-cells 指定中断描述符的 cell 数量 是
interrupts-extended 连接到父中断控制器的中断线 视情况

4.1.2 interrupt-parent 与 interrupts

每个需要中断的外设节点,都要通过这两个属性连接到中断树。我建议你把它想象成“打电话”:interrupt-parent 是拨给谁,interrupts 是拨什么号码。

uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x0 0x10000000 0x0 0x100>;
    interrupt-parent = <&plic>;
    interrupts = <10 4>;  /* 中断号10,高电平触发 */
    clock-frequency = <3686400>;
};

小技巧:如果整个系统只有一个中断控制器,可以在根节点用 interrupt-parent 设置默认值,子节点可以省略这个属性。但我个人习惯每个设备都显式指定,避免后续修改时遗漏。

4.2 中断触发类型详解

中断触发类型是个容易踩坑的地方。我记得有一次调试 SD 卡驱动,发现中断总是丢失,后来发现是触发类型配错了——设备用的是下降沿触发,我配成了电平触发。

常见的触发类型值如下:

值 触发类型 适用场景
0 未指定/默认 由驱动自行判断
1 上升沿触发 边沿敏感设备
2 下降沿触发 边沿敏感设备
4 高电平触发 电平敏感设备
8 低电平触发 电平敏感设备

注意:RISC-V 的 PLIC 通常只支持电平触发,而 CLINT 支持边沿触发。如果你把电平触发的设备配成边沿触发,中断可能只触发一次,后续事件全部丢失。

4.3 时钟属性:clocks 与 clock-frequency

时钟配置比中断稍微简单一些,但同样重要。设备树里有两种方式指定时钟:

  • 静态方式:直接用 clock-frequency 指定频率值,单位是 Hz。
  • 动态方式:通过 clocks 属性引用时钟控制器,由内核动态管理。

我建议在 QEMU 模拟环境中,优先使用 clock-frequency 静态方式,因为简单可靠。但在真实硬件上,一定要用 clocks 引用时钟树,否则内核无法做动态调频。

4.3.1 clock-frequency 的使用

这个属性直接写在设备节点里,告诉驱动“你就按这个频率跑”。

uart1: serial@10001000 {
    compatible = "ns16550a";
    reg = <0x0 0x10001000 0x0 0x100>;
    interrupt-parent = <&plic>;
    interrupts = <11 4>;
    clock-frequency = <1843200>;
};

这里有个坑:clock-frequency 的单位是 Hz,不是 kHz 也不是 MHz。我曾经见过有人写成 <18432> 以为是 kHz,结果串口波特率完全不对。

4.3.2 clocks 与 clock-names

对于需要多个时钟源的复杂设备,就要用 clocks 属性了。每个时钟源通过 phandle 引用,clock-names 给每个时钟起个名字,方便驱动识别。

eth0: ethernet@10020000 {
    compatible = "sifive,fu540-c000-gem";
    reg = <0x0 0x10020000 0x0 0x2000>;
    interrupt-parent = <&plic>;
    interrupts = <53 4>;
    clocks = <&prci PRCI_CLK_GEMGX_MGMT>,
             <&prci PRCI_CLK_GEMGX_MGMT>;
    clock-names = "pclk", "hclk";
};

关键点:clocks 数组的顺序必须和 clock-names 一一对应。驱动通过名字查找时钟,如果顺序错了,驱动可能拿到错误的时钟源。

4.4 中断与时钟的完整示例

咱们把前面讲的内容整合成一个完整的设备树片段。这个例子来自我调试过的一个 QEMU RISC-V 虚拟平台:

/dts-v1/;

/ {
    #address-cells = <2>;
    #size-cells = <2>;
    model = "riscv-virtio,qemu";
    compatible = "riscv-virtio";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu0: cpu@0 {
            device_type = "cpu";
            reg = <0>;
            compatible = "riscv";
            riscv,isa = "rv64imafdc";
            cpu0_intc: interrupt-controller {
                #interrupt-cells = <1>;
                interrupt-controller;
                compatible = "riscv,cpu-intc";
            };
        };
    };

    soc {
        #address-cells = <2>;
        #size-cells = <2>;
        compatible = "simple-bus";
        ranges;

        plic: interrupt-controller@c000000 {
            compatible = "riscv,plic0";
            reg = <0x0 0xc000000 0x0 0x4000000>;
            interrupts-extended = <&cpu0_intc 11>;
            interrupt-controller;
            #interrupt-cells = <2>;
            riscv,ndev = <127>;
        };

        uart0: serial@10000000 {
            compatible = "ns16550a";
            reg = <0x0 0x10000000 0x0 0x100>;
            interrupt-parent = <&plic>;
            interrupts = <10 4>;
            clock-frequency = <3686400>;
        };
    };
};

这个例子展示了完整的中断树:CPU 核内部有 cpu0_intc,PLIC 连接到它;UART 连接到 PLIC。时钟方面,UART 直接用了静态频率。

4.5 中断与时钟的 SVG 结构图

下面这张图展示了中断树和时钟树在设备树中的完整关系:

设备树中断与时钟结构图 设备树中断与时钟关系图 根节点 / cpus/cpu@0 cpu0_intc (中断控制器) soc plic (中断控制器) interrupts-extended uart@10000000 interrupt-parent = <&plic> interrupts = <10 4> clock-frequency = <3686400> 关键说明: • 中断树路径:设备 → interrupt-parent → 中断控制器 → 上级中断控制器 → CPU • 时钟配置:静态用 clock-frequency,动态用 clocks + clock-names 引用时钟树 • 每个中断控制器必须声明 interrupt-controller 和 #interrupt-cells

4.6 常见错误与避坑指南

我在调试设备树时,中断和时钟相关的错误占了很大比例。下面是我总结的几个高频问题:

我曾经踩过的坑:有一次给 QEMU 添加虚拟网卡,中断号配成了 0。结果内核启动时中断注册失败,网卡驱动直接 probe 失败。后来才发现,RISC-V PLIC 的中断号从 1 开始,0 是无效中断。

  • 中断号范围错误:PLIC 的中断号通常从 1 到 N,0 保留。检查 riscv,ndev 属性确认最大中断号。
  • 触发类型不匹配:PLIC 只支持电平触发,CLINT 只支持边沿触发。混用会导致中断丢失。
  • 时钟频率单位错误:clock-frequency 单位是 Hz,不是 kHz。3686400 表示 3.6864 MHz,不是 3.6864 kHz。
  • 缺少 interrupt-controller 声明:中断控制器节点必须显式声明 interrupt-controller,否则内核不会把它识别为中断树的一环。
  • interrupts-extended 格式错误:这个属性每个 cell 对包含 phandle 和中断号,格式是 <&phandle 中断号>,不要漏掉 & 符号。

调试建议:在 QEMU 中启动内核时,加上 earlycon 和 debug 参数,可以打印中断注册和时钟初始化的详细信息。我每次改设备树都会先看这些日志,能省下大量排查时间。

中断和时钟是设备树的“神经系统”和“心跳”。配对了,内核启动顺滑;配错了,各种诡异问题层出不穷。我个人建议,每添加一个新设备,都先确认它的中断路径和时钟源是否完整,不要等到内核启动失败再回头查。