1. 中断基础概念:ARM架构中断控制器(GIC)介绍、中断类型(IRQ/FIQ)、中断号与中断线
各位同学,咱们今天聊聊中断。说实话,中断是嵌入式Linux驱动开发里最核心、也最容易出问题的地方之一。我刚开始做驱动时,就被中断搞得焦头烂额——系统莫名其妙卡死,中断没响应,或者响应了但数据全乱套。后来才明白,很多坑其实是对中断基础概念理解不到位造成的。
这一章,咱们先把地基打牢。ARM架构下的中断控制器GIC、中断类型IRQ和FIQ、中断号与中断线的关系,这些搞清楚了,后面写中断处理程序才能心里有底。
1.1 什么是中断?为什么需要它?
先问个问题:CPU怎么知道外设发生了事件?比如网卡收到一个数据包,或者按键被按下。
两种方式:
- 轮询(Polling):CPU不停地去问外设“你有事吗?”——浪费CPU,效率低。
- 中断(Interrupt):外设主动告诉CPU“我有事,你快来处理!”——高效,实时。
说白了,中断就是外设给CPU发的一个“信号”,告诉CPU放下手头的事,先来处理我。处理完了,CPU再回去继续原来的工作。
核心要点:中断机制让CPU从“主动查询”变成“被动响应”,大大提高了系统效率。尤其在多任务系统中,没有中断,CPU几乎没法同时处理多个外设。
1.2 ARM架构中断控制器:GIC
ARM架构下,中断不是直接连到CPU的。中间有个“调度员”——GIC(Generic Interrupt Controller,通用中断控制器)。GIC负责接收所有外设的中断请求,然后根据优先级、亲和性等策略,分发给一个或多个CPU核心去处理。
GIC从v1发展到v4,咱们嵌入式Linux开发最常见的是GICv2和GICv3。Cortex-A7、A9这些老一些的核用GICv2,Cortex-A53、A72这些64位核用GICv3。
GIC内部主要分两大块:
- Distributor(分发器):负责中断的使能、屏蔽、优先级设置、中断号分配等管理功能。
- CPU Interface(CPU接口):每个CPU核心对应一个接口,负责把中断信号传递给具体的CPU。
我当年调试一个双核A9平台时,发现中断只在CPU0上响应,CPU1死活不响应。查了半天,原来是GIC的亲和性配置没设对,中断只路由到了CPU0。嗯,这个坑后面咱们会细讲。
1.3 中断类型:IRQ 与 FIQ
ARM架构把中断分为两种:
| 特性 | IRQ (Interrupt Request) | FIQ (Fast Interrupt Request) |
|---|---|---|
| 优先级 | 较低 | 较高(高于IRQ) |
| 中断向量地址 | 0x00000018 | 0x0000001C |
| 寄存器保存 | 需保存更多寄存器 | 有专用寄存器,保存少 |
| 典型用途 | 普通外设中断(网卡、串口等) | 高实时性需求(如DMA完成、紧急事件) |
| 中断嵌套 | 可被FIQ打断 | 不可被IRQ打断 |
FIQ的设计初衷是“快”。它有自己的影子寄存器(banked registers),进入中断时不需要保存那么多上下文。但说实话,我在实际项目中很少用FIQ。为什么?因为Linux内核的通用中断框架主要围绕IRQ设计,FIQ的支持需要额外处理,而且大多数外设的实时性要求IRQ就够用了。
我的建议:除非你有非常严格的实时性要求(比如微秒级的响应),否则优先用IRQ。FIQ虽然快,但带来的复杂度不值得。我曾经在一个工控项目里尝试用FIQ处理编码器脉冲,结果调试了整整一周,最后还是换回了IRQ加优先级调整,效果一样好。
1.4 中断号与中断线
这两个概念容易混淆,咱们理清楚。
- 中断线(Interrupt Line):硬件层面的概念。外设通过物理引脚连接到GIC的某个输入线。比如GPIO中断线、SPI中断线等。
- 中断号(Interrupt Number):软件层面的概念。GIC给每个中断源分配一个唯一的编号,Linux内核用这个编号来识别和管理中断。
在GICv2中,中断号范围如下:
| 中断类型 | 中断号范围 | 说明 |
|---|---|---|
| SGI (Software Generated Interrupt) | ID0 - ID15 | 软件触发,用于核间通信(IPI) |
| PPI (Private Peripheral Interrupt) | ID16 - ID31 | 私有外设中断,每个CPU独享(如本地定时器) |
| SPI (Shared Peripheral Interrupt) | ID32 - ID1019 | 共享外设中断,可路由到任意CPU(如网卡、UART) |
举个例子:你写一个按键驱动,按键接到GPIO1_IO03上。这个GPIO在SoC内部会映射到一个SPI中断号,比如irq = 86。你在驱动里注册中断时,用的就是这个86号。
/* 伪代码示例:注册中断 */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
/* 处理按键中断 */
printk("Key pressed! irq = %d\n", irq);
return IRQ_HANDLED;
}
/* 驱动初始化 */
static int __init key_driver_init(void)
{
int irq = 86; /* 这个值来自设备树或硬件手册 */
int ret;
ret = request_irq(irq, key_irq_handler, IRQF_TRIGGER_FALLING,
"my_key", NULL);
if (ret) {
pr_err("Failed to request irq %d\n", irq);
return ret;
}
return 0;
}
注意:中断号不是随便写的。每个SoC的中断号映射都不同,一定要查芯片手册或设备树。我曾经见过有人把SPI中断号和GPIO管脚号搞混,结果注册了错误的中断号,系统直接panic。嗯,这种错误排查起来特别痛苦。
1.5 中断处理流程概览
从外设触发中断到驱动处理函数执行,中间经过了好几个环节。我画了张图,帮你理清这个流程:
整个流程说白了就是:外设发信号 → GIC收信号并决定给哪个CPU → CPU收到信号后查表 → 内核找到对应的驱动处理函数 → 执行。每一步都有讲究,后面章节咱们会逐个深入。
1.6 设备树中的中断描述
在ARM Linux中,中断信息通常通过设备树(Device Tree)来描述。看一个实际例子:
/* 设备树片段:UART中断描述 */
uart0: serial@10000000 {
compatible = "arm,pl011";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>; /* 中断描述 */
interrupt-parent = <&gic>;
};
gic: interrupt-controller@10400000 {
compatible = "arm,cortex-a9-gic";
reg = <0x10400000 0x1000>,
<0x10401000 0x100>;
interrupt-controller;
#interrupt-cells = <3>;
};
interrupts = <0 33 4> 这三个数字的含义:
- 第一个数:中断类型(0=SPI, 1=PPI)
- 第二个数:中断号(SPI从32开始,这里33对应SPI ID 33)
- 第三个数:触发方式(1=上升沿, 2=下降沿, 4=高电平, 8=低电平)
小技巧:查看系统中所有中断的分配情况,可以用 cat /proc/interrupts。这个命令会列出每个CPU核心上注册的中断号、触发次数、中断名称等信息。调试中断问题时,这是我第一个会看的命令。
1.7 本章小结
这一章咱们把中断的骨架搭起来了:
- 中断是外设通知CPU的机制,比轮询高效得多
- GIC是ARM架构的中断控制器,负责接收、路由、分发中断
- IRQ是普通中断,FIQ是快速中断,优先级更高但用得少
- 中断号是软件标识,中断线是硬件连接,两者通过GIC映射
- 设备树描述了中断的硬件信息,驱动通过中断号注册处理函数