4. 设备树中断控制器:interrupt-parent、interrupt-cells、interrupt-map 配置与排查

中断,是嵌入式系统里最核心的通信机制之一。设备树里的中断描述,说白了就是告诉内核:谁产生了中断,中断信号走哪条路,最终送到哪个处理单元。我见过太多工程师在中断配置上栽跟头——要么系统不响应中断,要么中断风暴把 CPU 打满。今天咱们就把这三个关键属性彻底讲透。

4.1 中断控制器基础:interrupt-parent 与 interrupt-cells

先看一个最简单的例子。假设你的 SoC 里有一个 GPIO 控制器,它本身也是一个中断控制器——GPIO 引脚可以产生中断。

gpio0: gpio@ff000000 {
    compatible = "my-soc,gpio";
    reg = <0xff000000 0x1000>;
    interrupt-controller;
    #interrupt-cells = <2>;
    interrupts = <0 15 4>;
    interrupt-parent = <&gic>;
};

这里有几个关键点:

  • interrupt-controller:空属性,声明这个节点是一个中断控制器。
  • #interrupt-cells:描述该中断控制器需要几个 cell 来描述一条中断。比如 2 个 cell 通常表示:中断类型(上升沿、下降沿等)+ 中断号。
  • interrupt-parent:指向该节点自身的中断控制器——也就是谁给这个 GPIO 控制器提供中断服务。
  • interrupts:描述该节点自身需要的中断。这里用了 3 个 cell,因为 GIC(通用中断控制器)定义了 3 个 cell。

核心原则:每个需要中断服务的设备节点,必须通过 interrupt-parent 指向它的中断控制器,并用 interrupts 描述具体的中断信号。

我在项目中遇到过一种情况:有人把 interrupt-parent 写成了自己节点的 phandle,结果内核在解析中断时死循环了。你想想看,一个 GPIO 控制器怎么可能给自己发中断?

4.2 中断映射:interrupt-map 的复杂场景

当系统里有多个中断控制器,或者需要做中断路由时,interrupt-map 就派上用场了。最常见的是 PCIe 总线场景——PCIe 设备的中断需要通过桥接芯片映射到 SoC 的中断控制器。

pcie@f0000000 {
    compatible = "my-soc,pcie";
    reg = <0xf0000000 0x1000000>;
    #interrupt-cells = <1>;
    interrupt-map-mask = <0xf800 0 0 7>;
    interrupt-map = <
        0x0000 0 0 1 &pcie_intc 0 1
        0x0000 0 0 2 &pcie_intc 1 1
        0x0000 0 0 3 &pcie_intc 2 1
        0x0000 0 0 4 &pcie_intc 3 1
    >;
    pcie_intc: interrupt-controller {
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};

这个配置看起来有点绕,我拆开讲:

  • #interrupt-cells:PCIe 总线域里,每个设备用 1 个 cell 表示中断引脚(INTA#、INTB#、INTC#、INTD#)。
  • interrupt-map-mask:掩码,用于匹配设备地址和中断引脚。这里 0xf800 0 0 7 表示只匹配高 5 位地址和低 3 位中断号。
  • interrupt-map:映射表。每一行格式:子设备地址 子设备中断 父中断控制器 phandle 父中断描述。

我的调试技巧:当 interrupt-map 不生效时,先检查 interrupt-map-mask 是否覆盖了所有需要匹配的位。我曾经因为掩码写错,导致所有 PCIe 设备都映射到了同一个中断上——系统直接崩溃。

4.3 中断描述格式:不同控制器的 cell 定义

不同的中断控制器,#interrupt-cells 的含义完全不同。我整理了一个对照表:

中断控制器类型 #interrupt-cells cell 含义
GIC(ARM 通用中断控制器) 3 中断类型(0=SPI, 1=PPI)、中断号、触发方式(4=高电平, 8=上升沿)
简单 GPIO 中断控制器 2 GPIO 引脚号、触发方式(0=低电平, 1=高电平, 2=上升沿, 3=下降沿)
PCIe 中断控制器 1 中断引脚编号(1=INTA, 2=INTB, 3=INTC, 4=INTD)
Xilinx AXI INTC 2 中断号、触发方式(0=低电平, 1=高电平, 2=上升沿, 3=下降沿)

嗯,这里要注意:触发方式的编码不是统一的。GIC 用 4 表示高电平,但 GPIO 控制器可能用 1 表示高电平。一定要看对应控制器的 binding 文档。

4.4 常见排查场景与实战案例

我挑三个最常踩的坑,每个都附上排查方法。

场景一:中断号不匹配

症状:设备注册成功,但中断从不触发。用 cat /proc/interrupts 看不到该设备的中断计数。

排查步骤:

  1. 检查设备节点的 interrupts 属性,确认中断号是否正确。
  2. 用 dtc -I dtb -O dts 反编译 DTB,查看实际解析后的值。
  3. 对比 SoC 数据手册里的中断号映射表——很多 SoC 的中断号是从 0 还是从 32 开始,容易搞混。

我曾经踩过的坑:某款 SoC 的 GIC 文档里写 SPI 中断号从 0 开始,但实际设备树里需要加 32 的偏移。我查了整整两天,最后发现是参考手册和实际硬件不一致。

场景二:interrupt-parent 指向错误

症状:内核启动时打印 irq: no irq domain found for ...。

原因:interrupt-parent 指向的节点没有声明 interrupt-controller,或者 phandle 写错了。

排查方法:

  • 用 dtc 检查 phandle 是否有效:dtc -O dts -I dtb -o /tmp/test.dts boot.dtb && grep interrupt-parent /tmp/test.dts。
  • 确认目标节点确实有 interrupt-controller; 属性。
  • 检查是否有多个节点使用了相同的 phandle——这会导致内核解析混乱。

场景三:interrupt-map 映射失败

症状:PCIe 设备枚举成功,但中断分配失败,dmesg 里出现 irq: irq domain search failed。

排查步骤:

  1. 确认 interrupt-map-mask 是否覆盖了设备地址和中断引脚的所有有效位。
  2. 检查映射表里的父中断控制器 phandle 是否正确。
  3. 验证父中断控制器的 #interrupt-cells 与映射表里提供的 cell 数量一致。

我的调试习惯:在 interrupt-map 里只写一条映射做测试,确认通路后再补全其他条目。这样能快速定位是映射逻辑问题还是硬件问题。

4.5 知识体系总览

下面这张图总结了中断控制器配置的核心逻辑:

设备树中断控制器配置流程 设备树中断控制器配置流程 设备节点 interrupt-parent 中断控制器 interrupt-controller 关键属性说明: • interrupt-parent:指向提供中断服务的控制器节点 • #interrupt-cells:定义每个中断描述需要几个 cell • interrupts:设备自身需要的中断描述(按父控制器格式) 中断映射场景 interrupt-map 映射表 interrupt-map-mask 父中断控制器 #interrupt-cells 排查要点: ① 检查 phandle 有效性 ② 确认 cell 数量匹配 ③ 验证中断号偏移 ④ 核对触发方式编码

这张图把整个中断配置流程串起来了。从设备节点出发,通过 interrupt-parent 找到中断控制器,再根据 #interrupt-cells 解析中断描述。遇到复杂路由时,interrupt-map 负责做地址和中断的转换。

4.6 调试工具与命令

最后分享几个我常用的调试命令:

  • 查看中断分配:cat /proc/interrupts —— 看每个中断的触发次数和 CPU 亲和性。
  • 查看设备树解析结果:ls /proc/device-tree/ 配合 cat 查看具体属性值。
  • 动态调试中断域:echo 'file irqdomain.c +p' > /sys/kernel/debug/dynamic_debug/control —— 打开内核中断域调试信息。
  • 反编译 DTB:dtc -I dtb -O dts -o /tmp/debug.dts /sys/firmware/fdt —— 查看实际加载的设备树。

重要提醒:修改设备树后一定要重新编译并确认 DTB 被正确加载。我见过有人改了 .dts 文件但忘记编译,结果调试了一整天——最后发现内核用的还是旧 DTB。

中断配置说难不难,说简单也不简单。核心就是搞清楚三个问题:谁产生中断?中断怎么描述?中断路由到哪? 把这三个问题想明白,再配合上面的排查方法,大部分中断问题都能快速定位。