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 的完整路径。

GPIO 中断信号路径与中断控制器位置 GPIO 外设 引脚电平检测 中断控制器 GIC / INTC / NVIC 分发 · 屏蔽 · 确认 CPU 核心 IRQ / FIQ 中断请求 分发中断 中断确认 (IAR 读取) 关键说明: 1. GPIO 外设检测到有效边沿/电平后,向中断控制器发送中断请求信号。 2. 中断控制器根据优先级和屏蔽状态,决定是否将中断转发给 CPU。 3. CPU 响应中断后,通过读取中断确认寄存器获取中断号,并开始执行对应的 ISR。

4.2 中断分发机制

中断分发是中断控制器的首要职责。当多个外设同时产生中断请求时,中断控制器需要根据预先配置的优先级和目标 CPU,将中断信号路由到正确的核心。在 SMP(对称多处理)系统中,GIC 可以灵活地将不同中断分发到不同 CPU 核心,实现负载均衡。

4.2.1 分发流程

  1. 中断采样:中断控制器在时钟边沿采样所有中断请求线的电平状态。
  2. 优先级仲裁:比较所有活跃中断的优先级,选出当前优先级最高的中断。
  3. 目标匹配:检查该中断的目标 CPU 亲和性配置,确定转发给哪个核心。
  4. 信号断言:向目标 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 屏蔽层级

中断屏蔽的三层架构 外设级屏蔽 GPIO 中断使能寄存器 GPIO_IMR / GPIO_IER 控制器级屏蔽 GIC 使能寄存器 GICD_ISENABLER / GICD_ICENABLER CPU 级屏蔽 CPSR 的 I/F 位 IRQ/FIQ 全局使能 屏蔽优先级与使用场景: • 外设级屏蔽:控制单个 GPIO 引脚的中断使能/禁能,粒度最细。 • 控制器级屏蔽:在 GIC 层面屏蔽某个中断 ID,不影响其他中断。 • CPU 级屏蔽:全局关闭所有 IRQ 或 FIQ,用于临界区保护。

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 中断确认流程

  1. CPU 响应 IRQ:CPU 核心检测到 IRQ 信号,保存上下文,跳转到中断向量表。
  2. 读取 IAR:CPU 从 CPU 接口的 ICC_IAR(Interrupt Acknowledge Register)寄存器读取中断 ID。
  3. 硬件自动操作:读取 IAR 后,GIC 硬件自动将该中断状态从 Pending 变为 Active,并清除 CPU 接口中的中断信号。
  4. 执行 ISR:CPU 根据中断 ID 查找中断描述符,执行对应的中断服务例程。
  5. 写入 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 写入之间的时间窗口,对于设计高性能中断驱动至关重要。下图展示了完整的中断响应时序。

中断确认与结束时序图 时间 外设触发 CPU 响应 IRQ 读取 IAR 执行 ISR 写入 EOI 中断状态变化: 1. 外设触发 → 中断状态变为 Pending 2. CPU 读取 IAR → 状态从 Pending 变为 Active 3. ISR 执行期间 → 若再次触发,状态变为 Active and Pending 4. CPU 写入 EOI → 状态从 Active 变为 Inactive

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