4. 中断机制入门:ARM GIC中断控制器、中断号、中断触发方式

中断,说白了就是让CPU从“埋头苦干”的状态里跳出来,先处理一件更紧急的事。我刚开始做驱动时,总觉得中断就是个“回调函数”,后来被坑了几次才明白——不理解中断控制器,你连设备为什么没反应都查不出来。

这一章,咱们就聊聊ARM体系里最核心的中断管家——GIC(Generic Interrupt Controller)。你搞懂了它,UART的中断处理才算真正入门。

4.1 为什么需要GIC?

早期的ARM芯片,中断控制器很简单。每个外设直接拉一根线到CPU。但现在的SoC里,外设几十上百个,CPU核心也不止一个。你想想看,如果每个外设都直接连CPU,那芯片得有多少根引脚?

GIC就是来解决这个问题的。它像一个总调度台:

  • 收集所有外设的中断信号
  • 判断优先级
  • 决定发给哪个CPU核心
  • 告诉CPU中断源是谁

我在一个项目里遇到过,两个外设同时触发中断,结果系统卡死了。查了两天才发现,是GIC的优先级配置没设对。嗯,从那以后我再也不敢轻视这个“调度台”了。

4.2 GIC的架构与版本

ARM GIC发展了好几个版本。咱们做嵌入式Linux开发,最常见的是GICv2和GICv3。

特性 GICv2 GICv3
CPU接口 通过内存映射寄存器 通过系统寄存器(ICC_*)
最大中断数 最多1020个 最多10240个
多核支持 有限,需要软件亲和性 原生支持,路由灵活
典型芯片 Cortex-A7/A9 Cortex-A53/A72

我个人习惯,拿到一个新板子,第一件事就是查芯片手册里GIC的版本。版本不同,寄存器地址和操作方式差很多。

4.3 中断号:每个中断的“身份证”

GIC把中断分成三类。每一类都有自己的编号范围。

  • SGI(Software Generated Interrupt):软件触发的中断,编号0-15。多核通信常用。
  • PPI(Private Peripheral Interrupt):私有外设中断,编号16-31。每个CPU核独享,比如每个核的本地定时器。
  • SPI(Shared Peripheral Interrupt):共享外设中断,编号32-1019。这是咱们最常用的,UART、I2C、SPI都挂在这里。

关键点:设备树里写的interrupts属性,通常就是SPI号。但注意,设备树里的编号是从0开始算的,而GIC硬件编号是从32开始。所以设备树里写“UART0中断号=33”,实际对应GIC的SPI #1。

我见过不少新手,在设备树里写中断号时直接抄手册里的GIC编号,结果驱动死活不触发中断。其实就是没搞懂这个偏移。

4.4 中断触发方式:边沿还是电平?

触发方式,决定了中断信号怎么被识别。两种主流方式:

  • 边沿触发(Edge-triggered):信号从低到高(上升沿)或从高到低(下降沿)时触发一次。适合“事件通知”场景。
  • 电平触发(Level-triggered):信号保持高电平或低电平时持续触发。适合“状态指示”场景。
触发方式 优点 缺点 典型场景
边沿触发 不会重复触发,适合短脉冲 容易丢失,如果CPU没来得及响应 按键、GPIO中断
电平触发 信号持续有效,不容易丢 需要硬件清除,否则一直触发 UART接收、DMA完成

我曾经在一个UART驱动里,把接收中断配成了边沿触发。结果数据量一大,就丢字节。后来改成电平触发,配合FIFO读取,问题才解决。说白了,UART这种连续数据流,用电平触发更靠谱。

4.5 GIC中断处理流程

一个完整的中断处理,从硬件到软件,大概走这么几步:

  1. 外设产生中断信号,送到GIC的Distributor
  2. Distributor根据优先级和路由配置,选一个CPU目标
  3. GIC的CPU Interface向CPU核发送IRQ信号
  4. CPU保存现场,跳转到中断向量表
  5. Linux内核的通用中断处理层(IRQ domain)开始工作
  6. 调用注册的中断处理函数(handler)
  7. 处理完成后,写EOI(End Of Interrupt)寄存器,通知GIC

这个流程里,第7步最容易忘。我早期写驱动时,经常handler执行完了,没写EOI,结果系统一直进中断,根本跑不了别的任务。

注意:在GICv2里,EOI写在CPU Interface的GICC_EOIR寄存器。在GICv3里,通过系统寄存器ICC_EOIR0_EL1操作。别搞混了,否则中断永远清不掉。

4.6 在设备树中配置中断

咱们做驱动开发,中断配置通常写在设备树里。一个典型的UART节点长这样:

uart0: serial@10000000 {
    compatible = "my-company,uart";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
    interrupt-parent = <&gic>;
};

interrupts属性的三个字段:

  • 第一个0:表示SPI类型(0=SPI,1=PPI)
  • 第二个33:中断号(硬件SPI #1)
  • 第三个4:触发方式(1=上升沿,2=下降沿,4=高电平,8=低电平)

我个人习惯,每次改设备树后,都会在/sys/kernel/debug/irq/目录下确认一下中断号是否注册成功。这个目录里能看到每个中断的触发次数,调试时特别好用。

4.7 知识结构图:GIC中断体系

下面这张图,帮你把GIC的整体脉络理清楚。从外设到CPU,每一层做什么,一目了然。

ARM GIC 中断处理体系结构 UART 外设 定时器外设 GPIO 外设 GIC Distributor 优先级仲裁 · 中断路由 · 使能/屏蔽 GIC CPU Interface IRQ/FIQ信号生成 · EOI确认 · 优先级掩码 ARM CPU Core 中断向量表 · 保存现场 · 执行handler 中断号:SGI(0-15) | PPI(16-31) | SPI(32-1019)  触发方式:边沿触发 / 电平触发

4.8 调试技巧:怎么看中断有没有跑起来?

驱动写好了,中断就是不触发。怎么办?我分享几个实用方法。

方法一:看/proc/interrupts

这个文件会列出所有注册的中断,以及每个CPU上的触发次数。你敲一下UART,看对应中断号的计数有没有增加。

cat /proc/interrupts
           CPU0       CPU1
 33:       1023          0     GICv2  serial

如果计数不动,说明中断根本没来。要么硬件没拉信号,要么GIC没配通。

方法二:用ftrace跟踪中断延迟

想看看中断从触发到处理花了多久?ftrace可以帮你。

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo irq_handler_entry > /sys/kernel/debug/tracing/set_event
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 触发中断后
cat /sys/kernel/debug/tracing/trace

我曾经用这个方法,发现一个UART中断从触发到handler执行,竟然花了200微秒。后来优化了GIC的优先级和CPU亲和性,降到了20微秒以内。

小技巧:调试阶段,可以在中断handler里加一个GPIO翻转。用示波器看GPIO波形,就能直观看到中断响应时间和处理时长。这比看log更直接。

4.9 避坑指南

最后,总结几个我踩过的坑:

  • 中断号偏移:设备树里写中断号时,记得减去16(PPI)或32(SPI)的硬件偏移。
  • 触发方式选错:UART接收用边沿触发,高波特率下必丢数据。用电平触发。
  • 忘记写EOI:handler执行完,一定要写EOI寄存器。否则GIC认为中断还在处理,不会再发新中断。
  • 中断共享冲突:多个设备共享同一个中断号时,handler里要判断是哪个设备触发的。别一股脑全处理。

嗯,中断这东西,说难不难,说简单也不简单。关键是理解GIC这个“调度台”怎么工作。搞懂了它,后面UART的中断收发,你就知道该怎么配、怎么写了。