4. 设备树中断控制器

中断,说白了就是硬件主动找CPU“喊一嗓子”。

没有中断的时候,CPU只能轮询——一遍遍问“你好了没?你好了没?”效率低得可怜。有了中断,外设干完活了直接通知CPU,CPU该干嘛干嘛去。嗯,这里要注意,设备树里描述中断的方式,跟我们在裸机里写中断向量表完全是两码事。

4.1 中断控制器节点

在设备树里,中断控制器也是一个节点。它得有一个 interrupt-controller 属性,告诉内核:“我是管中断的”。

我个人习惯,拿到一块新板子,先看它的中断控制器是GIC(通用中断控制器)还是老式的NVIC。GIC在ARM Cortex-A系列里很常见,NVIC多见于Cortex-M。

一个典型的中断控制器节点长这样:

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

这里有个关键点:#interrupt-cells 决定了这个中断控制器需要多少个cell来描述一个中断。GIC用3个cell,分别表示中断类型(SPI、PPI、SGI)、中断号、触发方式。

重要: 中断控制器节点必须声明 interrupt-controller 属性,否则内核不认。

4.2 interrupt-parent:谁是我的“上级”?

每个需要中断的外设节点,都得告诉系统:“我的中断信号发给谁?”

这个“谁”就是 interrupt-parent。它指向一个中断控制器节点。

举个例子:

uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x10000000 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 33 4>;
};

这里 interrupt-parent = <&gic> 表示UART的中断信号连接到GIC上。我在项目中遇到过有人忘记加这个属性,结果中断死活不触发,查了半天才发现是父节点没指定。

小技巧: 如果整个系统只有一个中断控制器,可以在根节点设置 interrupt-parent,子节点默认继承。但我不建议这么做——万一以后加个二级中断控制器,代码改起来很麻烦。

4.3 interrupt-cells:中断描述符的“格式”

#interrupt-cells 定义了每个中断需要几个32位整数来描述。这个值由中断控制器自己决定。

常见的几种情况:

中断控制器类型 #interrupt-cells 含义
ARM GIC (v1/v2) 3 中断类型、中断号、触发方式
ARM GIC (v3/v4) 1 或 3 简化版只用中断号
老式中断控制器 2 中断号、触发方式
简单中断控制器 1 只有中断号

你想想看,如果 #interrupt-cells 写错了,内核解析中断信息时就会“对不上号”,轻则中断号错乱,重则直接panic。

避坑指南: 我曾经在移植一个老驱动时,发现中断控制器用了3个cell,但外设节点只给了2个。结果中断号被左移了8位,触发了一个完全不相关的中断。排查了整整一个下午。

4.4 中断映射:从硬件到软件

中断映射,说白了就是“硬件中断号”怎么变成“Linux IRQ号”。

这个过程由中断控制器驱动完成。设备树里描述的是硬件信息,内核启动时会解析这些信息,建立映射表。

一个典型的中断映射流程:

  1. 内核解析设备树,找到外设节点的 interrupts 属性
  2. 通过 interrupt-parent 找到对应的中断控制器
  3. 调用中断控制器的 .xlate 回调函数,把设备树里的cell转换成硬件中断号
  4. 分配一个Linux IRQ号,建立映射关系
  5. 驱动里调用 request_irq() 时,传入的就是这个Linux IRQ号

下面这张图展示了整个映射关系:

设备树中断映射流程 外设节点 interrupt-parent = <&gic> interrupts = <0 33 4> interrupt-parent 中断控制器节点 #interrupt-cells = <3> interrupt-controller; 解析 内核中断 子系统 中断映射表 硬件中断号 Linux IRQ号 33 62 34 63 建立映射

嗯,这里要注意,中断映射不是简单的“硬件号=软件号”。GIC里SPI中断号从32开始,但Linux内部可能从16开始。所以驱动里看到的IRQ号,跟数据手册里的中断号,往往对不上。

调试技巧: 在驱动里加 printk("IRQ number: %d\n", irq);,然后跟 /proc/interrupts 里的值对比,就能验证映射是否正确。

4.5 实际项目中的经验

我在项目中遇到过最典型的问题,就是中断号冲突。两个外设用了同一个硬件中断号,结果一个触发,另一个也跟着响应。

排查方法其实很简单:

  • 先看 /proc/interrupts,确认每个IRQ号对应的设备
  • 再看设备树,确认每个外设的 interrupts 属性
  • 最后查中断控制器的 #interrupt-cells 定义

还有一个坑:有些SoC内部有多个中断控制器,比如一个GIC加一个GPIO中断控制器。这时候 interrupt-parent 必须明确指定,不能依赖默认继承。我曾经见过一个工程师,把所有外设的 interrupt-parent 都指向了GIC,结果GPIO中断全跑到了GIC上,触发方式完全不对。

避坑指南: 如果外设连接到二级中断控制器(比如GPIO控制器),interrupt-parent 要指向这个二级控制器,而不是直接指向GIC。二级控制器自己再通过 interrupts 连接到GIC。

说白了,设备树里的中断描述,就是一张“谁的中断发给谁”的关系图。搞清楚了这张图,中断问题就解决了一半。