LV005-中断子系统框架简介
一、中断子系统框架
一个完整的中断子系统框架可以分为四个层次, 由上到下分别为用户层、 通用层、 硬件相关层和硬件层, 每个层相关的介绍如下:

用户层: 用户层是中断的使用者, 主要包括各类设备驱动。 这些驱动程序通过中断相关的接口进行中断的申请和注册。 当外设触发中断时, 用户层驱动程序会进行相应的回调处理, 执行特定的操作。
通用层: 通用层也可称为框架层, 它是硬件无关的层次。 通用层的代码在所有硬件平台上都是通用的, 不依赖于具体的硬件架构或中断控制器。 通用层提供了统一的接口和功能, 用于管理和处理中断, 使得驱动程序能够在不同的硬件平台上复用。
硬件相关层: 硬件相关层包含两部分代码。 一部分是与特定处理器架构相关的代码, 比如 ARM64 处理器的中断处理相关代码。 这些代码负责处理特定架构的中断机制, 包括中断向量表、 中断处理程序等。 另一部分是中断控制器的驱动代码, 用于与中断控制器进行通信和配置。这些代码与具体的中断控制器硬件相关。
硬件层: 硬件层位于最底层, 与具体的硬件连接相关。 它包括外设与 SoC(系统片上芯片)的物理连接部分。 中断信号从外设传递到中断控制器, 由中断控制器统一管理和路由到处理器。硬件层的设计和实现决定了中断信号的传递方式和硬件的中断处理能力。
二、中断控制器 GIC
从硬件角度来看,中断由 CPU、中断控制器(Interrupt Controller),其他外设 组成。各个外设在硬件上是通过中断线(irq request line)与 CPU 相连的,在复杂的系统中,外设比较多的情况下,就需要一个 中断控制器 来协助 CPU 进行中断的处理,比如 ARM 架构下的 GIC,或者 X86 架构中的 APIC。根据外设的多少,Interrupt Controller 可以级联。
1. GIC 简介
中断控制器 GIC(Generic Interrupt Controller) 是中断子系统框架硬件层中的一个关键组件,用于管理和控制中断。 它接收来自各种中断源的中断请求, 并根据预先配置的中断优先级、 屏蔽和路由规则, 将中断请求分发给适当的处理器核心或中断服务例程。
GIC 是由 ARM 公司提出设计规范, 当前有四个版本, GIC V1-V4(V2 最多支持 8 个 ARM core,V3/V4 支持更多的 ARM core,主要用于 ARM64 服务器系统结构)。 设计规范中最常用的, 有 3 个版本 V2.0、 V3.1、 V4.1, GICv3 版本设计主要运行在 Armv8-A, Armv9-A 等架构上。 ARM 公司并给出一个实际的控制器设计参考, 比如 GIC-400(支持 GIC v2 架构)、 gic500(支持 GIC v3 架构)、 GIC-600(支持 GIC v3 和 GIC v4 架构)。 最终芯片厂商可以自己实现 GIC 或者直接购买 ARM 提供的设计。
具体 GIC 硬件的实现形态有两种,一种是在 ARM vensor 研发自己的 SOC 的时候,会向 ARM 公司购买 GIC 的 IP,这些 IP 包括的型号有:PL390,GIC-400,GIC-500。其中 GIC-500 最多支持 128 个 cpu core,它要求 ARM core 必须是 ARMV8 指令集的(例如 Cortex-A57),符合 GIC architecture specification version 3。另外一种形态是 ARM vensor 直接购买 ARM 公司的 Cortex A9 或者 A15 的 IP,Cortex A9 或者 A15 中会包括了 GIC 的实现,当然,这些实现也是符合 GIC V2 的规格。每个 GIC 版本及相应特性如下表 :
| 版本 | 关键特性 | 常用核心 |
| GICv1 | -支持最多八个处理器核心(PE) - 支持最多 1020 个中断 ID | ARM Cortex-A5 MPCore ARM Cortex-A9 MPCore ARM Cortex-R7 MPCore |
| GICv2 | - GICv1 的所有关键特性 -支持虚拟化 | ARM Cortex-A7 MPCore ARM Cortex-A15 MPCore ARM Cortex-A53 MPCore ARM Cortex-A57 MPCore |
| GICv3 | - GICv2 的所有关键特性 -支持超过 8 个处理器核心 -支持基于消息的中断 -支持超过 1020 个中断 ID - CPU 接口寄存器的系统寄存器访问 -增强的安全模型, 分离安全和非安全的 Group 1 中断 | ARM Cortex-A53MPCore ARM Cortex-A57MPCore ARM Cortex-A72 MPCore |
| GICv4 | - GICv3 的所有关键特性 -虚拟中断的直接注入 | ARM Cortex-A53 MPCore ARMCortex-A57MPCore ARM Cortex-A72 MPCore |
这里拿 RK3568(因为这里刚好有张图,比较容易理解)的举个例子,在 RK3568 上使用的 GIC 版本为 GICv3, 相应的中断控制器模型:

Tips:GIC 相关文档可以看 Learn the architecture - Generic Interrupt Controller v3 and v4, Overview、ARM Generic Interrupt Controller Architecture Specification - Version 2.0 (B.b)
GIC 中断控制器可以分为 Distributor 接口、 Redistributor 接口和 CPU 接口:
| Distributor 中断仲裁器 | 含影响所有处理器核心中断的全局设置。 包含以下编程接口 : - 启用和禁用 SPI。 - 设置每个 SPI 的优先级级别。 - 每个 SPI 的路由信息。 - 将每个 SPI 设置为电平触发或边沿触发。 - 生成基于消息的 SPI。 - 控制 SPI 的活动和挂起状态。 - 用于确定在每个安全状态中使用的程序员模型的控制(亲和性路由或遗留模型) 。 |
| Redistributor 重新分配器 | 对于每个连接的处理器核心(PE) , 都有一个重新分配器(Redistributor) 。 重新分配器提供以下编程接口: - 启用和禁用 SGI(软件生成的中断) 和 PPI(处理器专用中断) 。 - 设置 SGI 和 PPI 的优先级级别。 - 将每个 PPI 设置为电平触发或边沿触发。 - 将每个 SGI 和 PPI 分配给一个中断组。 - 控制 SGI 和 PPI 的状态。 - 对支持关联 LP(I 低功耗中断)的中断属性和挂起状态的内存中的数据结构进行基址控制。 - 支持与连接的处理器核心的电源管理。 |
| CPU 接口 | 每个重新分配器都连接到一个 CPU 接口。 CPU 接口提供以下编程接口: - 通用控制和配置, 用于启用中断处理。 - 确认中断。 - 执行中断的优先级降低和停用。 - 为处理器核心设置中断优先级屏蔽。 - 定义处理器核心的抢占策略。 - 确定处理器核心最高优先级的挂起中断。 |
2. 中断类型
GIC-V3 支持四种类型的中断, 分别是 SGI、 PPI、 SPI 和 LPI, 每个中断类型的介绍如下:
SGI(Software Generated Interrupt, 软件生成中断) : SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。 它通常用于处理器之间的通信, 允许一个 PE 发送中断给一个或多个指定的 PE, 中断号 ID0 - ID15 用于 SGI。
PPI(I Private Peripheral Interrupt, 私有外设中断) : 针对特定 PE 的外设中断。 不与其他 PE 共享, 中断号 ID16 - ID31 用于 PPI。
SPI(Shared Peripheral Interrupt, 共享外设中断) : 全局外设中断, 可以路由到指定的处理器核心(PE) 或一组 PE, 它允许多个 PE 接收同一个中断。 中断号 ID32 - ID1019 用于 SPI。
LPI(Locality-specific Peripheral Interrupt, 特定局部外设中断) : LPI 是 GICv3 中引入的一种中断类型, 与其他类型的中断有几个不同之处。 LPI 总是基于消息的中断, 其配置存储在内存表中, 而不是寄存器中。
| INTID 范围 | 中断类型 | 备注 |
|---|---|---|
| 0 - 15 | SGI(软件生成中断) | 每个核心分别存储 |
| 16 - 31 | PPI(私有外设中断) | 每个核心分别存储 |
| 32 - 1019 | SPI(共享外设中断) | |
| 1020 - 1023 | 特殊中断号 | 用于表示特殊情况 |
| 1024 - 8191 | 保留 | |
| 8192 及更大 | LPI(特定局部外设中断) | 上限由实现定义 |
三、中断处理的状态
中断处理的状态机如下图:

- Inactive(非活动状态) : 中断源当前未被触发。
- Pending(等待状态) : 中断源已被触发, 但尚未被处理器核心确认。
- Active(活动状态) : 中断源已被触发, 并且已被处理器核心确认。
- Active and Pending(活动且等待状态) : 已确认一个中断实例, 同时另一个中断实例正在等待处理。
每个外设中断可以是以下两种类型之一 :
边沿触发(Edge-triggered) :这是一种在检测到中断信号上升沿时触发的中断, 然后无论信号状态如何, 都保持触发状态, 直到满足本规范定义的条件来清除中断。
电平触发(Level-sensitive) :这是一种在中断信号电平处于活动状态时触发的中断, 并且在电平不处于活动状态时取消触发。
四、两个中断号?
在 linux 内核中, 我们使用 IRQ number 和 HW interrupt ID 两个 ID 来标识一个来自外设的中断:
IRQ number: CPU 需要为每一个外设中断编号, 我们称之 IRQ Number。 这个 IRQ number 是一个 虚拟的 interrupt ID, 和硬件无关, 仅仅是被 CPU 用来标识一个外设中断。 我们后面使用 imx6ull 的时候,申请的 GPIO1_IO18 对应的中断号就是 79,但是根据参考手册,上面就事 99,这就是不一样的,这里的 99 应该是下面要提到的 HW interrupt ID。
HW interrupt ID: 对于 GIC 中断控制器而言, 它收集了多个外设的 interrupt request line 并向上传递, 因此, GIC 中断控制器需要对外设中断进行编码。 GIC 中断控制器用 HW interrupt ID 来标识外设的中断。 如果只有一个 GIC 中断控制器, 那 IRQ number 和 HW interrupt ID 是可以一一对应的, 如下图

但如果是在 GIC 中断控制器级联的情况下, 仅仅用 HW interrupt ID 就不能唯一标识一个外设中断, 还需要知道该 HW interrupt ID 所属的 GIC 中断控制器( HW interrupt ID 在不同的 Interrupt controller 上是会重复编码的) :

这样, CPU 和中断控制器在标识中断上就有了一些不同的概念, 但是, 对于驱动开发者而言, 我们和 CPU 视角是一样的, 我们只希望得到一个 IRQ number, 而不关系具体是那个 GIC 中断控制器上的那个 HW interrupt ID。 这样一个好处是在中断相关的硬件发生变化的时候, 驱动软件不需要修改。 因此, linux kernel 中的中断子系统需要提供一个将 HW interrupt ID 映射到 IRQ number 上来的机制, 也就是 irq domain。
Linux kernel 中使用 IRQ domain 来描述一个中断控制器所管理的中断源。也就是说,每个中断控制器都有自己的 domain,可以将 IRQ Domain 看作是 Interrupt controller 的软件抽象。
这里的 Interrupt controller 并不仅仅是指传统意义上的中断控制器,如 GIC,也可以代表一种“虚拟”的中断控制器,如 GPIO 控制器。GPIO 控制器也可以注册一个 IRQ domain 来管理 GPIO 中断,所以它也可以实现成为一个虚拟的中断控制器。
五、从源码上看看这个框架
1. irqchip_init()
按照这个调用关系可以找到 irqchip_init() 函数:start_kernel()→ init_IRQ()→ irqchip_init()(有设备树的情况)
void __init irqchip_init(void)
{
of_irq_init(__irqchip_of_table);
acpi_probe_device_table(irqchip);
}2. __irqchip_of_table
__irqchip_of_table 是什么?这里有它的声明:
extern struct of_device_id __irqchip_of_table[];以通用的 irq-gic.c - drivers/irqchip/irq-gic.c 为例,通过 IRQCHIP_DECLARE 这个宏定义若干个静态的 struct of_device_id 常量:
编译系统会把所有的 IRQCHIP_DECLARE 宏定义的数据放入到一个特殊的 section 中(__irqchip_of_table), 我们称这个特殊的 section 叫做 irq chip table,这个 table 也就保存了 kernel 支持的所有的中断控制器的 ID 信息:
#if defined(CONFIG_OF) && !defined(MODULE)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
#else
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__attribute__((unused)) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
#endif
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)我们以下面这个 cortex_a15_gic 为例看一下:
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);对应的 compatible 为 "arm, gic-400",data 字段这是 gic_of_init() 函数。我们打开我们的 imx6ul.dtsi,找到对应的设备树节点为:
intc: interrupt-controller@a01000 {
compatible = "arm,gic-400", "arm,cortex-a7-gic";
interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
#interrupt-cells = <3>;
interrupt-controller;
interrupt-parent = <&intc>;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x2000>,
<0x00a04000 0x2000>,
<0x00a06000 0x2000>;
};3. of_irq_init()
of_irq_init() 函数定义如下:
void __init of_irq_init(const struct of_device_id *matches)
{
const struct of_device_id *match;
struct device_node *np, *parent = NULL;
struct of_intc_desc *desc, *temp_desc;
struct list_head intc_desc_list, intc_parent_list;
INIT_LIST_HEAD(&intc_desc_list);
INIT_LIST_HEAD(&intc_parent_list);
//在所有的 device node 中寻找定义了 interrupt-controller 属性的中断控制器节点并匹配 of_device_id ,形成树状结构
for_each_matching_node_and_match(np, matches, &match) {
if (!of_property_read_bool(np, "interrupt-controller") ||
!of_device_is_available(np))
continue;
//......
desc = kzalloc(sizeof(*desc), GFP_KERNEL);//分配内存
if (WARN_ON(!desc)) {
of_node_put(np);
goto err;
}
//data 既是 IRQCHIP_DECLARE 里的最后一个参数,宏展开既是 of_device_id 的 data 字段, of_device_id 的第二个参数既是 compatible, 用来和 dts 匹配的
desc->irq_init_cb = match->data;
desc->dev = of_node_get(np);
desc->interrupt_parent = of_irq_find_parent(np);
if (desc->interrupt_parent == np)
desc->interrupt_parent = NULL;
list_add_tail(&desc->list, &intc_desc_list); //挂入链表
}
/*
* The root irq controller is the one without an interrupt-parent.
* That one goes first, followed by the controllers that reference it,
* followed by the ones that reference the 2nd level controllers, etc.
*/
//从根节点开始,依次递进到下一个 level 的 interrupt controller
while (!list_empty(&intc_desc_list)) {
/*
* Process all controllers with the current 'parent'.
* First pass will be looking for NULL as the parent.
* The assumption is that NULL parent means a root controller.
*/
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
int ret;
//最开始的时候 parent 变量是 NULL,确保第一个被处理的是 root interrupt controller。在处理完 root node 之后,parent 变量被设定为 root interrupt controller,因此,第二个循环中处理的是所有 parent 是 root interrupt controller 的 child interrupt controller。也就是 level 1(如果 root 是 level 0 的话)的节点。
if (desc->interrupt_parent != parent)
continue;
list_del(&desc->list);
of_node_set_flag(desc->dev, OF_POPULATED);
pr_debug("of_irq_init: init %pOF (%p), parent %p\n",
desc->dev,
desc->dev, desc->interrupt_parent);
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);//执行初始化函数, 即 gic_of_init
if (ret) {
of_node_clear_flag(desc->dev, OF_POPULATED);
kfree(desc);
continue;
}
/*
* This one is now set up; add it to the parent list so
* its children can get processed in a subsequent pass.
*/
list_add_tail(&desc->list, &intc_parent_list);//处理完的节点放入 intc_parent_list 链表,后面会用到
}
/* Get the next pending parent that might have children */
desc = list_first_entry_or_null(&intc_parent_list,
typeof(*desc), list);
if (!desc) {
pr_err("of_irq_init: children remain, but no parents\n");
break;
}
list_del(&desc->list);//每处理完一个节点就会将该节点删除,当所有的节点被删除,整个处理过程也就是结束了
parent = desc->dev;
kfree(desc);
}
list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
list_del(&desc->list);
kfree(desc);
}
err:
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
list_del(&desc->list);
of_node_put(desc->dev);
kfree(desc);
}
}在 machine driver 初始化的时候会调用 of_irq_init() 函数,在该函数中会扫描所有 interrupt controller 的节点,并调用适合的 interrupt controller driver 进行初始化,根据里面配置的级联中断信息按顺序做好映射的工作。毫无疑问,初始化需要注意顺序,首先初始化 root,然后 first level,second level,最后是 leaf node。
4. gic_of_init()
继续跟踪 desc-> irq_init_cb 的回调函数 gic_of_init():
int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *cpu_base;
void __iomem *dist_base;
u32 percpu_offset;
int irq;
dist_base = of_iomap(node, 0);//获取 reg 地址空间,GIC Distributor 的寄存器地址,负责连接系统中所有的中断源
WARN(!dist_base, "unable to map gic dist registers\n");
cpu_base = of_iomap(node, 1);//获取 reg 地址空间,GIC CPU interface 的寄存器地址,
WARN(!cpu_base, "unable to map gic cpu registers\n");
if (gic_cnt == 0 && !gic_check_eoimode(node, &cpu_base))
static_key_slow_dec(&supports_deactivate);
if (of_property_read_u32(node, "cpu-offset", &percpu_offset))
percpu_offset = 0;
__gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset,
&node->fwnode);//里面会注册 domain
if (!gic_cnt)
gic_init_physaddr(node);
if (parent) {//root GIC 不会执行到这里, 因为其 parent 是 null
irq = irq_of_parse_and_map(node, 0);//解析 second GIC 的 interrupts 属性,并进行 mapping,返回 IRQ number
gic_cascade_irq(gic_cnt, irq);//设置 handler
}
gic_cnt++;
return 0;
}值得说明的是,root GIC 不会执行执行 irq_of_parse_and_map 函数,关于 irq_of_parse_and_map 函数,我们放到后面叙述。
六、说明
后面的部分有点多,感觉不是现在学习的重点,可以参考下面的文章:
嵌入式 Linux 驱动笔记(二十七)------中断子系统框架分析_irq: type mismatch-CSDN 博客
Linux kernel 的中断子系统之(二):IRQ Domain 介绍
linux kernel 的中断子系统之(七):GIC 代码分析
Linux 中断子系统框架流程详解(基于 Kernel 3.16,arm,设备树)_linux 硬件中断详细流程处理视频-CSDN 博客