2、中断控制器架构:GIC/APIC工作原理、中断优先级管理、中断路由
好,咱们接着聊中断控制器。说实话,这是整个中断系统的“大脑”。没有它,CPU就是个聋子瞎子。
我最早接触中断控制器,是在做一款工业控制板的时候。当时用的还是老式的8259A,那玩意儿只能管8个中断源,扩展起来还得级联。后来转到ARM平台,第一次看到GIC的寄存器手册,说实话,头都大了。但啃下来之后,发现这东西的设计思路其实非常清晰。
2.1 GIC:ARM世界的标准答案
GIC,全称Generic Interrupt Controller。ARM搞出来的,现在基本是ARM架构的标配。
GIC的核心思想,就是把中断源、中断分发、中断处理这三件事分开。它内部主要有两大块:分发器(Distributor)和CPU接口(CPU Interface)。
- 分发器:负责收集所有中断源,管理它们的优先级,然后决定发给哪个CPU核。
- CPU接口:每个CPU核都有一个。它负责把分发器送来的中断,真正“怼”到CPU面前。
我个人习惯把GIC想象成一个“总机接线员”。分发器就是那个接电话的,CPU接口就是那个转接给具体分机的。
2.1.1 中断类型:三种“客人”
GIC把中断分成了三类,我当年刚学时觉得有点绕,后来发现其实很好理解:
| 类型 | ID范围 | 特点 | 我遇到过的坑 |
|---|---|---|---|
| PPI(私有外设中断) | ID 16-31 | 每个CPU核独享,比如每个核自己的定时器 | 曾经把PPI配成了SPI,结果所有核都收到了同一个定时器中断,乱套了 |
| SPI(共享外设中断) | ID 32-1019 | 所有CPU核共享,比如网卡、UART | 这个最常用,但要注意路由配置,不然中断全挤在CPU0上 |
| LPI(本地特定中断) | ID 8192+ | 基于消息的中断,用于GICv3/v4 | 这个我还在摸索,主要是为了虚拟化场景 |
我的小技巧:调试时,先确认中断ID属于哪一类。如果是PPI,检查是不是配错了CPU掩码。如果是SPI,检查路由表。80%的问题出在这两个地方。
2.2 APIC:x86世界的“老大哥”
说完了ARM,咱们看看x86。x86用的是APIC,全称Advanced Programmable Interrupt Controller。
APIC分两种:Local APIC和I/O APIC。Local APIC每个CPU核一个,I/O APIC通常挂在南桥或者芯片组里。
我记得第一次调APIC的时候,被它的“中断重定向表”搞懵了。每个I/O APIC有24个引脚,每个引脚都可以独立配置中断向量、触发方式、目标CPU。说白了,就是一张24行的表格,每行告诉你:这个引脚来的中断,该发给谁,用什么优先级。
2.2.1 中断优先级管理
APIC的优先级管理,核心是任务优先级寄存器(TPR)和处理器优先级寄存器(PPR)。
- TPR:软件可以写,用来屏蔽低优先级中断。
- PPR:硬件自动计算,等于max(TPR, 当前正在服务的中断优先级)。
为什么会这样设计?你想想看,如果CPU正在处理一个高优先级中断,这时候来了个低优先级的,CPU肯定不想理它。PPR就是干这个的——自动帮你挡住低优先级的中断。
我曾经踩过的坑:在写设备驱动时,忘了在中断处理函数里更新TPR。结果导致同一个中断反复触发,CPU占用率直接飙到100%。后来查了三天才发现,是TPR没及时恢复。嗯,从那以后,我每次写中断处理函数,第一件事就是检查TPR的保存和恢复。
2.3 中断路由:谁该处理这个中断?
中断路由,说白了就是决定“这个中断到底发给哪个CPU核”。
在多核系统里,这是个大学问。我见过不少项目,所有中断都往CPU0上怼,结果CPU0忙死,其他核闲死。这其实是个负载均衡的问题。
2.3.1 GIC的路由方式
GIC支持两种路由模式:
- 固定路由:指定中断发给某个特定CPU核。适合PPI这种私有中断。
- 动态路由:根据CPU核的负载情况,自动选择最空闲的核。适合SPI这种共享中断。
我个人习惯,对于实时性要求高的中断(比如电机控制),用固定路由,绑定到某个专用核。对于网络、存储这类吞吐型中断,用动态路由,让GIC自己调度。
2.3.2 APIC的路由方式
APIC的路由,主要通过I/O APIC的重定向表来实现。每个表项里有个目标模式字段:
- 物理模式:直接指定目标CPU的APIC ID。
- 逻辑模式:通过目标地址寄存器(DDR)来匹配一组CPU。
逻辑模式的好处是,你可以把几个CPU核编成一个组,中断来了,组里的核轮流处理。这其实就是最简单的负载均衡。
核心要点:中断路由不是越复杂越好。对于嵌入式实时系统,我建议优先考虑固定路由+优先级管理。先把确定性做出来,再考虑负载均衡。很多实时性出问题,都是因为中断被路由到了错误的核上。
2.4 中断优先级管理:谁先谁后?
优先级管理,是中断控制器的“裁判”。它决定了当多个中断同时到达时,谁先被处理。
GIC和APIC都支持多级优先级。GIC支持256级(0-255),APIC也类似。数值越小,优先级越高(嗯,这个和直觉相反,但ARM和x86都是这么设计的)。
2.4.1 优先级分组
GIC有个很实用的功能:优先级分组。你可以把256级优先级分成若干组,每组对应一个抢占级别。
举个例子:
- 组0:优先级0-63,最高,用于系统致命错误
- 组1:优先级64-127,用于实时任务
- 组2:优先级128-191,用于普通外设
- 组3:优先级192-255,用于低优先级后台任务
这样设计的好处是,组内的中断可以互相嵌套,但组间的中断不能随意抢占。说白了,就是给中断分了“阶级”。
我的建议:在项目初期,先把中断优先级分组定好。我见过太多项目,开发到一半才发现中断优先级不够用,或者嵌套太深导致栈溢出。提前规划,能省很多事。
2.5 实战:配置一个简单的中断路由
说了这么多理论,咱们来点实际的。假设你有一个UART中断(SPI类型,ID=33),你想把它路由到CPU1上,优先级设为128。
在GICv2上,配置代码大概长这样:
// 1. 配置中断优先级
GICD_IPRIORITYR[33] = 0x80; // 优先级128
// 2. 配置中断目标CPU
GICD_ITARGETSR[33] = 0x02; // 只发给CPU1 (bit1置位)
// 3. 配置触发方式
GICD_ICFGR[33] = 0x02; // 边沿触发
// 4. 使能中断
GICD_ISENABLER[33] = 0x01; // 使能中断33
// 5. 在CPU1上使能该中断
GICC_PMR = 0xF0; // 设置CPU1的优先级掩码
GICC_IAR; // 读取IAR,确认中断已就绪
这段代码看着简单,但我当年第一次写的时候,漏了第5步。结果中断死活不触发。查了半天,发现是CPU1的优先级掩码设得太低,把中断给屏蔽了。嗯,这种低级错误,犯过一次就不会再犯了。
2.6 小结
中断控制器架构,说白了就是三件事:收中断、管优先级、发中断。GIC和APIC虽然实现细节不同,但核心思想是一样的。
我个人觉得,理解中断控制器的最好方法,就是动手配一次。找个开发板,写个简单的驱动,把中断从CPU0路由到CPU1,再调调优先级。跑通了,你就真正懂了。
下一章,咱们聊聊中断向量表和中断服务程序。那又是另一个有意思的话题了。