中断控制器:GIC 架构与中断类型详解
说到中断处理,GIC(Generic Interrupt Controller)是 ARM 体系里绕不开的核心部件。我最早接触 GIC 是在做一款多核网络处理器的时候,当时被 SPI、PPI、SGI 这些缩写搞得晕头转向。后来亲手调过几轮中断亲和性、踩过电平触发的坑,才算真正摸透了它。
说白了,GIC 就是 CPU 和中断源之间的“总调度员”。它负责接收各个外设发来的中断信号,按优先级排队,再分发给一个或多个 CPU 核心去处理。没有它,多核系统的中断就乱套了。
GIC 架构概览
GIC 从 v2 发展到 v4,但基本框架没变。以最常用的 GICv2 为例,它由两部分组成:
- Distributor(分发器)——负责中断的使能、优先级配置、目标 CPU 选择。我习惯把它想象成“路由表”。
- CPU Interface(CPU 接口)——每个 CPU 核心独享一份,负责把中断信号送给对应的核心。
你想想看,一个外设触发中断,信号先到 Distributor,Distributor 查表决定“这个中断该给哪个核”,然后通过 CPU Interface 通知那个核。流程很清晰,对吧?
三种中断类型
GIC 把中断分成三类,我当年刚接触时觉得这分类有点多余,后来才发现——每种类型都有它不可替代的用途。
| 中断类型 | ID 范围 | 目标 | 典型用途 |
|---|---|---|---|
| SPI(Shared Peripheral Interrupt) | 32 ~ 1019 | 可路由到任意一个或多个 CPU | 外设中断(UART、I2C、GPIO 等) |
| PPI(Private Peripheral Interrupt) | 16 ~ 31 | 固定绑定到某个 CPU | 本地定时器、性能监测单元 |
| SGI(Software Generated Interrupt) | 0 ~ 15 | 由软件写入触发,可定向到指定 CPU | 核间通信、IPI |
SPI——共享外设中断
这是最常用的中断类型。板子上的 UART、I2C、SPI、GPIO 等外设,默认都走 SPI。它的特点是“共享”——一个外设的中断线可以被路由到任意一个 CPU 核心。
我在项目中遇到过一个问题:两个 CPU 核心同时收到了同一个 SPI 中断,导致处理函数重入。后来查手册才发现,SPI 虽然可以路由到多个核心,但实际使用中最好只配给一个核心,或者用中断亲和性绑定到特定核。否则你要自己处理重入保护。
PPI——私有外设中断
PPI 是每个 CPU 核心“独享”的中断线。同一个中断号,在不同核心上可以对应不同的物理中断源。最典型的例子就是每个核心的本地定时器。
你想想看,如果本地定时器用 SPI,那所有核心的定时器中断都挤在一个号上,根本分不清是哪个核的定时器到期了。PPI 完美解决了这个问题——每个核有自己的定时器中断,互不干扰。
SGI——软件触发中断
SGI 是“写寄存器就能触发”的中断。你往 GIC 的 SGI 触发寄存器里写一个值,就能让指定 CPU 核心收到一个中断。这玩意儿在 Linux 内核里用得特别多,比如 TLB 刷新、进程调度、CPU 热插拔等场景。
我记得有一次调试多核死锁问题,就是用 SGI 在某个核心上触发一个调试中断,然后 dump 出那个核的寄存器状态。没有 SGI,这种调试手段根本没法实现。
/* Linux 内核中发送 SGI 的示例(简化) */
static void send_sgi_to_cpu(unsigned int cpu, unsigned int sgi_id)
{
unsigned long flags;
void __iomem *gic_dist_base = get_gic_dist_base();
raw_spin_lock_irqsave(&gic_lock, flags);
/* 写入 SGI 触发寄存器 */
writel_relaxed(1UL << cpu, gic_dist_base + GIC_DIST_SGI + (sgi_id * 4));
raw_spin_unlock_irqrestore(&gic_lock, flags);
}
中断触发方式
触发方式就两种:电平触发和边沿触发。但选错了,后果很严重。我见过一个同事因为把电平触发配成了边沿触发,导致按键中断只能响应一次,按第二次就没反应了。
| 触发方式 | 信号特征 | 优点 | 缺点 |
|---|---|---|---|
| 电平触发(Level-triggered) | 中断信号保持高/低电平,直到被清除 | 不会丢失中断;适合共享中断线 | 必须及时清除,否则反复触发 |
| 边沿触发(Edge-triggered) | 检测到上升沿或下降沿时触发一次 | 适合脉冲信号;不会因电平持续而重复触发 | 容易丢失中断(脉冲太窄时) |
电平触发
电平触发的中断,只要外设把中断线拉高(或拉低),GIC 就会一直认为有中断 pending。直到 CPU 处理完中断,通知外设把电平拉回去,中断才会消失。
这种方式的优点是“可靠”——只要电平还在,中断就不会丢。但缺点也很明显:如果你在中断处理函数里忘了清除中断源,或者清除顺序不对,中断会立刻再次触发,形成死循环。
边沿触发
边沿触发只关心信号跳变的那一瞬间。上升沿来了,触发一次;下降沿来了,再触发一次。信号保持高电平或低电平都不会重复触发。
这种方式的优点是“干净”——不会因为电平持续而反复进中断。但缺点是:如果脉冲宽度太窄,CPU 还没来得及响应,脉冲就消失了,这个中断就丢了。
我建议:对于 GPIO 按键、外部事件触发这类场景,用边沿触发比较合适。对于需要可靠传输的外设(比如 DMA 完成中断),用电平触发更稳妥。
如何选择触发方式
这里没有绝对的标准,但我个人有几个经验:
- 电平触发:适合中断源需要 CPU 主动确认的场景,比如 DMA 传输完成、FIFO 非空。中断处理函数里必须清除外设的中断源。
- 边沿触发:适合脉冲式信号,比如按键按下、外部事件跳变。注意脉冲宽度要足够宽,至少大于几个 CPU 时钟周期。
- 共享中断线:多个设备共用一根中断线时,必须用电平触发。边沿触发无法区分是哪个设备触发了中断。