文章目录
1. 中断的概念和作用
当计算机的CPU需要在执行任务的同时响应外部事件时,中断是一种重要的机制。中断是异步事件的一种形式,这是指发起事件与处理事件之间的时间间隔没有固定的模式,而是在不同的时间点发生的。
在计算机系统中,中断可分为软件中断和硬件中断两类。在软件中断中,中断是由CPU执行的一条特殊指令造成的,它用于暂停CPU的正常执行流程,然后转而执行一个内部事件或处理程序。硬件中断则是由硬件设备发出的,它与处理器无关,并向CPU发出中断请求信号。
中断的作用主要有以下几个方面:
-
响应外部事件:当外部事件发生时,中断能够及时响应并引导CPU执行事件处理程序,保证事件能够得到及时处理。
-
提高系统效率:CPU无需主动地去轮询外部设备是否有数据需要处理,中断机制可以使CPU在处理器时间的同时响应必要的事件处理。
-
处理复杂任务:某些外部事件处理需要一段复杂的代码,中断处理程序可以协助CPU快速完成这些复杂任务。
-
进行设备驱动:硬件设备的驱动需要中断的支持,来达到与CPU通讯和协调工作的目的。
2. Linux中断处理机制
Linux操作系统具有广泛的应用,它的中断处理机制是实时、高效和可扩展的。Linux中断处理机制采用了事件驱动模型,对中断按照优先级进行排队,以确保最优先处理高优先级事件,同时保证低优先级事件不会被阻塞。下面从中断请求、中断处理和中断完成三个方面介绍Linux中断处理机制。
2.1 中断请求
在Linux中,中断被认为是分离的、独立的事件,因此中断的处理必须是实时的,使用优先级表进行调度和排序。Linux内核实现了基于可插拔中断体系结构的抽象机制,从而可以在处理器和设备机制之间提供更好的耦合性和深入性。这种体系结构允许中断请求线共享,提高了系统的可扩展性和可靠性,提升了系统的响应速度和性能表现。
当一个硬件设备需要处理器的处理时,它将通过硬件 Intline 请求一个中断。中断请求将被系统中断控制器传递,并被处理器接收。 Linux 支持多个中断请求,通常使用 PCI 设备接口来实现。当控制器接收到中断请求后,它会将请求传递给 Linux 中的 IRQ 子系统,于是就有了一个中断。
2.2 中断处理
中断处理是指系统响应中断事件的过程,它通常包含以下几个步骤:
- 中断调度:中断请求被发送到CPU中心处理器上的中断控制器后,由硬件中断控制器将中断信号传递到内核。Linux内核根据中断向量号找到对应的中断处理程序。
- 应答中断:内核通过设置正确的输入 / 输出引脚响应中断,这样中断控制器就可以发送指令将中断信息传递给 CPU。
- 中断上下文的保存:当内核处理中断时,必须保存当前进程运行的上下文,为后续恢复工作做好准备。
- 中断处理程序运行:中断程序处理中断,执行需要的中断服务程序,并最终返回到主线程。
- 中断恢复工作:恢复中断执行前的上下文,这包括无序通知的应用程序。
2.3 中断完成
当中断处理完成后,处理器会向中断控制器发送完成中断的信号。中断控制器将中断信号发送回硬件设备。在中断完成的过程中,主要包含以下几个步骤:
-
中断状态恢复:在中断处理过程中,处理器会保存中断处理程序执行前的所有中断状态,包括进程的上下文、寄存器和状态字等。因此,在中断处理程序执行结束后,处理器需要通过对这些状态进行恢复,来重新运行之前被中断的进程。
-
设备控制器响应:当设备控制器接收到中断完成信号后,将在下一个可用的时钟周期中恢复设备操作,以继续处理新的输入或输出请求。此时,处理器将会准备新的请求或服务,以响应下一次中断请求。
中断完成是中断处理机制的最后一个阶段,通过该阶段可以使得设备控制器继续其后续的工作,同时为下一次中断请求做好准备。
2.4.中断触发和处理步骤详解
我们以gpio中断位例子讲解一下arm64中断的完整流程,先看看cpu到gpio的硬件上是怎么样的一个关系:
看到上面这个图片我们可以看到中断触发的流程:外部设备(比如按键)→GPIO控制器→GIC→CPU。
当触发流程走到cpu后,cpu会跳转到中断异常向量表,执行后面的操作:读取GIC信息,了解到是GPIO触发的中断,再读取GPIO控制器的信息,了解到是某一个GPIO的外设导致的,执行这个外设的中断处理函数。
看到上面这个简单的过程,感觉还是很不清晰。
arm64中断初始化,cpu接受到中断后为什么会自动跳转到异常向量表呢?
这是因为linux初始化的时候,会把异常向量表vectors的地址写入到vbar_el1寄存器中,我们linux内核触发了中断,则会自动跳转到vbar_el1这个这个地址上运行。具体初始化可以查看linux内核启动分析(一)。
我们再看看异常向量表是怎么样的,linux的异常向量表代码在arm64\kernel\entry.S:
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
SYM_CODE_END(vectors)
2.4.1 异常向量表的解读
其实,在 ARM64 体系结构中,异常分为同步异常和异步异常。
同步异常是试图执行指令时生成的异常,或是作为指令的执行结果生成的异常。同步异常包括如下。
- 系统调用。异常级别 0 使用 svc指令陷入异常级别 1,异常级别1 使用hv指令陷入异常级别2,异常级别 2 使用 smc指令陷入异常级别 3。
- 数据中止,即访问数据时的页错误异常,虚拟地址没有映射到物理地址,或者没有写权限。
- 指令中止,即取指令时的页错误异常,虚拟地址没有映射到物理地址,或者没有执行权限。
- 栈指针或指令地址没有对齐。
- 没有定义的指令。
- 调试异常。
异步异常不是由正在执行的指令生成的,和正在执行的指令没有关联。异步异常包括以下。
- 中断(normal priority interrupt,IRQ),即普通优先级的中断。
- 快速中断(fast interrupt,FIQ),即高优先级的中断。
- 系统错误(System Error,SError),是由硬件错误触发的异常,例如最常见的是把脏数据从缓存行写回内存时触发异步的数据中止异常。
当异常发生的时候,处理器需要执行异常的处理程序。存储异常处理程序的内存位置称为异常向量,通常把所有异常向量存放在一张表中,称为异常向量表。对于 ARM64 处理器的异常级别 1、2 和 3,每个异常级别都有自己的异常向量表,异常向量表的起始虚拟地址存放在寄存器 VBAR_ELn(向量基准地址寄存器,Vector Based Address Register)中。每个异常向量表有 16 项,分为 4 组,每组 4项,每项的长度是 128 字节(可以存放32 条指令)中。
我们看到有4组,每一组的4项都是分别表示发生了同步异常,irq,firq和系统错误。这4组的区别就是,第一组表示异常发生在EL0,处理异常的特权等级也是EL0。第二组表示异常发生在ELn(n可以为1,2,3),处理异常的特权等级也是ELn(n可以为1,2,3),但是这里是linux内核,所以我们的特权为EL1,我们可以理解为异常发生在EL1,处理异常的特权等级也是EL1。这两组的共同点是异常的发生和处理在同一个特权级别,不需要进行特区那级别的切换;而后面两组则是异常的发生和处理不在同一个特权级别,需要进行特区那级别的切换。在linux这里就是说,第三组和第四组表示异常发生在EL0,但是异常处理却在EL1,而他们的区别就是,第三组表示异常发生在64位环境下,第四组表示异常发生在32位环境下。
我们现在知道这4组,16项的含义了,我们看fiq都是invalid的,是因为linux不支持fiq,所以没有对fiq异常进行处理。我们也看到第一组都是invalid的,是因为EL0发生了异常,不会在EL0处理,会陷入EL1处理,所以第一组也不需要实现。
第二组第一项kernel_ventry 1, sync ,在这里就是b el1_sync ,也就是跳转到el1_sync函数了。到这里,我们把每一项支持的异常向量跳转函数写成表格就是:
异常向量 | 跳转的函数 |
---|---|
kernel_ventry 1, sync | el1_sync |
kernel_ventry 1, irq | el1_irq |
kernel_ventry 1, error | el1_error |
kernel_ventry 0, sync | el0_sync |
kernel_ventry 0, irq | el0_irq |
kernel_ventry 0, error | el0_error |
kernel_ventry 0, sync_compat, 32 | el0_sync_compat |
kernel_ventry 0, irq_compat, 32 | el0_irq_compat |
kernel_ventry 0, error_compat, 32 | el0_error_compat |
也就是说linux在用户态触发中断会执行el0_irq,在内核态触发中断会执行el1_irq,这两个的代码看下面:
SYM_CODE_START_LOCAL_NOALIGN(el0_irq)
kernel_entry 0
el0_irq_naked:
el0_interrupt_handler handle_arch_irq
b ret_to_user
SYM_CODE_END(el0_irq)
SYM_CODE_START_LOCAL_NOALIGN(el1_irq)
kernel_entry 1
el1_interrupt_handler handle_arch_irq
kernel_exit 1
SYM_CODE_END(el1_irq)
我么可以看到他们都是执行函数handle_arch_irq函数,最后返回各自的状态。handle_arch_irq在下面的gic控制器的初始化过程中会设置的。
3. GICv3中断控制器
GICv3(Generic Interrupt Controller Version 3)是一种基于ARM Cortex-A架构的中断控制器,它提供了高度灵活和可扩展的中断架构,适用于多核系统和虚拟化环境中,它还提供对TrustZone安全性扩展的支持。
GICv3 控制器接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。GICv3 还支持虚拟中断,允许将虚拟机中的中断映射到真实机器上,并为每个虚拟处理器提供独立的中断处理能力。
GICv3 中,中断处理程序可以以不同的模式运行:Secure EL1、Non-secure EL1、Secure EL2 和 Non-secure EL2。这使得 GICv3 在处理不同的安全级别和特权级别时,有更强的灵活性和可定制性。
在GICv3中,中断被分为三种类型:软件生成中断(SGI)、私有中断(PPI)和共享中断(SPI)。下面对这三种中断类型进行简要的介绍:
-
软件生成中断(SGI):SGI是由处理器上的软件或操作系统生成的中断,用于在特定的CPU上触发中断。它们是非可屏蔽中断(NMI),具有固定的优先级和中断号。在ARMv8-A体系结构中,每个处理器都支持4个SGI中断线,其中SGI 0用于调试控制,其他SGI可以用于特定的事件处理。
-
私有中断(PPI):PPI是由处理器上的硬件设备或系统外设生成的中断。PPI是由每个处理器的本地中断控制器(LPI)处理并响应的,因此不会发送到网络中的其他处理器。PPI的中断号和优先级是固定的,但不保证在所有处理器上相同,因为它们由LPI分配。
-
共享中断(SPI):SPI是由GICv3的分发器(Distributor)处理分配给多个处理器的中断。SPI可以由设备连接到任意处理器中的任意一个或多个中断信号线,因此在多核系统中,分配给不同处理器的SPI可以在生成它的设备上具有不同的优先级和中断号。
GICv3包括了Distributor和Redistributor子系统。Distributor子系统接收来自外部设备的中断请求,进行中断的分类处理,再分配到不同的Redistributor进行处理。Redistributor子系统接收来自Distributor的中断请求,将其分配到不同的CPU处理器进行响应。GICv3 可以通过软件配置来实现中断控制器的定制化和协同工作。这些寄存器包括:
- Distributor registers:用于将中断向量路由到正确的 CPU 指令队列和 CPU 核中。也就是GICD_的系列寄存器。
- Redistributor registers:用于将来自 Distributor 的中断路由到对应的 CPU 核。也就是GICR_的系列寄存器。
- CPU interface registers:用于在 CPU 上启用、禁用、优先级排序和处理中断。也就是GICC_的系列寄存器。
当外部设备发出中断请求时,请求被分配给Distributor子系统,由该子系统根据优先级进行分类。分类后的中断请求发送给不同的Redistributor子系统,再由Redistributor子系统将中断请求发送给对应的CPU处理器进行处理。处理器通过在向量地址映射到对应的中断服务程序,来响应中断请求。
GICv3控制器会记录中断的状态,中断可以处于多种不同状态:
① 非活动状态(Inactive)–这意味着该中断未触发。
② 挂起(Pending)–这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。
③ 活动(Active)–描述了一个已被内核接收并正在处理的中断。
④ 活动和挂起(Active and pending)–描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。
中断的优先级和可接收中断的核都在分发器(distributor)中配置。外设发给分发器的中断将标记为pending状态(或Active and Pending状态,如触发时果状态是active)。distributor确定可以传递给CPU核的优先级最高的pending中断,并将其转发给内核的CPU interface。通过CPU interface,该中断又向CPU核发出信号,此时CPU核将触发FIQ或IRQ异常。
作为响应,CPU核执行异常处理程序。异常处理程序必须从CPU interface寄存器查询中断ID,并开始为中断源提供服务。完成后,处理程序必须写入CPU interface寄存器以报告处理结束。然后CPU interface准备转发distributor发给它的下一个中断。在处理中断时,中断的状态开始为pending,active,结束时变成inactive。中断状态保存在distributor寄存器中。
3.1 GICv3中断控制器设备树
gic: interrupt-controller@30800000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
reg = <0x0 0x30800000 0 0x20000>, /* GICD */
<0x0 0x30880000 0 0x80000>, /* GICR */
<0x0 0x30840000 0 0x10000>, /* GICC */
<0x0 0x30850000 0 0x10000>, /* GICH */
<0x0 0x30860000 0 0x10000>; /* GICV */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_LOW>;
its: gic-its@30820000 {
compatible = "arm,gic-v3-its";
msi-controller;
reg = <0x0 0x30820000 0x0 0x20000>;
};
};
这是飞腾的gic的设备树,跟瑞芯微的差不多,不过瑞芯微的的reg只有GICD和GICR,虽然飞腾的设备树多了这个多寄存器,实际上驱动也是没有去读取那些寄存器的。its的我们不用看,这是PCIE才用到的。
3.2 GICv3中断控制器驱动
驱动代码在drivers/irqchip/irq-gic-v3.c文件中,首先我们看看compatible 是怎么匹配的:
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
展开后为:
static const struct of_device_id __of_table_gic_v3 \
__used __section(__irqchip_of_table) \
= { .compatible = "arm,gic-v3", \
.data = gic_of_init }
那么gic_of_init 函数是怎么调用到的呢?其调用路径如下:
start_kernel (init\main.c) → init_IRQ (arch\arm64\kernel\irq.c) → irqchip_init (drivers\irqchip\irqchip.c) → of_irq_init(__irqchip_of_table);(drivers\of\irq.c) → desc->irq_init_cb = match->data;和 ret = desc->irq_init_cb(desc->dev,desc->interrupt_parent);
前面的调用路径都好理解,就是start_kernel一直调用到 of_irq_init这个函数,我们的宏申明了__irqchip_of_table这个段,所以在 of_irq_init这个函数里面会遍历整个设备树,找到所有compatible匹配的节点,并且判断是否为interrupt-controller,是否有data成员,如果都有,则把date成员赋值给desc->irq_init_cb,最后调用desc->irq_init_cb。这就会让"arm,gic-v3"的date成员gic_of_init函数调用起来,精简过的of_irq_init函数如下:
void __init of_irq_init(const struct of_device_id *matches)
{
...
for_each_matching_node_and_match(np, matches, &match) {
if (!of_property_read_bool(np, "interrupt-controller") ||
!of_device_is_available(np))
continue;
if (WARN(!match->data, "of_irq_init: no init function for %s\n",
match->compatible))
continue;
...
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
desc->irq_init_cb = match->data;
...
}
while (!list_empty(&intc_desc_list)) {
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);
...
}
}
}
现在我们看看gic_of_init函数主要是获取设备树中的信息,然后初始化gic控制器:
static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *dist_base;
struct redist_region *rdist_regs;
u64 redist_stride;
u32 nr_redist_regions;
int err, i;
dist_base = of_iomap(node, 0);//获取GICD的地址,并且进行了映射
if (!dist_base) {
pr_err("%pOF: unable to map gic dist registers\n", node);
return -ENXIO;
}
err = gic_validate_dist_version(dist_base);//验证gic版本
if (err) {
pr_err("%pOF: no distributor detected, giving up\n", node);
goto out_unmap_dist;
}
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
nr_redist_regions = 1;
rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),
GFP_KERNEL);
if (!rdist_regs) {
err = -ENOMEM;
goto out_unmap_dist;
}
//redistributor-regions不存在,nr_redist_regions值为1,获取GICR的地址
for (i = 0; i < nr_redist_regions; i++) {
struct resource res;
int ret;
ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i);
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %d\n", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start;
}
if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
redist_stride = 0;
gic_enable_of_quirks(node, gic_quirks, &gic_data);
//初始化GIC控制器,设置GICD等寄存器来配置NMI、SGI、PPI和SPI中断
err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
redist_stride, &node->fwnode);
if (err)
goto out_unmap_rdist;
gic_populate_ppi_partitions(node);
if (static_branch_likely(&supports_deactivate_key))
gic_of_setup_kvm_info(node);
return 0;
out_unmap_rdist:
for (i = 0; i < nr_redist_regions; i++)
if (rdist_regs[i].redist_base)
iounmap(rdist_regs[i].redist_base);
kfree(rdist_regs);
out_unmap_dist:
iounmap(dist_base);
return err;
}
gic_of_init主要做了一下几件事:
- 获取GICD的地址,并且进行了映射,映射地址存到dist_base
- 调用函数gic_validate_dist_version验证gic版本
- 获取GICR的地址存到rdist_regs数组中
- 调用函数gic_init_bases初始化GIC控制器,设置GICD等寄存器来配置NMI、SGI、PPI和SPI中断
我们再看看gic_init_bases函数:
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
u32 typer;
int err;
if (!is_hyp_mode_available())
static_branch_disable(&supports_deactivate_key);
if (static_branch_likely(&supports_deactivate_key))
pr_info("GIC: Using split EOI/Deactivate mode\n");
//设置全局变量gic_data
gic_data.fwnode = handle;
gic_data.dist_base = dist_base;
gic_data.redist_regions = rdist_regs;
gic_data.nr_redist_regions = nr_redist_regions;
gic_data.redist_stride = redist_stride;
/*
* Find out how many interrupts are supported.
*/
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer;
gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),
gic_quirks, &gic_data);
pr_info("%d SPIs implemented\n", GIC_LINE_NR - 32);
pr_info("%d Extended SPIs implemented\n", GIC_ESPI_NR);
/*
* ThunderX1 explodes on reading GICD_TYPER2, in violation of the
* architecture spec (which says that reserved registers are RES0).
*/
if (!(gic_data.flags & FLAGS_WORKAROUND_CAVIUM_ERRATUM_38539))
gic_data.rdists.gicd_typer2 = readl_relaxed(gic_data.dist_base + GICD_TYPER2);
//创建并且初始化domain,最重要的是gic_irq_domain_ops方法
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
&gic_data);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_rvpeid = true;
gic_data.rdists.has_vlpis = true;
gic_data.rdists.has_direct_lpi = true;
gic_data.rdists.has_vpend_valid_dirty = true;
if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
err = -ENOMEM;
goto out_free;
}
irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);
gic_data.has_rss = !!(typer & GICD_TYPER_RSS);
pr_info("Distributor has %sRange Selector support\n",
gic_data.has_rss ? "" : "no ");
if (typer & GICD_TYPER_MBIS) {
err = mbi_init(handle, gic_data.domain);
if (err)
pr_err("Failed to initialize MBIs\n");
}
//设置handle_arch_irq为gic_handle_irq,cpu触发irq就会跑到gic_handle_irq
set_handle_irq(gic_handle_irq);
gic_update_rdist_properties();
gic_dist_init();//初始化GICD分发器,配置控制器的中断路由
gic_cpu_init();//初始化GICR,配置PPI,
gic_smp_init();//初始化SGI中断
gic_cpu_pm_init();//初始化CPU的GIC控制器用于中断的电源管理特性
if (gic_dist_supports_lpis()) {
its_init(handle, &gic_data.rdists, gic_data.domain);
its_cpu_init();
} else {
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(handle, gic_data.domain);
}
gic_enable_nmi_support();//初始化NMI中断
return 0;
out_free:
if (gic_data.domain)
irq_domain_remove(gic_data.domain);
free_percpu(gic_data.rdists.rdist);
return err;
}
gic_init_bases主要做了一下几件事:
- 设置全局变量gic_data,
- 创建并且初始化domain,
- 调用函数set_handle_irq设置handle_arch_irq为gic_handle_irq,handle_arch_irq就是上面异常向量表的未知函数
- 调用函数gic_dist_ini初始化GICD分发器,配置控制器的中断路由
- 调用函数gic_cpu_ini初始化GICR,配置PPI,
- 调用函数gic_smp_ini初始化SGI中断
- 调用函数 gic_cpu_pm_init初始化CPU的GIC控制器用于中断的电源管理特性
- 调用函数 gic_enable_nmi_suppor初始化NMI中断
到这里,这个gic控制器的初始化流程就讲完了。
4. GIC的下一级中断控制器
4.1 设备树
gpio0: gpio0@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
这里是瑞芯微3568的gpio0模块,瑞芯微每个gpio有32个引脚。
4.2 内核对设备树的处理
linux内核的设备树处理函数入口是drivers/of/platform.c文件的of_device_alloc函数,这个函数会在平台设备遍历设备树注册成平台设备的过程中被调用:
struct platform_device *of_device_alloc(struct device_node *np,
const char *bus_id,
struct device *parent)
{
struct platform_device *dev;
int rc, i, num_reg = 0, num_irq;
struct resource *res, temp_res;
//分配平台设备
dev = platform_device_alloc("", PLATFORM_DEVID_NONE);
if (!dev)
return NULL;
/* count the io and irq resources */
while (of_address_to_resource(np, num_reg, &temp_res) == 0)
num_reg++;
num_irq = of_irq_count(np);//统计节点使用irq的次数
/* Populate the resource table */
if (num_irq || num_reg) {
res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);
if (!res) {
platform_device_put(dev);
return NULL;
}
dev->num_resources = num_reg + num_irq;
dev->resource = res;
for (i = 0; i < num_reg; i++, res++) {
//把设备树地址转换为资源
rc = of_address_to_resource(np, i, res);
WARN_ON(rc);
}
//根据设备节点中的中断信息, 构造中断资源
if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
pr_debug("not all legacy IRQ resources mapped for %pOFn\n",
np);
}
dev->dev.of_node = of_node_get(np);//把node节点保存到deb中
dev->dev.fwnode = &np->fwnode;
dev->dev.parent = parent ? : &platform_bus;//保存父节点
if (bus_id)
dev_set_name(&dev->dev, "%s", bus_id);
else
of_device_make_bus_id(&dev->dev);
return dev;
}
of_device_alloc主要做了以下一些工作:
- 调用函数platform_device_alloc分配平台设备
- 调用函数of_irq_count统计节点使用irq的次数
- 遍历reg资源,调用of_address_to_resource函数把设备树地址转换为资源
- 调用函数of_irq_to_resource_table根据设备节点中的中断信息, 构造中断资源
- 设置平台设备的父节点、节点和名字等信息
我们主要关注of_irq_to_resource_table函数:
int of_irq_to_resource_table(struct device_node *dev, struct resource *res,
int nr_irqs)
{
int i;
for (i = 0; i < nr_irqs; i++, res++)
if (of_irq_to_resource(dev, i, res) <= 0)
break;
return i;
}
of_irq_to_resource_table函数主要是遍历全部irq,然后调用of_irq_to_resource解析节点中的中断信息,和构造中断资源。我们看of_irq_to_resource这个函数:
int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)
{
int irq = of_irq_get(dev, index);
if (irq < 0)
return irq;
/* Only dereference the resource if both the
* resource and the irq are valid. */
if (r && irq) {
const char *name = NULL;
memset(r, 0, sizeof(*r));
/*
* Get optional "interrupt-names" property to add a name
* to the resource.
*/
of_property_read_string_index(dev, "interrupt-names", index,
&name);
r->start = r->end = irq;
r->flags = IORESOURCE_IRQ | irqd_get_trigger_type(irq_get_irq_data(irq));
r->name = name ? name : of_node_full_name(dev);
}
return irq;
}
of_irq_to_resource主要是调用of_irq_get函数找到对应节点的中断号,同时找到其父节点,创建映射关系,最后保存为resource。我们看看of_irq_get:
int of_irq_get(struct device_node *dev, int index)
{
int rc;
struct of_phandle_args oirq;
struct irq_domain *domain;
//解析设备树中的中断信息, 保存在of_phandle_args结构体中
rc = of_irq_parse_one(dev, index, &oirq);
if (rc)
return rc;
domain = irq_find_host(oirq.np);
if (!domain)
return -EPROBE_DEFER;
return irq_create_of_mapping(&oirq);//创建中断映射
}
of_irq_get主要做了两件事:
- 调用函数of_irq_parse_one解析设备树中的中断信息, 保存在of_phandle_args结构体中
- 调用函数irq_create_of_mapping创建中断映射
我们继续看irq_create_of_mapping:
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
//根据irq_data的信息填充irq_fwspec结构体
of_phandle_args_to_fwspec(irq_data->np, irq_data->args,
irq_data->args_count, &fwspec);
//创建从fwspec到IRQ号的映射关系
return irq_create_fwspec_mapping(&fwspec);
}
irq_create_of_mapping函数首先根据irq_data的信息填充irq_fwspec结构体,然后调用函数irq_create_fwspec_mapping创建从fwspec到IRQ号的映射关系。irq_create_fwspec_mapping函数:
unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
struct irq_domain *domain;
struct irq_data *irq_data;
irq_hw_number_t hwirq;
unsigned int type = IRQ_TYPE_NONE;
int virq;
//根据fwspec找到对应的domian
if (fwspec->fwnode) {
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
if (!domain)
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
} else {
domain = irq_default_domain;
}
if (!domain) {
pr_warn("no irq domain found for %s !\n",
of_node_full_name(to_of_node(fwspec->fwnode)));
return 0;
}
//调用irq_domain->ops的translate或xlate,把设备节点里的中断信息解析为hwirq, type
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
return 0;
/*
* WARN if the irqchip returns a type with bits
* outside the sense mask set and clear these bits.
*/
if (WARN_ON(type & ~IRQ_TYPE_SENSE_MASK))
type &= IRQ_TYPE_SENSE_MASK;
//看看这个hwirq是否已经映射, 如果virq非0,说明找到了就直接返回
virq = irq_find_mapping(domain, hwirq);
if (virq) {
/*
* If the trigger type is not specified or matches the
* current trigger type then we are done so return the
* interrupt number.
*/
if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
return virq;
/*
* If the trigger type has not been set yet, then set
* it now and return the interrupt number.
*/
if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {
irq_data = irq_get_irq_data(virq);
if (!irq_data)
return 0;
irqd_set_trigger_type(irq_data, type);
return virq;
}
pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",
hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));
return 0;
}
//来到这里说明没有找到,需要创建映射
if (irq_domain_is_hierarchy(domain)) {//如果这个是级联中断
//根据domian分配虚拟中断号
virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
if (virq <= 0)
return 0;
} else {//否则就是链式中断
//创建硬中断号和虚拟中断号的映射关系
virq = irq_create_mapping(domain, hwirq);
if (!virq)
return virq;
}
//通过虚拟中断号查找irq_data,找不到就返回
irq_data = irq_get_irq_data(virq);
if (!irq_data) {
if (irq_domain_is_hierarchy(domain))
irq_domain_free_irqs(virq, 1);
else
irq_dispose_mapping(virq);
return 0;
}
//保存中断触发类型
irqd_set_trigger_type(irq_data, type);
return virq;
}
irq_create_fwspec_mapping函数主要做了一下几件事:
- 根据fwspec找到对应的domian
- 调用函数irq_domain_translate把设备节点里的中断信息解析出hwirq, type
- 调用函数irq_find_mapping查看这个hwirq是否已经映射,如果已经映射到某个虚拟中断号,则返回;否则往下走
- 调用函数irq_domain_is_hierarchy判断是否为级联中断,如果是则调用函数irq_domain_alloc_irqs根据domian分配虚拟中断号,
- 如果不是级联中断,说明是链式中断,则调用函数irq_create_mapping创建硬中断号和虚拟中断号的映射关系
- 调用函数irq_get_irq_data通过软件中断号查找irq_data,找不到就返回
- 调用函数irqd_set_trigger_type保存中断触发类型,返回虚拟中断号
下面我们分小结讲解irq_domain_translate、irq_domain_alloc_irqs和irq_create_mapping这几个函数
4.2.1 irq_domain_translate
static int irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
irq_hw_number_t *hwirq, unsigned int *type)
{
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
if (d->ops->translate)//如果translate方法集存在,就调用它
return d->ops->translate(d, fwspec, hwirq, type);
#endif
if (d->ops->xlate)//如果xlate方法集存在,就调用它
return d->ops->xlate(d, to_of_node(fwspec->fwnode),
fwspec->param, fwspec->param_count,
hwirq, type);
//如果转换方法都不存在,则假定中断号
*hwirq = fwspec->param[0];
return 0;
}
irq_domain_translate函数做了一下几件事:
- /如果translate方法集存在,就调用translate方法后返回
- 如果xlate方法集存在,就调用xlate方法后返回
- 如果转换方法都不存在,则假定中断号
translate和xlate都是中断控制器驱动注册的时候写好的,后面会详细说。
4.2.2 irq_domain_alloc_irqs
static inline int irq_domain_alloc_irqs(struct irq_domain *domain,
unsigned int nr_irqs, int node, void *arg)
{
return __irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false,
NULL);
}
int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,
unsigned int nr_irqs, int node, void *arg,
bool realloc, const struct irq_affinity_desc *affinity)
{
int i, ret, virq;
if (domain == NULL) {
domain = irq_default_domain;
if (WARN(!domain, "domain is NULL; cannot allocate IRQ\n"))
return -EINVAL;
}
if (realloc && irq_base >= 0) {
virq = irq_base;
} else {
//分配和初始化irq_des内存
virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node,
affinity);
if (virq < 0) {
pr_debug("cannot allocate IRQ(base %d, count %d)\n",
irq_base, nr_irqs);
return virq;
}
}
//最外层的irq_data被嵌入到结构体irq_desc中
if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {
pr_debug("cannot allocate memory for IRQ%d\n", virq);
ret = -ENOMEM;
goto out_free_desc;
}
mutex_lock(&irq_domain_mutex);
//调用domain->ops->alloc申请虚拟中断号
ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
if (ret < 0) {
mutex_unlock(&irq_domain_mutex);
goto out_free_irq_data;
}
for (i = 0; i < nr_irqs; i++) {
//在IRQ域中修剪层次结构,并更新IRQ域的继承关系
ret = irq_domain_trim_hierarchy(virq + i);
if (ret) {
mutex_unlock(&irq_domain_mutex);
goto out_free_irq_data;
}
}
for (i = 0; i < nr_irqs; i++)
//将新的中断描述符加入到IRQ域中,以及将中断描述符与对应的中断控制器进行关联
irq_domain_insert_irq(virq + i);
mutex_unlock(&irq_domain_mutex);
return virq;
out_free_irq_data:
irq_domain_free_irq_data(virq, nr_irqs);
out_free_desc:
irq_free_descs(virq, nr_irqs);
return ret;
}
irq_domain_alloc_irqs直接调用函数__irq_domain_alloc_irqs,__irq_domain_alloc_irqs函数做了以下几件事:
- 调用函数irq_domain_alloc_descs分配和初始化irq_des内存
- 调用函数irq_domain_alloc_irq_data把最外层的irq_data被嵌入到结构体irq_desc中
- 调用函数irq_domain_alloc_irqs_hierarchy调用domain->ops->alloc申请虚拟中断号
- 遍历每一个虚拟中断号,调用函数irq_domain_trim_hierarchy在IRQ域中修剪层次结构,并更新IRQ域的继承关系
- 遍历每一个虚拟中断号,调用函数irq_domain_insert_irq将新的中断描述符加入到IRQ域中,以及将中断描述符与对应的中断控制器进行关联
4.2.3 irq_create_mapping
static inline unsigned int irq_create_mapping(struct irq_domain *host,
irq_hw_number_t hwirq)
{
//创建映射
return irq_create_mapping_affinity(host, hwirq, NULL);
}
unsigned int irq_create_mapping_affinity(struct irq_domain *domain,
irq_hw_number_t hwirq,
const struct irq_affinity_desc *affinity)
{
struct device_node *of_node;
int virq;
pr_debug("irq_create_mapping(0x%p, 0x%lx)\n", domain, hwirq);
/* Look for default domain if nececssary */
if (domain == NULL)
domain = irq_default_domain;
if (domain == NULL) {
WARN(1, "%s(, %lx) called with NULL domain\n", __func__, hwirq);
return 0;
}
pr_debug("-> using domain @%p\n", domain);
of_node = irq_domain_get_of_node(domain);
//通过硬中断号找软中断号
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("-> existing mapping on virq %d\n", virq);
return virq;
}
//申请软件中断号
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node),
affinity);
if (virq <= 0) {
pr_debug("-> virq allocation failed\n");
return 0;
}
//建立软中断号和硬中断号的映射关系
if (irq_domain_associate(domain, virq, hwirq)) {
irq_free_desc(virq);
return 0;
}
pr_debug("irq %lu on domain %s mapped to virtual irq %u\n",
hwirq, of_node_full_name(of_node), virq);
return virq;
}
irq_create_mapping函数直接调用irq_create_mapping_affinity,irq_create_mapping_affinity做了以下几件事:
- 调用函数irq_find_mapping通过硬中断号找软中断号,找到了就返回
- 调用函数irq_domain_alloc_descs申请软件中断号
- 调用函数irq_domain_associate建立软中断号和硬中断号的映射关系
irq_domain_associate主要是通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断号与软中断号的映射关系:
int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
irq_hw_number_t hwirq)
{
struct irq_data *irq_data = irq_get_irq_data(virq);
int ret;
if (WARN(hwirq >= domain->hwirq_max,
"error: hwirq 0x%x is too large for %s\n", (int)hwirq, domain->name))
return -EINVAL;
if (WARN(!irq_data, "error: virq%i is not allocated", virq))
return -EINVAL;
if (WARN(irq_data->domain, "error: virq%i is already associated", virq))
return -EINVAL;
mutex_lock(&irq_domain_mutex);
//初始化硬中断号,软中断号在alloc_desc->desc_set_defaults中初始化
irq_data->hwirq = hwirq;
irq_data->domain = domain;
//调用map方法进行映射
if (domain->ops->map) {
ret = domain->ops->map(domain, virq, hwirq);
if (ret != 0) {
/*
* If map() returns -EPERM, this interrupt is protected
* by the firmware or some other service and shall not
* be mapped. Don't bother telling the user about it.
*/
if (ret != -EPERM) {
pr_info("%s didn't like hwirq-0x%lx to VIRQ%i mapping (rc=%d)\n",
domain->name, hwirq, virq, ret);
}
irq_data->domain = NULL;
irq_data->hwirq = 0;
mutex_unlock(&irq_domain_mutex);
return ret;
}
/* If not already assigned, give the domain the chip's name */
if (!domain->name && irq_data->chip)
domain->name = irq_data->chip->name;
}
domain->mapcount++;
//建立了软硬中断号之间的映射关系
irq_domain_set_mapping(domain, hwirq, irq_data);
mutex_unlock(&irq_domain_mutex);
irq_clear_status_flags(virq, IRQ_NOREQUEST);
return 0;
}
irq_domain_associate做了以下几件事:
- 初始化硬中断号和domian
- 调用调用map方法进行映射
- 调用函数irq_domain_set_mapping建立了软硬中断号之间的映射关系
4.3 链式中断驱动
链式中断驱动的初始化代码都在rockchip_interrupts_register中,其调用路径为rockchip_gpio_probe→rockchip_gpiolib_register→rockchip_interrupts_register。
static int rockchip_interrupts_register(struct rockchip_pin_bank *bank)
{
unsigned int clr = IRQ_NOREQUEST | IRQ_NOPROBE | IRQ_NOAUTOEN;
struct irq_chip_generic *gc;
int ret;
bank->domain = irq_domain_create_linear(dev_fwnode(bank->dev), 32,
&irq_generic_chip_ops, NULL);
if (!bank->domain) {
dev_warn(bank->dev, "could not init irq domain for bank %s\n",
bank->name);
return -EINVAL;
}
ret = irq_alloc_domain_generic_chips(bank->domain, 32, 1,
"rockchip_gpio_irq",
handle_level_irq,
clr, 0, 0);
if (ret) {
dev_err(bank->dev, "could not alloc generic chips for bank %s\n",
bank->name);
irq_domain_remove(bank->domain);
return -EINVAL;
}
gc = irq_get_domain_generic_chip(bank->domain, 0);
if (bank->gpio_type == GPIO_TYPE_V2) {
gc->reg_writel = gpio_writel_v2;
gc->reg_readl = gpio_readl_v2;
}
gc->reg_base = bank->reg_base;
gc->private = bank;
gc->chip_types[0].regs.mask = bank->gpio_regs->int_mask;
gc->chip_types[0].regs.ack = bank->gpio_regs->port_eoi;
gc->chip_types[0].chip.irq_ack = irq_gc_ack_set_bit;
gc->chip_types[0].chip.irq_mask = irq_gc_mask_set_bit;
gc->chip_types[0].chip.irq_unmask = irq_gc_mask_clr_bit;
gc->chip_types[0].chip.irq_enable = rockchip_irq_enable;
gc->chip_types[0].chip.irq_disable = rockchip_irq_disable;
gc->chip_types[0].chip.irq_set_wake = irq_gc_set_wake;
gc->chip_types[0].chip.irq_suspend = rockchip_irq_suspend;
gc->chip_types[0].chip.irq_resume = rockchip_irq_resume;
gc->chip_types[0].chip.irq_set_type = rockchip_irq_set_type;
gc->wake_enabled = IRQ_MSK(bank->nr_pins);
/*
* Linux assumes that all interrupts start out disabled/masked.
* Our driver only uses the concept of masked and always keeps
* things enabled, so for us that's all masked and all enabled.
*/
rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->int_mask);
rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->port_eoi);
rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->int_en);
gc->mask_cache = 0xffffffff;
irq_set_chained_handler_and_data(bank->irq,
rockchip_irq_demux, bank);
return 0;
}
rockchip_interrupts_register函数主要做了以下几件事:
- 调用函数irq_domain_create_linear分配和初始化irq_domain数据
- 调用函数irq_alloc_domain_generic_chips分配和初始化irq_chip_generic数据
- 调用函数irq_get_domain_generic_chip获取irq_chip_generic指针,
- 填充irq_chip_generic->chip_types->chip的方法集
- 调用函数irq_set_chained_handler_and_data设置irq_desc[].handle_irq为rockchip_irq_demux,rockchip_irq_demux的功能是分辨是哪一个hwirq, 调用对应的irq_desc[].handle_irq。
gpio中断控制器的初始化就这么简单,这是因为很多工作在设备树被注册为平台设备的时候已经调用GIC的方法完成了初始化工作。当然,我们还要重点关注irq_generic_chip_ops结构体、irq_chip里面的方法和rockchip_irq_demux这个中断处理函数。这个东西有点长,这里不多介绍。