4. 中断控制器(GIC/INTC)原理:中断分发、中断屏蔽、中断确认机制
在嵌入式系统中,GPIO 引脚的电平变化或边沿跳变产生的中断信号,并不会直接送达 CPU 核心。所有外设中断(包括 GPIO 中断)都必须经过一个中央管理机构——中断控制器。在 ARM Cortex-A 系列处理器中,该控制器称为 GIC(Generic Interrupt Controller);在 ARM Cortex-M 系列或许多 MCU 中,则称为 INTC(Nested Vectored Interrupt Controller) 或 NVIC。本章深入剖析中断控制器的三大核心机制:中断分发、中断屏蔽与中断确认。
4.1 中断控制器在系统中的位置
中断控制器位于外设与 CPU 核心之间,承担中断信号的汇聚、优先级仲裁、路由分发以及状态管理。下图展示了 GPIO 中断从引脚到 CPU 的完整路径。
4.2 中断分发机制
中断分发是中断控制器的首要职责。当多个外设同时产生中断请求时,中断控制器需要根据预先配置的优先级和目标 CPU,将中断信号路由到正确的核心。在 SMP(对称多处理)系统中,GIC 可以灵活地将不同中断分发到不同 CPU 核心,实现负载均衡。
4.2.1 分发流程
- 中断采样:中断控制器在时钟边沿采样所有中断请求线的电平状态。
- 优先级仲裁:比较所有活跃中断的优先级,选出当前优先级最高的中断。
- 目标匹配:检查该中断的目标 CPU 亲和性配置,确定转发给哪个核心。
- 信号断言:向目标 CPU 的 IRQ 或 FIQ 引脚断言中断信号。
GIC 分发器 vs. CPU 接口: 在 GICv2/v3 架构中,分发器(Distributor) 负责全局优先级管理和中断使能,而每个 CPU 核心拥有独立的 CPU 接口(CPU Interface),处理本核心的中断确认和结束。这种分离设计使得多核中断管理更加高效。
4.2.2 中断类型与分发策略
| 中断类型 | 触发方式 | 分发策略 | 典型用途 |
|---|---|---|---|
| PPI(私有外设中断) | 电平/边沿 | 固定分发到指定 CPU | 本地定时器、交叉触发 |
| SPI(共享外设中断) | 电平/边沿 | 可配置分发到一个或多个 CPU | GPIO 中断、UART、DMA |
| SGI(软件生成中断) | 边沿 | 由软件写入 ICDSGIR 触发,可指定目标 CPU | 核间通信(IPI) |
| LPI(本地特定中断) | 消息 | 基于消息的中断,通过 ITS 路由 | PCIe MSI/MSI-X |
4.3 中断屏蔽机制
中断屏蔽允许软件在多个层级上阻止特定中断的传递。屏蔽机制是中断控制器的核心安全特性,用于防止关键代码段被不恰当的中断打断。
4.3.1 屏蔽层级
4.3.2 优先级屏蔽
除了逐位屏蔽,GIC 还支持优先级屏蔽。CPU 接口中的 ICC_PMR(Priority Mask Register)寄存器设置一个优先级阈值。只有优先级高于该阈值的中断才能被传递给 CPU。这在实现中断嵌套时非常关键:ISR 可以通过提高 PMR 阈值来临时屏蔽低优先级中断。
驱动开发陷阱: 在 Linux 内核中,local_irq_disable() 仅屏蔽当前 CPU 的 IRQ(通过操作 CPSR),不会影响其他 CPU 核心的中断接收。如果驱动运行在 SMP 系统上,必须使用 spin_lock_irqsave() 等机制来确保跨核同步。
4.4 中断确认机制
中断确认是 CPU 与中断控制器之间的握手协议。CPU 在响应中断后,必须通过读取特定的确认寄存器来获取中断号,并告知中断控制器“我已接管该中断”。
4.4.1 GIC 中断确认流程
- CPU 响应 IRQ:CPU 核心检测到 IRQ 信号,保存上下文,跳转到中断向量表。
- 读取 IAR:CPU 从 CPU 接口的
ICC_IAR(Interrupt Acknowledge Register)寄存器读取中断 ID。 - 硬件自动操作:读取 IAR 后,GIC 硬件自动将该中断状态从 Pending 变为 Active,并清除 CPU 接口中的中断信号。
- 执行 ISR:CPU 根据中断 ID 查找中断描述符,执行对应的中断服务例程。
- 写入 EOI:ISR 执行完毕后,CPU 向
ICC_EOIR(End of Interrupt Register)写入中断 ID,通知 GIC 中断处理完成。
4.4.2 中断状态机
| 状态 | 描述 | 触发条件 |
|---|---|---|
| Inactive | 中断未触发 | 初始状态或 EOI 写入后 |
| Pending | 中断请求已到达,等待 CPU 确认 | 外设触发中断且未被屏蔽 |
| Active | CPU 已确认中断,正在执行 ISR | CPU 读取 IAR 寄存器 |
| Active and Pending | ISR 执行期间,同一中断源再次触发 | 边沿触发中断在 Active 期间再次产生 |
4.4.3 中断确认的代码示例
以下代码展示了在裸机或 RTOS 环境中,如何手动完成中断确认与结束流程。Linux 内核的 __handle_domain_irq() 函数内部封装了类似的逻辑。
/* 中断服务例程汇编入口(简化伪代码) */
irq_handler:
/* 保存上下文 */
SAVE_CONTEXT
/* 1. 中断确认:读取 IAR 获取中断号 */
LDR r0, =ICC_IAR_BASE
LDR r1, [r0] /* r1 = 中断 ID */
/* 2. 根据中断号跳转到对应的 C 语言 ISR */
BL handle_irq
/* 3. 中断结束:写入 EOI */
LDR r0, =ICC_EOIR_BASE
STR r1, [r0] /* 写入相同的中断 ID */
/* 4. 恢复上下文并返回 */
RESTORE_CONTEXT
SUBS pc, lr, #4
关键寄存器速查(GICv2 CPU 接口):
ICC_IAR(0x04):读取获取中断 ID,同时硬件自动清除 Pending 状态。ICC_EOIR(0x08):写入中断 ID 以结束中断,将状态从 Active 变回 Inactive。ICC_PMR(0x00):优先级屏蔽寄存器,设置中断通过阈值。ICC_BPR(0x0C):二进制点寄存器,控制优先级分组。
4.5 中断确认与结束的时序
正确理解 IAR 读取与 EOI 写入之间的时间窗口,对于设计高性能中断驱动至关重要。下图展示了完整的中断响应时序。
4.6 中断确认的硬件细节
在 GIC 架构中,IAR 的读取操作具有副作用:它不仅返回中断 ID,还会自动完成以下硬件操作:
- 将中断状态从 Pending 更新为 Active。
- 如果该中断是边沿触发的,硬件会自动清除 Pending 标志,防止重复触发。
- 对于电平触发的中断,硬件不会清除 Pending 标志;ISR 必须通过操作外设寄存器来消除电平信号。
- 在多核系统中,IAR 返回的中断 ID 已经被分发器路由到当前 CPU,其他核心不会收到相同的中断。
电平触发 vs. 边沿触发确认差异:
- 边沿触发:读取 IAR 后,GIC 自动清除 Pending 状态。即使外设引脚再次产生边沿,也需要新的触发事件才能再次置位 Pending。
- 电平触发:读取 IAR 后,GIC 不会清除 Pending 状态。如果外设输出电平仍然有效,中断会立即再次 Pending。因此,ISR 中必须首先操作外设寄存器清除中断源,最后再写入 EOI。
4.7 中断确认在 Linux 内核中的实现
Linux 内核的 GIC 驱动(drivers/irqchip/irq-gic.c)封装了中断确认的底层操作。以下是关键函数的调用链:
/* Linux 内核 GIC 中断处理入口(简化) */
static asmlinkage void __exception_irq_entry
gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic->cpu_base;
/* 1. 读取 IAR 确认中断 */
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & ~0x1c00; /* 提取中断 ID */
if (likely(irqnr > 15 && irqnr < 1020)) {
/* 2. 调用通用中断处理框架 */
handle_domain_irq(gic->domain, irqnr, regs);
return;
}
/* 3. 处理 SGI(核间中断) */
if (irqnr < 16) {
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
handle_IPI(irqnr, regs);
}
}
/* handle_domain_irq 内部最终调用 */
void handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq, struct pt_regs *regs)
{
struct irq_desc *desc = irq_to_desc(hwirq);
/* 执行 ISR */
generic_handle_irq_desc(desc);
/* 写入 EOI */
gic_eoi_irq(desc);
}
从上述代码可以看出,Linux 内核将 IAR 读取放在汇编入口层,将 EOI 写入放在通用中断处理框架的末尾。这种设计确保了中断确认与结束的配对操作不会被遗漏。
4.8 本章小结
| 机制 | 核心功能 | 关键寄存器/操作 | 驱动开发关注点 |
|---|---|---|---|
| 中断分发 | 优先级仲裁、目标 CPU 路由 | GICD_IPRIORITYR、GICD_ITARGETSR | 配置中断优先级与 CPU 亲和性 |
| 中断屏蔽 | 外设级、控制器级、CPU 级三级屏蔽 | GPIO_IMR、GICD_ICENABLER、CPSR.I | 临界区保护、SMP 同步 |
| 中断确认 | IAR 读取获取中断 ID,EOI 写入结束中断 | ICC_IAR、ICC_EOIR | 电平触发需先清除外设源,再写 EOI |