1. 中断基础:ARM Cortex-A 中断控制器(GIC)架构与中断处理流程

中断,说白了就是让CPU从手头工作里跳出来,去处理一件更紧急的事。在汽车嵌入式Linux里,中断处理的好坏直接决定了系统的实时性。我刚开始做驱动时,总觉得中断就是配个IRQ号、写个handler就完事了,结果在实车上踩了不少坑——中断延迟过高导致CAN报文丢失,那叫一个头疼。

今天咱们就从GIC(Generic Interrupt Controller)架构入手,把中断的整个处理流程掰开揉碎了讲清楚。

1.1 GIC到底长什么样?

ARM Cortex-A系列处理器用的中断控制器叫GIC,目前主流是GICv2和GICv3。GICv2在Cortex-A7/A15上很常见,GICv3则出现在Cortex-A53/A72等64位核上。两者的核心思想一致,只是GICv3支持了更多中断源和更灵活的路由。

GIC的逻辑架构分为两大块:

  • Distributor(分发器):负责中断的使能、优先级配置、触发方式设置,以及将中断分发到指定的CPU接口。
  • CPU Interface(CPU接口):每个CPU核独享一个接口,负责向核发送中断信号,并处理中断的应答和结束。

我画了张图,帮你快速建立整体印象:

GICv2 架构与中断处理流程 GICv2 架构与中断处理流程 外设中断源 软件触发中断 核间中断(IPI) Distributor 中断使能/屏蔽 优先级配置 触发方式设置 中断分组 路由决策 CPU Interface 0 IRQ/FIQ信号 应答/结束控制 CPU Interface 1 IRQ/FIQ信号 应答/结束控制 CPU0 CPU1 中断处理流程(以IRQ为例): ① 外设触发中断 → Distributor判断优先级、使能状态 ② Distributor将中断路由到目标CPU Interface ③ CPU Interface向核发送IRQ信号 → CPU保存上下文 ④ CPU读GICC_IAR获取中断ID → 跳转到对应的handler ⑤ handler处理完毕 → 写GICC_EOIR通知中断结束

1.2 中断类型:三种中断,各有各的脾气

GIC把中断分成三类,咱们一个一个说:

中断类型 中断ID范围 典型用途 我踩过的坑
SPI(共享外设中断) 32~1019 UART、I2C、SPI、CAN等外设 多个核共享中断时,没配好亲和性导致中断风暴
PPI(私有外设中断) 16~31 每个核私有的定时器、PMU 误以为PPI是全局的,结果只在一个核上生效
SGI(软件生成中断) 0~15 核间通信(IPI)、CPU唤醒 SGI优先级没设对,导致死锁
小技巧: 在设备树里配中断时,留意 interrupts 属性的前两个字段——第一个是中断类型(0=SPI,1=PPI),第二个是中断号偏移。比如 interrupts = <0 42 4> 表示SPI类型,硬件中断号是42+32=74。

1.3 中断优先级与分组:别让低优先级的事耽误了刹车

GICv2支持256级优先级(0最高,255最低)。但实际实现中,芯片厂商可能只用了高4位或高5位。我建议你查一下芯片手册里的GIC_PRIORITY寄存器描述,确认到底支持多少级。

优先级分组(Priority Group)是GICv2引入的概念,把中断分成两组:

  • Group 0:安全中断,通常给TrustZone使用,以FIQ方式触发。
  • Group 1:非安全中断,Linux跑在非安全世界,用的就是Group 1。

为什么要分组?说白了就是为了隔离。安全世界的代码(比如TEE)不能被Linux打断,所以安全中断必须走FIQ,优先级高于所有非安全IRQ。

注意: 在汽车ECU里,安全相关的功能(如制动控制、转向控制)通常会走FIQ或高优先级IRQ。我曾经在一个项目中,把安全相关的CAN中断优先级设低了,结果被其他外设中断频繁抢占,导致安全报文响应超时——嗯,那是一次深刻的教训。

1.4 中断处理流程:从硬件到软件,一步都不能少

咱们把整个流程拆成6个步骤,配合代码来看:

  1. 外设触发中断:外设拉高中断请求线。
  2. Distributor仲裁:检查中断是否使能、优先级是否足够高,然后选一个目标CPU。
  3. CPU Interface发送信号:向目标CPU核发送IRQ或FIQ信号。
  4. CPU保存上下文:硬件自动保存CPSR、PC到SVC模式下的栈,跳转到异常向量表。
  5. 读取中断ID:软件读GICC_IAR寄存器,获取中断号,然后查表调用对应的handler。
  6. 结束中断:handler处理完后,写GICC_EOIR寄存器,通知GIC中断处理完毕。

在Linux里,第5步和第6步被封装成了通用的中断框架。驱动开发者只需要注册一个中断处理函数:

/* 驱动中注册中断的典型写法 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    /* 处理中断事件 */
    /* 注意:这里不能睡,不能调度 */
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    return request_irq(irq, my_irq_handler,
                       IRQF_TRIGGER_RISING,
                       "my_device", dev);
}
关键点: 中断上下文里不能调用可能导致睡眠的函数(如kmalloc(GFP_KERNEL)、mutex_lock等)。如果需要做耗时操作,用tasklet或workqueue推到下半部去处理。

1.5 中断亲和性:把中断绑到正确的核上

在多核系统里,中断可以路由到任意一个核。但如果不加控制,所有中断都往CPU0上涌,CPU0忙死,其他核闲死。这就是所谓的「中断风暴」。

Linux提供了/proc/irq/{irq_num}/smp_affinity接口,可以设置中断的CPU亲和性:

# 把中断42绑定到CPU0和CPU1(位掩码0x03)
echo 3 > /proc/irq/42/smp_affinity

在汽车场景里,我一般这样分配:

  • CPU0:运行实时控制任务,只绑高优先级中断(如CAN、安全相关)。
  • CPU1:处理普通外设中断(UART、I2C)。
  • CPU2/CPU3:跑Linux网络栈、文件系统等非实时任务。
建议: 在设备树里用 interrupts-affinity 属性,或者在驱动里调用 irq_set_affinity() 来绑定。别等到运行时才去写 /proc,那样太晚了。

1.6 中断延迟:汽车场景下的生死线

中断延迟 = 硬件延迟 + 软件延迟。硬件延迟主要看GIC的仲裁时间,一般几十ns到几μs。软件延迟才是大头——关中断的时间、中断嵌套、cache miss都会拖慢响应。

我整理了一个典型的中断延迟分布表:

环节 典型耗时 优化方向
GIC仲裁 ~100ns 硬件固定,无法优化
CPU保存上下文 ~200ns 减少异常向量表跳转次数
读GICC_IAR ~50ns 确保IAR在L1 cache中
查中断描述符表 ~500ns 使用基数树(radix tree)加速查找
执行handler 几μs~几百μs 把耗时操作推到下半部
写GICC_EOIR ~50ns 确保EOIR写操作不阻塞
避坑指南: 我曾经在一个项目中,发现中断延迟偶尔会飙升到几百μs。查了半天,原来是某个驱动在中断上下文里调了printk(),而printk会拿控制台锁,导致其他中断被阻塞。记住:中断上下文里别打日志,真要打就用tracepoint或ftrace。

1.7 小结:中断基础,决定了你的车机稳不稳

GIC架构是ARM中断处理的基石。理解Distributor和CPU Interface的分工,搞清楚SPI/PPI/SGI的区别,掌握中断优先级和亲和性的配置,你就能写出靠谱的驱动。