4. 设备树中断:中断控制器、中断映射、中断号、中断标志

中断,是嵌入式系统里最核心的异步事件处理机制。没有中断,CPU就得轮询,效率低得吓人。在设备树里描述中断,说白了就是告诉内核:哪个设备能产生中断,中断信号怎么走,最终送到哪个中断控制器。

我刚开始接触设备树中断时,也被那一堆 interrupt-parent、interrupts、interrupt-cells 搞得晕头转向。后来在项目里调一个触摸屏驱动,死活收不到中断,才发现是中断映射配错了。嗯,从那以后,我对中断这块就格外上心。

4.1 中断控制器与中断生产者

设备树里,中断相关的节点分两类:

  • 中断生产者(Interrupt Producer):能发出中断信号的设备,比如 GPIO 按键、网卡、UART。
  • 中断控制器(Interrupt Controller):接收中断信号,并转发给 CPU 的硬件模块,比如 GIC(Generic Interrupt Controller)、NVIC。

一个 SoC 里可能有多个中断控制器,它们可以级联。比如 GPIO 控制器本身也是一个中断控制器,GPIO 引脚上的中断先送到 GPIO 控制器,再送到 GIC。

核心属性:

  • interrupt-controller:空属性,声明这个节点是一个中断控制器。
  • #interrupt-cells:指定中断描述符需要几个 cell(32位整数)。
  • interrupt-parent:指定该节点连接到哪个中断控制器。
  • interrupts:描述中断信号,格式由上游中断控制器的 #interrupt-cells 决定。

4.2 中断映射:信号是怎么走的

中断映射,就是描述中断信号从设备到 CPU 的完整路径。设备树通过 interrupt-parent 和 interrupts 属性建立映射关系。

举个例子,一个 UART 设备连接到 GIC:

gic: interrupt-controller@f0001000 {
    compatible = "arm,cortex-a15-gic";
    interrupt-controller;
    #interrupt-cells = <3>;
    reg = <0xf0001000 0x1000>;
};

uart0: serial@f0002000 {
    compatible = "arm,pl011";
    reg = <0xf0002000 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 33 4>;
};

这里 interrupts = <0 33 4> 的三个 cell 含义由 GIC 的 #interrupt-cells = <3> 决定:

Cell 位置 含义 常见取值
cell 0 中断类型 0 = SPI(共享外设中断),1 = PPI(私有外设中断)
cell 1 中断号 SPI 从 32 开始编号,PPI 从 16 开始
cell 2 中断标志 触发类型和极性

我的习惯:写设备树前,先看 SoC 手册里中断控制器的 #interrupt-cells 定义。ARM GIC 通常是 3,RISC-V PLIC 通常是 1,有些简单控制器是 2。搞错了,驱动里拿到的中断号就是错的。

4.3 中断号:硬件号与 Linux IRQ 号

这里有个容易混淆的点:设备树里写的 中断号 是硬件中断号,不是 Linux 内核里使用的 IRQ 号。

Linux 内核在初始化时,会通过 irq_domain 把硬件中断号映射到 Linux IRQ 号。驱动开发者在 probe 函数里调用 platform_get_irq() 或 irq_of_parse_and_map() 拿到的,已经是映射后的 Linux IRQ 号。

我曾经在调试一个网卡驱动时,直接拿设备树里的硬件中断号去注册中断处理函数,结果系统直接崩溃。后来才发现,必须用映射后的 IRQ 号。

避坑指南:

  • 设备树里写的是硬件中断号,不是 Linux IRQ 号。
  • 驱动里永远用 platform_get_irq() 获取中断号。
  • 不要假设硬件中断号等于 Linux IRQ 号,尤其是在多中断控制器级联的场景下。

4.4 中断标志:触发类型与极性

中断标志决定了中断信号怎么触发。常见的标志定义在 include/dt-bindings/interrupt-controller/irq.h 里:

宏定义 值 含义
IRQ_TYPE_EDGE_RISING 1 上升沿触发
IRQ_TYPE_EDGE_FALLING 2 下降沿触发
IRQ_TYPE_EDGE_BOTH 3 双边沿触发
IRQ_TYPE_LEVEL_HIGH 4 高电平触发
IRQ_TYPE_LEVEL_LOW 8 低电平触发

举个例子,一个按键中断,按下时电平从高变低,松开时恢复高电平。如果按键硬件是低电平有效,应该用 IRQ_TYPE_LEVEL_LOW 或 IRQ_TYPE_EDGE_FALLING。选哪个?

  • 电平触发:只要电平保持有效状态,中断就一直触发。适合需要持续通知的场景。
  • 边沿触发:只在电平跳变时触发一次。适合按键这种只关心动作瞬间的场景。

我个人习惯:按键用边沿触发,避免在按下期间反复触发中断。但要注意,如果按键有抖动,边沿触发可能会产生多次中断,需要在驱动里做消抖处理。

4.5 多中断控制器级联

复杂 SoC 里,中断控制器经常级联。比如 GPIO 控制器作为 GIC 的子中断控制器:

gpio0: gpio@f0003000 {
    compatible = "brcm,bcm2835-gpio";
    reg = <0xf0003000 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    interrupt-parent = <&gic>;
    interrupts = <0 42 4>;
};

key: gpio-keys {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;
    interrupts = <17 2>;  /* GPIO 17,下降沿触发 */
};

这里 key 节点的中断先送到 gpio0,gpio0 再通过自己的中断线(中断号 42)送到 gic。Linux 内核的 irq_domain 会建立两级映射:

  1. GPIO 子域:把 GPIO 引脚中断号映射到 GPIO 控制器的 Linux IRQ 号。
  2. GIC 父域:把 GPIO 控制器的中断线映射到 GIC 的 Linux IRQ 号。

驱动里调用 platform_get_irq() 时,内核会自动穿越这两级映射,返回最终的 Linux IRQ 号。你想想看,如果手动去算这个映射关系,得有多痛苦?

4.6 中断映射流程图

下面这张图展示了中断从设备到 CPU 的完整映射路径:

设备树中断映射流程图 设备树中断映射流程 设备节点 interrupt-parent 子中断控制器 #interrupt-cells 根中断控制器 GIC / NVIC CPU 核心 映射过程 1. 设备节点通过 interrupt-parent 找到子中断控制器 2. 子中断控制器通过自己的 interrupt-parent 找到根中断控制器 3. 内核 irq_domain 逐级映射,最终得到 Linux IRQ 号 4. 驱动调用 platform_get_irq() 拿到映射后的 IRQ 号

4.7 驱动里怎么拿中断

设备树配好了,驱动里拿中断其实很简单。以 platform 驱动为例:

static int my_driver_probe(struct platform_device *pdev)
{
    int irq;
    int ret;

    /* 获取中断号,自动完成映射 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "failed to get irq\n");
        return irq;
    }

    /* 注册中断处理函数 */
    ret = request_irq(irq, my_interrupt_handler,
                      IRQF_TRIGGER_FALLING,
                      "my_device", dev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request irq\n");
        return ret;
    }

    return 0;
}

这里 platform_get_irq(pdev, 0) 拿的是设备树里 interrupts 属性中第 0 个中断。如果设备有多个中断,可以用 platform_get_irq(pdev, 1) 拿第二个。

我的建议:注册中断时,中断标志最好在驱动里明确指定,不要依赖设备树里的默认值。不同内核版本对中断标志的处理可能有差异,显式指定更可靠。

4.8 常见问题与排查方法

中断配置出问题,现象通常是驱动 probe 成功,但中断从不触发,或者系统频繁崩溃。我总结了几条排查思路:

现象 可能原因 排查方法
中断从不触发 中断号错误、中断标志不匹配 检查 /proc/interrupts 看中断是否注册
系统频繁崩溃 中断标志用错,导致中断风暴 电平触发误配成边沿触发
probe 返回 -EINVAL interrupt-parent 指向错误 检查设备树 phandle 是否正确
中断号对不上 硬件号与 Linux IRQ 号混淆 用 cat /proc/interrupts 确认实际 IRQ 号

避坑指南:我曾经在一个项目里,GPIO 按键中断配成了 IRQ_TYPE_LEVEL_LOW,结果按键按下时中断持续触发,CPU 占用率直接飙到 100%。后来改成 IRQ_TYPE_EDGE_FALLING,问题解决。记住:电平触发适合共享中断线,边沿触发适合单次事件。

中断配置是设备树里最容易出错的地方之一。我的经验是:先确认中断控制器的 #interrupt-cells,再确认每个 cell 的含义,最后在驱动里用 platform_get_irq 验证。只要这三步走对,中断基本不会出大问题。