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 会建立两级映射:
- GPIO 子域:把 GPIO 引脚中断号映射到 GPIO 控制器的 Linux IRQ 号。
- GIC 父域:把 GPIO 控制器的中断线映射到 GIC 的 Linux IRQ 号。
驱动里调用 platform_get_irq() 时,内核会自动穿越这两级映射,返回最终的 Linux IRQ 号。你想想看,如果手动去算这个映射关系,得有多痛苦?
4.6 中断映射流程图
下面这张图展示了中断从设备到 CPU 的完整映射路径:
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 验证。只要这三步走对,中断基本不会出大问题。