1. 设备树中的中断描述:interrupt-parent、interrupts、interrupt-cells

大家好,我是你们的嵌入式系统讲师。今天咱们来聊聊设备树里中断描述这块硬骨头。说实话,我刚入行那会儿,看到设备树里一堆 interrupt 开头的属性,头都大了。后来踩了不少坑,才慢慢摸清楚这里面的门道。

中断描述,说白了就是告诉内核:这个设备用哪根中断线,怎么触发,找谁去处理。设备树里用三个核心属性来完成这件事:interrupt-parentinterruptsinterrupt-cells。咱们一个一个来看。

1.1 interrupt-parent:你的中断归谁管?

每个设备要使用中断,首先得知道能处理它的中断请求。这个「谁」就是中断控制器,比如 GIC(通用中断控制器)、GPIO 控制器,甚至可能是另一个芯片。

interrupt-parent 属性就是一个 phandle(指针),指向设备的中断控制器节点。举个例子:

// 设备节点
uart0: serial@10000000 {
    compatible = "arm,pl011";
    reg = <0x10000000 0x1000>;
    interrupt-parent = <&gic>;  // 指向 GIC 中断控制器
    interrupts = <0 33 4>;
};

// 中断控制器节点
gic: interrupt-controller@2c001000 {
    compatible = "arm,cortex-a15-gic";
    reg = <0x2c001000 0x1000>;
    interrupt-controller;        // 声明自己是中断控制器
    #interrupt-cells = <3>;     // 每个中断描述需要 3 个 cell
};

这里有个细节:如果某个设备没有显式指定 interrupt-parent,内核会沿着设备树向上查找,直到找到某个节点的父节点带有 interrupt-controller 属性。嗯,这个机制我当年调试时吃过亏——明明配了中断,就是触发不了,最后发现是父节点没设对。

注意:一个设备只能有一个 interrupt-parent。如果你的设备需要连接到多个中断控制器(比如一个 GPIO 中断加一个系统中断),那得用 interrupts-extended 属性,这个咱们后面章节会讲。

1.2 interrupts:告诉内核中断怎么用

interrupts 属性描述的是设备具体使用的中断信息。它的格式由中断控制器的 #interrupt-cells 决定。每个 cell 是一个 32 位整数,组合起来描述一个中断。

拿最常见的 ARM GICv2 来说,#interrupt-cells = <3>,三个 cell 的含义是:

Cell 位置 含义 取值说明
cell 0 中断类型 0 = 共享外设中断 (SPI)
1 = 私有外设中断 (PPI)
cell 1 中断号 SPI 范围:32~1019
PPI 范围:16~31
cell 2 触发类型 1 = 上升沿触发
2 = 下降沿触发
4 = 高电平触发
8 = 低电平触发

举个例子:interrupts = <0 33 4> 表示这是一个 SPI 中断(类型 0),中断号 33,高电平触发(值 4)。

我个人习惯在写设备树时,把中断触发类型单独用宏定义出来,比如:

#define IRQ_TYPE_EDGE_RISING  1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_LEVEL_HIGH   4
#define IRQ_TYPE_LEVEL_LOW    8

interrupts = <0 33 IRQ_TYPE_LEVEL_HIGH>;

这样可读性高很多,别人一看就知道是电平触发还是边沿触发。

小技巧:如果你不确定某个中断控制器的 cell 格式,可以去内核源码的 Documentation/devicetree/bindings/interrupt-controller/ 目录下找对应的 binding 文档。我每次对接新芯片时,第一件事就是翻这个目录。

1.3 interrupt-cells:定义中断描述的长度

#interrupt-cells 是中断控制器节点上的属性,它告诉系统:我这个控制器描述一个中断需要几个 32 位整数。这个值完全由硬件设计决定。

常见的几种情况:

  • 简单中断控制器(如老式 8259 PIC):#interrupt-cells = <1>,只需要一个中断号。
  • ARM GIC#interrupt-cells = <3>,需要类型、中断号、触发方式。
  • GPIO 控制器作为中断控制器:通常是 #interrupt-cells = <2>,第一个 cell 是 GPIO 引脚号,第二个 cell 是触发类型。

来看一个 GPIO 中断的例子:

gpio0: gpio@50000000 {
    compatible = "snps,dw-apb-gpio";
    reg = <0x50000000 0x1000>;
    #gpio-cells = <2>;
    #interrupt-cells = <2>;   // 两个 cell:引脚号 + 触发类型
    interrupt-controller;      // 这个 GPIO 也能处理中断
};

button {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;
    interrupts = <5 IRQ_TYPE_EDGE_FALLING>;  // GPIO5,下降沿触发
};

这里有个坑:GPIO 控制器既要管理 GPIO 输出,又要处理中断输入。我在一个项目里就遇到过,GPIO 的中断触发类型配错了,导致按键按下时中断没响应,松开时反而触发了。后来查了半天,发现是 interrupts 里的触发类型和 GPIO 的电气特性不匹配。

1.4 三者如何协同工作?

咱们用一张图来理清这三个属性的关系:

设备节点 uart0: serial@10000000 interrupt-parent = <&gic> interrupts = <0 33 4> interrupt-parent 中断控制器节点 gic: interrupt-controller@2c001000 interrupt-controller; #interrupt-cells = <3> (定义 cell 格式) interrupts 按此格式解析 3 个 cell 含义 cell 0: 中断类型 (0=SPI) cell 1: 中断号 (33) cell 2: 触发类型 (4=高电平) 三者关系: 1. interrupt-parent 建立设备到控制器的连接 2. #interrupt-cells 定义中断描述格式 3. interrupts 按格式提供具体中断参数 ⚠ 常见坑点 • 中断号不要重复,否则内核会报 "irq already mapped" • 触发类型要和硬件电气特性一致,电平/边沿别搞混

从这张图可以看得很清楚:interrupt-parent 是连接设备和中断控制器的桥梁,#interrupt-cells 定义了这座桥上跑的数据格式,而 interrupts 就是实际传输的数据内容。三者缺一不可。

1.5 实战中的避坑指南

我在几个项目里积累了一些经验,分享给大家:

  • 中断号冲突:我曾经在一个 SoC 上同时接了多个外设,结果两个设备用了同一个中断号。内核启动时直接报 "irq already mapped",系统直接挂掉。排查方法是用 /proc/interrupts 查看已注册的中断。
  • 触发类型不匹配:硬件设计是高电平触发,设备树里配成了下降沿触发。结果中断要么不触发,要么疯狂触发。我建议拿到硬件原理图后,先确认中断信号的电气特性,再写设备树。
  • 忘记声明 interrupt-controller:中断控制器节点上必须加 interrupt-controller; 属性,否则内核不认。这个我犯过两次,都是低级错误。
核心要点:中断描述的本质是「谁(interrupt-parent)→ 什么中断(interrupts)→ 怎么描述(#interrupt-cells)」。把这个链条理清楚,设备树中断这块就通了八成。

好了,这一章的内容就到这里。中断描述是设备树里最基础也最容易出错的部分,建议大家动手写几个例子,在开发板上实际跑一跑。下一章咱们会深入 interrupts-extended 和中断映射的底层机制,到时候见。

专注资料整理