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 结构图
下面这张图展示了中断树和时钟树在设备树中的完整关系:
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 参数,可以打印中断注册和时钟初始化的详细信息。我每次改设备树都会先看这些日志,能省下大量排查时间。
中断和时钟是设备树的“神经系统”和“心跳”。配对了,内核启动顺滑;配错了,各种诡异问题层出不穷。我个人建议,每添加一个新设备,都先确认它的中断路径和时钟源是否完整,不要等到内核启动失败再回头查。