4、中断控制器节点:设备树中如何描述 GIC、中断控制器级联
好,咱们接着聊中断控制器节点。说实话,这部分内容在设备树里属于「硬骨头」,但啃下来之后,你对整个中断系统的理解会上一个台阶。
我个人习惯把中断控制器节点比作「中断路由的总闸」。CPU 不会直接去管每个外设的中断线,它只认 GIC(通用中断控制器)。而设备树的作用,就是把这个「总闸」的拓扑结构、每个引脚怎么接,清清楚楚地告诉内核。
4.1 GIC 节点:中断系统的核心
先看一个典型的 GIC 节点长什么样。以 ARM Cortex-A 系列最常见的 GIC-400 为例:
gic: interrupt-controller@f1000000 {
compatible = "arm,cortex-a15-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0xf1000000 0x0 0x10000>,
<0x0 0xf1001000 0x0 0x2000>,
<0x0 0xf1002000 0x0 0x2000>;
interrupts = <1 9 0xf04>;
};
这里有几个关键属性,我一个个拆开讲。
#interrupt-cells = <3> 这个属性决定了子节点在描述中断时,需要提供几个 32 位整数。GIC 用 3 个 cell,分别表示:中断类型(SPI、PPI 等)、中断号、触发方式。我在项目中遇到过有人把 cell 数写错,结果中断死活不触发,查了两天才发现是这里少写了一个参数。
interrupt-controller; 这是个空属性,就一个标志位。告诉内核:「我是中断控制器,外设可以来找我注册中断。」没有这个属性,内核不会把它当成中断路由的终点。
reg 属性 定义了 GIC 的寄存器地址空间。GIC-400 通常有 3 块寄存器区域:
| 区域 | 偏移 | 用途 |
|---|---|---|
| GIC Distributor | 0x0000 - 0x0FFF | 中断分发、优先级、使能控制 |
| GIC CPU Interface | 0x1000 - 0x1FFF | CPU 侧中断处理接口 |
| GIC Virtual Interface | 0x2000 - 0x3FFF | 虚拟化支持(非必须) |
interrupts = <1 9 0xf04> 这个属性有意思了。GIC 本身也会产生中断——比如维护中断(Maintenance Interrupt)。这里的 1 表示 PPI(私有外设中断),9 是中断号,0xf04 是触发方式(高电平触发 + 安全状态)。
嗯,这里要注意:GIC 节点自己的 interrupts 属性,描述的是它「作为外设」时怎么被上一级中断控制器处理。在级联场景下,这个属性特别重要。
4.2 中断控制器级联:从单级到多级
实际项目中,很少只有一个中断控制器。比如一个 SoC 里既有 GIC,又有 GPIO 控制器做中断扩展,甚至还有老式的 8259 PIC。这时候就需要级联(Cascade)。
级联的本质是什么?说白了就是「下级中断控制器把多个中断源合并成一条线,接到上级中断控制器的某个引脚上」。设备树里用 interrupt-parent 和 interrupts 两个属性来描述这种关系。
看一个 GPIO 控制器级联到 GIC 的例子:
gpio0: gpio@ff000000 {
compatible = "mychip,gpio-controller";
reg = <0x0 0xff000000 0x0 0x1000>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&gic>;
interrupts = <0 32 4>; /* SPI 32, 高电平触发 */
};
这里的关键是:GPIO 控制器本身也是一个中断控制器。它管理着 32 个 GPIO 引脚的中断,但最终通过一根物理连线(SPI 32)报告给 GIC。设备树里用 interrupt-parent = <&gic> 明确告诉内核:「我的上级是 GIC,我产生的中断最终要走 GIC 的 SPI 32 号线。」
我曾经调试过一个触摸屏中断不响应的 bug。查到最后发现,GPIO 控制器的 interrupt-parent 写错了,指向了另一个不存在的节点。内核在解析中断时找不到上级控制器,直接跳过了这个设备。所以写设备树时,级联关系一定要画个拓扑图确认一遍。
4.3 多级级联的实战写法
有时候级联不止两级。比如一个系统里有:GIC → GPIO 控制器 → 外设扩展芯片。这时候设备树怎么写?
/* 第一级:GIC */
gic: interrupt-controller@f1000000 {
compatible = "arm,cortex-a15-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0xf1000000 0x0 0x1000>;
};
/* 第二级:GPIO 控制器,挂在 GIC 的 SPI 32 上 */
gpio1: gpio@ff100000 {
compatible = "mychip,gpio-controller";
reg = <0x0 0xff100000 0x0 0x1000>;
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&gic>;
interrupts = <0 32 4>;
};
/* 第三级:外设扩展芯片,挂在 GPIO 控制器的第 5 脚上 */
ext_device: expander@20 {
compatible = "mychip,ext-interrupt";
reg = <0x20>;
interrupt-parent = <&gpio1>;
interrupts = <5 2>; /* GPIO1 的第 5 脚,下降沿触发 */
};
你想想看,这个三级级联的路径是:
外设产生中断 → GPIO 控制器第 5 脚 → GPIO 控制器内部处理 → 通过 SPI 32 上报 GIC → GIC 分发到 CPU。
内核在解析 ext_device 节点时,会沿着 interrupt-parent 链一直往上找,直到找到根中断控制器(通常是 GIC)。这个过程叫「中断域解析」,内核会为每个中断控制器创建一个 irq_domain,然后通过映射关系把硬件中断号转换成 Linux 的 IRQ 号。
注意:级联层数不是无限的。Linux 内核默认支持最多 16 级中断控制器级联。虽然实际项目中很少超过 3 级,但如果你在 FPGA 上做实验,堆了 10 层级联,内核可能会报错。我曾经见过一个同事在 FPGA 原型验证时堆了 5 级,结果中断响应延迟从微秒级变成了毫秒级,整个系统性能崩了。
4.4 中断映射的 SVG 结构图
为了让你更直观地理解级联关系,我画了一张图:
4.5 避坑指南:我踩过的几个坑
讲几个实战中容易出问题的地方:
- interrupt-parent 写错路径:设备树里引用节点用 & 符号,比如 &gic。我见过有人直接写字符串 "gic",内核解析时找不到节点,直接报错。
- #interrupt-cells 不匹配:父节点定义的是 3 个 cell,子节点只给了 2 个。内核解析时会读取错误的数据,中断号完全乱掉。
- 级联环路:A 的 interrupt-parent 指向 B,B 的 interrupt-parent 又指向 A。内核在解析时会陷入死循环,直接 panic。写设备树时一定要检查有没有形成环。
- 中断号冲突:两个不同的外设用了同一个 SPI 中断号。GIC 会认为只有一个中断源,另一个设备永远收不到中断。
我个人习惯在写完设备树后,用 dtc 工具反编译成 dts,然后 grep 一下 interrupt-parent 和 interrupts 属性,手动画一个拓扑图。虽然麻烦,但能避免 90% 的中断配置问题。
好了,中断控制器节点和设备树级联的内容就讲到这里。记住一句话:设备树里的中断描述,本质上是在告诉内核「谁管谁、怎么管」。把这个逻辑理清了,写出来的设备树就不会出大问题。
公众号:蓝海资料掘金营,微信deep3321