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中断处理流程
一个完整的中断处理,从硬件到软件,大概走这么几步:
- 外设产生中断信号,送到GIC的Distributor
- Distributor根据优先级和路由配置,选一个CPU目标
- GIC的CPU Interface向CPU核发送IRQ信号
- CPU保存现场,跳转到中断向量表
- Linux内核的通用中断处理层(IRQ domain)开始工作
- 调用注册的中断处理函数(handler)
- 处理完成后,写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,每一层做什么,一目了然。
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的中断收发,你就知道该怎么配、怎么写了。