3. 设备树中断控制器:中断控制器属性与中断映射机制

大家好,我是你们的老朋友。今天我们来聊聊设备树里中断控制器的配置。说实话,中断这块儿在嵌入式开发中是个绕不开的坎儿。我刚开始接触设备树时,看到那些 interrupt-controller、#interrupt-cells 属性,也是一头雾水。后来在项目中踩过几次坑,才慢慢摸清了门道。

咱们今天就把这个事儿彻底讲明白。你想想看,一个 SoC 里可能有多个中断控制器,比如 GIC(通用中断控制器)、GPIO 控制器本身也能产生中断,甚至有些外设内部还有自己的子中断控制器。设备树怎么描述这种层级关系?这就是我们今天要解决的核心问题。

3.1 中断控制器的核心属性

每个中断控制器在设备树中都需要两个关键属性来表明身份:

  • interrupt-controller:这是一个空属性(无值),表示该节点是一个中断控制器。说白了,就是告诉内核:“嘿,我能处理中断!”
  • #interrupt-cells:指定中断描述符需要几个 cell(32位整数)来描述。这个值取决于中断控制器的硬件设计。

举个例子,ARM 的 GICv2 通常需要 3 个 cell:

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

这三个 cell 分别表示:中断类型(0=SPI,1=PPI)、中断号、触发方式(如高电平触发、上升沿触发等)。

重要提示:#interrupt-cells 的值必须与硬件手册完全一致。我见过有人把 GIC 的 #interrupt-cells 写成 2,结果中断根本配不上,系统直接 panic。这种低级错误排查起来特别费时间。

3.2 中断映射机制

中断映射,说白了就是解决“谁的中断连到了谁的哪个引脚上”这个问题。设备树通过三个属性来描述这种连接关系:

  • interrupt-parent:指定该设备的中断连接到哪个中断控制器。这是一个 phandle 引用。
  • interrupts:描述中断的具体信息,格式由父中断控制器的 #interrupt-cells 决定。
  • interrupt-map 和 interrupt-map-mask:用于复杂的中断路由场景,比如 PCIe 桥或者多级中断控制器级联。

咱们先看一个简单的例子。假设一个 UART 设备连接到 GIC 的第 32 号中断,上升沿触发:

uart0: serial@10020000 {
    compatible = "ns16550a";
    reg = <0x10020000 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 32 4>;  // SPI, 中断号32, 高电平触发
};

这里 interrupt-parent 指向了 gic 节点,interrupts 的三个值分别对应 GIC 的 #interrupt-cells = 3 的格式。

个人经验:我习惯在设备树文件的开头用宏定义来声明中断类型和触发方式,比如:

#define GIC_SPI 0
#define GIC_PPI 1
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_EDGE_RISING 1

这样写 interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH> 可读性会好很多。内核的 irq.h 头文件里其实已经定义了这些宏,但设备树里用的话需要自己加。

3.3 多级中断控制器与中断映射

实际项目中,中断控制器往往不止一级。比如 GPIO 控制器本身可以产生中断,但它又连接到 GIC 上。这时候就形成了两级中断控制器。

看一个 GPIO 控制器的例子:

gpio0: gpio@10030000 {
    compatible = "brcm,bcm2835-gpio";
    reg = <0x10030000 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    interrupt-parent = <&gic>;
    interrupts = <0 42 4>;  // GPIO控制器本身的中断连到GIC
};

这里 gpio0 既是 GPIO 控制器,又是中断控制器。它自己的中断(比如 GPIO 引脚电平变化)通过 interrupt-parent 连接到 GIC。而其他设备如果要使用 GPIO 的中断功能,就需要把 interrupt-parent 指向 gpio0:

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

这个 button 节点的 interrupts 只有两个 cell,因为 gpio0 的 #interrupt-cells = 2。第一个 cell 是 GPIO 引脚号,第二个是触发方式。

我曾经踩过的坑:有一次我写一个按键驱动,把 interrupt-parent 写成了 GIC,然后 interrupts 里填了 GPIO 引脚号。结果中断根本触发不了,因为 GIC 根本不认识 GPIO 引脚号。排查了半天才发现是 interrupt-parent 指错了。记住:interrupt-parent 一定要指向能解析你 interrupts 格式的那个中断控制器。

3.4 中断映射的层级结构

为了让大家更直观地理解,我画了一张图:

设备树中断映射层级结构 CPU GIC (中断控制器) interrupt-controller; #interrupt-cells = <3> interrupt-parent interrupt-parent interrupt-parent GPIO控制器 interrupt-controller; #interrupt-cells = <2> UART interrupts = <0 32 4> Timer interrupts = <0 29 4> interrupt-parent 按键 (GPIO17) interrupts = <17 2> 中断控制器 外设设备 GPIO子设备

从这张图可以清楚地看到:GIC 是顶层中断控制器,UART 和 Timer 直接连到 GIC。而 GPIO 控制器既是 GIC 的子设备(通过 interrupt-parent 连接),又是按键设备的中断父控制器。这种层级关系在设备树中通过 interrupt-parent 和 interrupts 属性逐级串联起来。

3.5 中断映射的高级用法:interrupt-map

当遇到更复杂的场景,比如一个 PCIe 桥下有多个设备,每个设备的中断号需要动态映射时,就需要用到 interrupt-map 属性了。这个属性通常和 interrupt-map-mask 配合使用。

看一个 PCIe 中断映射的例子:

pcie: pcie@f0000000 {
    compatible = "pci-host-ecam-generic";
    reg = <0xf0000000 0x10000000>;
    #address-cells = <3>;
    #size-cells = <2>;
    #interrupt-cells = <1>;
    interrupt-map-mask = <0xf800 0 0 7>;
    interrupt-map = <
        /* 设备号 0x0000, 中断引脚 INTA -> GIC 168 */
        0x0000 0 0 1 &gic 0 0 0 168 4
        /* 设备号 0x0000, 中断引脚 INTB -> GIC 169 */
        0x0000 0 0 2 &gic 0 0 0 169 4
        /* 设备号 0x0000, 中断引脚 INTC -> GIC 170 */
        0x0000 0 0 3 &gic 0 0 0 170 4
        /* 设备号 0x0000, 中断引脚 INTD -> GIC 171 */
        0x0000 0 0 4 &gic 0 0 0 171 4
    >;
};

这里 interrupt-map 的每一行都定义了一个映射规则:PCIe 设备的某个中断引脚(INTA~INTD)映射到 GIC 的哪个中断号。interrupt-map-mask 用于匹配设备地址,0xf800 表示只匹配高 5 位的设备号。

我的建议:interrupt-map 的写法非常容易出错。我一般会先在用户空间写一个脚本,把映射关系打印出来验证一遍,再放到设备树里。另外,不同架构的 PCIe 中断映射规则可能不同,x86 和 ARM 的处理方式就有差异,一定要参考芯片手册。

3.6 常见问题与避坑指南

最后,我总结几个实际项目中容易遇到的问题:

  1. 中断号冲突:多个设备使用了相同的中断号。这种情况在 GPIO 中断中尤其常见,因为 GPIO 控制器的中断线是有限的。
  2. 触发方式不匹配:设备树里配的是上升沿触发,但硬件实际输出的是低电平。我曾经因为这个原因,按键按下去中断只触发一次,松开时反而没反应。
  3. 中断控制器级联深度:有些 SoC 的中断控制器层级很深,比如 GIC -> GPIO -> 子设备。每一级都要正确配置 interrupt-parent,否则中断根本传不到 CPU。
  4. #interrupt-cells 不一致:父节点和子节点对中断描述符的 cell 数量理解不一致。比如父节点期望 3 个 cell,子节点只给了 2 个,内核解析时会直接报错。

避坑指南:我曾经在一个项目里,GPIO 子设备的中断死活不工作。查了两天才发现,GPIO 控制器的 interrupt-controller 属性忘记加了。内核根本不知道这个节点能处理中断,自然就不会把子设备的中断路由过来。所以,写设备树时一定要检查每个中断控制器节点是否都加了 interrupt-controller 属性。

好了,关于中断控制器的属性和映射机制,今天就聊到这儿。这些内容看起来琐碎,但每个细节都可能影响系统的稳定性。下次遇到中断相关的问题,不妨先从设备树配置入手排查,往往能事半功倍。

可可精品资料库