2. Linux内核中断子系统¶
前面的Pinctrl子系统和GPIO子系统读取引脚电平实验,依托内核定时器周期性扫描按键电平,虽能实现功能, 但存在CPU空转占用率高、响应延迟大的核心弊端,尤其在多任务嵌入式Linux系统中,会大幅浪费系统算力。 而本章节介绍的Linux中断子系统,正是内核为解决“外设主动通知、内核异步响应”设计的核心基础子系统, 是构建高效、低功耗外设驱动的关键支撑,也是内核驱动开发中必须掌握的核心模块。
2.1. 中断子系统框架¶
中断是指CPU正常运行期间,由于内外部事件或程序预先安排的事件,引起的 CPU暂时停止正在运行的程序, 转而为该内部或外部预先安排的事件服务的程序中去,服务完毕后再返回去继续执行被暂时中断的程序。
2.1.1. 中断硬件描述¶
中断硬件主要有三种器件参与,各外设、中断控制器和CPU之间的关系如下图:
提示
在ARMv8体系结构中,将处理器处理事务的抽象过程定义为PE(Processing Element),可以将PE简单理解为处理器核心
各组件作用说明如下:
外设:在发生中断事件的时候,通过外设上的电气信号向中断控制器请求处理。
中断控制器:由于中断数量越来越多,需要一个专门设备来管理中断,中断控制器就是起这个作用,它连接外设中断系统和CPU系统的桥梁,接收外部中断,处理后向处理器上报中断信号。以ARM为例,GIC中断控制器接收外部中断信号以后,经过处理标记为FIQ、IRQ等,然后传输给arm内核。
CPU:CPU的主要功能是运算,CPU并不处理中断优先级,只是接收中断控制器的中断信息,运行中断处理。例如:对于ARM,CPU会接收到是IRQ和FIQ信号,分别让ARM进入IRQ mode和FIQ mode。
2.1.2. 中断软件框架¶
Linux内核中断子系统的软件部分通常划分为以下4个核心部分:
1、通用中断处理模块
内核核心层,位于内核源码/kernel/irq/目录,是中断子系统的核心框架,提供统一的中断管理接口和通用处理逻辑:
提供标准化API(request_irq()、free_irq()等)给上层驱动,屏蔽硬件细节;
维护中断描述符(irq_desc)和中断动作链表(irqaction);
实现中断流控(电平/边沿触发处理、嵌套管理)和中断亲和性调度;
负责中断上下部调度,如软中断、tasklet、工作队列管理。
2、体系架构相关处理层
arch层,位于内核源码/arch/xxx/kernel/目录,处理特定CPU架构的中断特性:
实现CPU异常向量表和中断入口(汇编代码),处理CPU与中断控制器的交互;
管理CPU本地中断状态(使能/禁用中断、中断栈设置);
处理架构相关的中断上下文切换和异常处理流程;
提供架构特定的中断初始化和硬件抽象接口。
3、中断控制器驱动层
irqchip层,抽象各类中断控制器(如GIC、APIC、VIC)的硬件操作,通过irq_chip结构体封装底层硬件访问:
实现中断控制器的基本操作:中断屏蔽、清除、触发、优先级设置等;
管理中断号映射(irq_domain机制),将硬件中断号(hwirq)转换为内核全局IRQ号;
支持多级中断控制器拓扑结构,适配复杂系统中的中断路由;
典型实现:GICv3驱动(arm64)、IO-APIC驱动(x86)等。
4、外设驱动层
用户层,位于驱动程序中,是中断的最终使用者,通过通用 API 注册中断处理函数:
调用通用中断API申请/释放中断资源,绑定中断服务例程(ISR);
实现设备特定的中断处理逻辑;
区分紧急处理(顶半部,原子操作)和延迟处理(底半部,软中断/工作队列);
无需关注底层硬件细节,实现跨平台移植性。
2.1.3. 中断常见的概念¶
下面列举一些中断常见的概念:
硬件中断ID(HW interrupt ID)
硬件中断ID就是GIC硬件上的irq ID,是GIC标准定义的,在GIC小节会讲。
中断号(IRQ number)
中断号是软件方面定义,和硬件无关,CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,仅仅是被CPU用来标识一个外设中断。
中断域(IRQ domain)
中断域在内核中是一个比较常见的概念,也就是将某一类资源划分成不同的区域,相同的域下共享一些共同的属性, domain实际上也是对模块化的一种体现,irq domain的产生是因为GIC对于级联的支持。实际上,虽然内核对每个GIC都设立了不同的域,但是它只做了一件事:负责GIC中hwirq和逻辑irq的映射。
中断向量表
中断向量表就是中断向量的列表,中断向量表在内存中存放,表中存放着中断源所对应的中断处理程序的入口地址。
中断处理架构
中断会打断内核进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。实际中,中断需要处理程序往往不是短小的, 对于一些繁重的中断服务程序,内核中断处理程序将分解为两部分:中断顶半部和中断底半部。
中断顶半部
中断顶半部也称中断上半部,中断服务函数本身,负责快速响应硬件、标记事件、退出中断,避免阻塞其他中断。
中断底半部
中断底半部也称中断下半部,处理中断耗时逻辑,顶半部触发后延后执行。
2.2. GIC v3中断控制器简介¶
ARM多核处理器里最常用的中断控制器是GIC, GIC是Generic Interrupt Controller的缩写,提供了灵活的和可扩展的中断管理方法,支持单核系统到数百个大型多芯片设计的核心。 主要作用就是接受硬件中断信号,通过一定的设置策略,然后分发给对应的CPU进行处理。
GIC是由ARM公司提出设计规范,当前有四个版本,GIC v1~v4。设计规范中最常用的,有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 V3基本结构以及实现方法,更详细的介绍可以参考 《Arm® Generic Interrupt Controller Architecture Specification》 。
2.2.1. GIC v3中断类型¶
GIC v3处理的不同种中断源:
SGI (Software Generated Interrupt):软件触发的中断,软件可以通过写 GICD_SGIR寄存器来触发一个中断事件,一般用于核间通信,内核中的IPI:inter-processor interrupts 就是基于SGI。
PPI (Private Peripheral Interrupt):私有外设中断,该终端来自于外设,被特定的核处理。GIC 是支持多核的,每个核有自己独有的中断。
SPI (Shared Peripheral Interrupt):共享外设中断,所有核共享的中断。中断产生后,可以分发到某一个CPU上。
LPI (Locality-specific Peripheral Interrupt):LPI是在GICv3中引入的,并且与其他三种类型的中断具有非常不同的编程模型,LPI是基于消息的中断,它们的配置保存在表中而不是寄存器。
每个中断都有一个ID号标识,称为INTID,下面是ARM GIC v3手册的中断号规定:
INTID |
中断类型 |
说明 |
|---|---|---|
0-15 |
SGI |
每个CPU核都有自己的16个 |
16-31 |
PPI |
每个CPU核都有自己的16个 |
32-1019 |
SPI |
例如GPIO 中断、串口中断等这些外部中断,具体由SOC厂商定义 |
1020-1023 |
- |
特殊中断号 |
1024 - 8191 |
- |
保留 |
8192-MAX |
LPI |
- |
2.2.2. GIC v3基本结构¶
GIC V3.0逻辑图组成如下图所示。
如上图片所示,GIC v3主要这几部分组成:Distributor、CPU interface、Redistributor、ITS。 GIC v3中,将cpu interface从GIC中抽离,放入到了cpu中,cpu interface通过AXI Stream,与gic进行通信。 当GIC要发送中断,GIC通过AXI stream接口,给cpu interface发送中断命令,cpu interface收到中断命令后,根据中断线映射配置,决定是通过IRQ还是FIQ管脚,向cpu发送中断。
2.2.2.1. Distributor¶
Distributor用于SPI(Shared peripheral interrupts)中断的管理,具有仲裁和分发的作用,会将中断发送给Redistributor。
Distributor提供了一些编程接口或者说是寄存器,我们可以通过Distributor的编程接口实现如下操作,后面会简单介绍这些操作对应的寄存器。 下面是Distributor主要功能:
全局的开启或关闭CPU的中断。
控制任意一个中断请求的开启和关闭。
中断优先级控制。
指定中断发生时将中断请求发送到那些CPU。
interrupt属性设定,设置每个”外部中断”的触发方式(边缘触发或者电平触发)。
2.2.2.2. CPU interface简介¶
CPU接口为链接到GIC的处理器提供接口,与Distributor类似它也提供了一些编程接口,我们可以通过CPU接口实现以下功能(列举几项,详细参考arm手册):
打开或关闭 CPU interface 向连接的 CPU assert 中断事件。
中断确认(acknowledging an interrupt)。
中断处理完毕的通知。
为处理器设置中断优先级掩码。
设置处理器的抢占策略
确定挂起的中断请求中优先级最高的中断请求
简单来说,CPU接口可以开启或关闭发往CPU的中断请求,CPU中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到CPU。 在任何时候CPU都可以从(CPU接口寄存器)读取当前活动的最高优先级。
2.2.2.3. Redistributor简介¶
GICv3中,Redistributor管理SGI,PPI,LPI中断,然后将中断发送给CPU interface,包括下面功能:
启用和禁用 SGI 和 PPI。
设置 SGI 和 PPI 的优先级。
将每个 PPI 设置为电平触发或边缘触发。
将每个 SGI 和 PPI 分配给中断组。
控制 SGI 和 PPI 的状态。
内存中数据结构的基址控制,支持 LPI 的相关中断属性和挂起状态。
电源管理支持。
2.2.2.4. ITS(Interrupt translation service)¶
ITS 是GIC v3架构中的一种可选硬件机制,ITS提供了一种将基于消息的中断转换为LPI的软件机制,它是 在支持LPI的配置中可选地支持。ITS接收LPI中断,进行解析,然后发送到对应的redistributor,再由redistributor将中断信息,发送给cpu interface。
2.2.3. 中断状态和处理流程¶
每个中断都维护一个状态机,支持Inactive、Pending、Active、Active and pending。 中断处理的状态机如下图:
Inactive:无中断状态,即没有 Pending 也没有 Active。
Pending:硬件或软件触发了中断,该中断事件已经通过硬件信号通知到 GIC,等待 GIC分配的那CPU进行处理,在电平触发模式下,产生中断的同时保持Pending状态。
Active:CPU已经应答该中断请求,并且正在处理中。
Active and pending:当一个中断源处于Active状态的时候,同一中断源又触发了中断,进入pending状态,挂起状态。
一个简单的中断处理过程是:外设发起中断,发送给Distributor ,Distributor并基于它们的中断特性(优先级、是否使能等等)对中断进行分发处理,分发给合适的Redistributor, Redistributor 将中断信息,发送给 CPU interface,CPU interface产生合适的中断异常给处理器,处理器接收该异常,最后软件处理该中断。
2.2.4. GIC-600简单介绍¶
rk3568/rk3588的中断控制器是GIC-600,rk3562/rk3576的中断控制器是GIC-400,我们这里简单介绍GIC-600中断控制器,GIC-600是支持GIC v3版本,是arm一个实际的控制器设计参考。 rk3568的GIC流程框图如下:
rk3568的256个SPI中断号简单分配表(仅列举部分):
2.3. 中断驱动简单分析¶
2.3.1. 设备树中的中断信息¶
在Linux系统中已经替我们封装好了大部分的工作内容,我们需要做的只是简单地在这个框架下使用。 下面将以rk3568的中断为例讲解,简单讲解下中断控制的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
reg = <0x0 0xfd400000 0 0x10000>, /* GICD */
<0x0 0xfd460000 0 0xc0000>; /* GICR */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
its: interrupt-controller@fd440000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0xfd440000 0x0 0x20000>;
};
};
|
compatible:compatible属性用于平台设备驱动的匹配。
reg:reg指定中断控制器相关寄存器的地址及大小,GICD是Distributor寄存器,GICR是指Redistributor寄存器。
interrupt-controller:声明该设备树节点是一个中断控制器。
#interrupt-cells :指定使用该中断控制器的节点要用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。
interrupts :描述中断信息,这里是用三个u32描述,是前面#interrupt-cells指定的。第一个指定中断类型,第二个中断号,第三位是触发类型
its : 在gic设备节点下,有一个子设备节点its,ITS设备用于将消息信号中断(MSI)路由到cpu
msi-controller : 标识该设备是MSI控制器
#msi-cells :必须是1,MSI设备的DeviceID
学过前面内容的同学们想必对GIC中断控制器并不陌生,GIC架构分为了:Distributor、Redistributor、 CPU Interface,上面设备树节点就是用来描述整个GIC控制器的。
一个GIC中断控制器的使用实例,以uart3为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / {
compatible = "rockchip,rk3568";
interrupt-parent = <&gic>;
#address-cells = <2>;
#size-cells = <2>;
/*.............*/
uart3: serial@fe670000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe670000 0x0 0x100>;
interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART3>, <&cru PCLK_UART3>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 6>, <&dmac0 7>;
pinctrl-names = "default";
pinctrl-0 = <&uart3m0_xfer>;
status = "disabled";
};
/*.............*/
};
|
uart3是根节点下的一个子节点,根节点指定了interrupt-parent为gic。 那么uart3子节点也继承使用GIC控制器中断控制器,并用interrupts描述了它使用的资源。
interrupts:具体的中断描述信息,在该节点使用的中断控制器gic,gic节点中“#interrupt-cells = <3>”规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:
第一个参数用于指定中断类型,在GIC的中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。
第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-256]。
第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:
1 2 3 4 5 6 | #define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
|
其中第三个参数的[8-15]位,在PPI中断中还用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU, 一位代表一个CPU,为1则将PPI中断发送到CPU0,否则屏蔽。
如下示例:
1 2 3 4 5 6 7 8 | timer {
compatible = "arm,armv8-timer";
interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
arm,no-tick-in-suspend;
};
|
2.3.2. GIC v3 中断控制器的代码¶
中断控制器通过IRQCHIP_DECLARE 宏注册到__irqchip_of_table
1 2 | //初始化一个struct of_device_id的静态常量,并放置在__irqchip_of_table中
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);
|
系统开机初始化阶段,of_irq_init函数会去查找设备节点信息,根据__irqchip_of_table段中的”arm,gic-v3”, 匹配到前面小节的设备树节点“gic”,获取设备信息。 最终会执行IRQCHIP_DECLARE声明的回调函数gicv3_of_init。
在内核源码include/linux/irqchip.h文件中定义了IRQCHIP_DECLARE宏:
1 2 3 4 5 6 7 8 9 10 11 | #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
__aligned(__alignof__(struct of_device_id)) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
|
该宏初始化了一个struct of_device_id静态常量,放到并放置在__irqchip_of_table段中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | static int __init gicv3_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硬件的版本是GICv3或者GICv4
if (err) {
pr_err("%pOF: no distributor detected, giving up\n", node);
goto out_unmap_dist;
}
/*读取设备树节点redistributor-regions 的值,没有就是1*/
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;
}
for (i = 0; i < nr_redist_regions; i++) { //为一个 GICR 域分配基地址
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;
}
/*通过 DTS 读取 redistributor-regions 的值,没有就是0*/
if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
redist_stride = 0;
/*GIC v3初始化的核心工作*/
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); //设置PPI的亲和性
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;
}
|
函数gicv3_of_init如上所示,该函数将映射GIC寄存器基地址,然后获取GIC的版本信息(通过读GICD_PIDR2寄存器,读取寄存器bit[7:4],是0x3就是GIC v3), 获取设备树的属性值,调用gic_init_bases函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | 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 gic_irqs;
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 v3硬件设备相关的数据结构初始化*/
gic_data.fwnode = handle; //一些回调方法
gic_data.dist_base = dist_base; //Distributor内存区间的地址
gic_data.redist_regions = rdist_regs; //gic中Redistributor域的信息
gic_data.nr_redist_regions = nr_redist_regions; //Redistributor域的个数
gic_data.redist_stride = redist_stride; //Redistributor域之间的步长
/*
* Find out how many interrupts are supported.
* The GIC only supports up to 1020 interrupt sources (SGI+PPI+SPI)
*/
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer;
gic_irqs = GICD_TYPER_IRQS(typer); //获取bit[4:0]的值,算出SPI个数
if (gic_irqs > 1020)
gic_irqs = 1020;
gic_data.irq_nr = gic_irqs;
/*在系统中为GIC V3注册一个irq domain的数据结构*/
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
&gic_data);
irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_vlpis = true;
gic_data.rdists.has_direct_lpi = true;
if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
err = -ENOMEM;
goto out_free;
}
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");
}
set_handle_irq(gic_handle_irq); //设置中断回调函数,gic_handle_irq将查表等获取到是哪一个中断,处理中断
gic_update_vlpi_properties(); /*更新Redistributor相关的属性*/
if (IS_ENABLED(CONFIG_ARM_GIC_V3_ITS) && gic_dist_supports_lpis())
its_init(handle, &gic_data.rdists, gic_data.domain); //初始化ITS
gic_smp_init(); //设置核间通信等
gic_dist_init(); //初始化Distributor,
gic_cpu_init();
gic_cpu_pm_init(); //初始化GIC电源管理
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函数如上,一些解释看下上面的代码注释,源码分析到这,主要分析了下中断控制器驱动相关源码,详细的IRQ Domain映射,irq事件详细处理过程可自行参考内核源码。
2.4. 中断常用的API和重要的数据结构¶
2.4.1. 中断申请和释放函数¶
2.4.1.1. request_irq函数¶
request_irq是Linux内核中断子系统对外提供的最核心、最常用的标准API函数,专门给外设驱动程序使用,是驱动和中断硬件“打通连接”的关键入口。
函数原型:
1 2 | static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
|
参数说明:
irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。
handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。
flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 | #define IRQF_TRIGGER_NONE 0x00000000 // 无指定触发方式
#define IRQF_TRIGGER_RISING 0x00000001 // 上升沿触发
#define IRQF_TRIGGER_FALLING 0x00000002 // 下降沿触发
#define IRQF_TRIGGER_HIGH 0x00000004 // 高电平触发
#define IRQF_TRIGGER_LOW 0x00000008 // 低电平触发
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \ // 触发方式掩码
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010 // 触发方式探测
#define IRQF_SHARED 0x00000080 // 共享中断
/*-----------以下宏定义省略------------*/
|
name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。
dev:如果使用了 IRQF_SHARED 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。
返回值:
成功:返回0
失败:返回负数。
2.4.1.2. free_irq函数¶
free_irq函数是request_irq的配对函数,是内核中断子系统提供的释放中断资源的标准API,驱动卸载时必须调用,缺一不可。
1 | void free_irq(unsigned int irq, void *dev);
|
参数说明:
irq:从设备树中得到或者转换得到的中断编号。
dev:与request_irq函数中dev传入的参数一致。
2.4.1.3. devm_request_irq函数¶
此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般 不需要 在出错处理和remove()接口里再显式的释放。
函数原型:
1 2 | int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
|
2.4.2. 中断号获取函数¶
2.4.2.1. gpiod_to_irq函数¶
gpiod_to_irq函数将GPIO描述符转换为内核对应的中断号,是GPIO驱动申请中断的前置步骤。
函数原型:
1 | int gpiod_to_irq(struct gpio_desc *desc);
|
参数说明:
desc:通过devm_gpiod_get获取的GPIO描述符指针。
返回值:正整数:有效的中断号;负值:转换失败,返回错误码。
2.4.3. 中断处理函数¶
在中断申请时需要指定一个中断处理函数,书写格式如下所示。
1 | irqreturn_t (*irq_handler_t)(int irq, void * dev);
|
参数:
irq:用于指定“内核中断号”。
dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev, 直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。
返回值:
irqreturn_t类型:枚举类型变量,如下所示。
1 2 3 4 5 6 7 | enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
|
如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。 第三个参数涉及到我们后面会讲到的中断服务函数的“顶半部分”和“底半部分”, 如果在中断服务函数是使用“顶半部分”和“底半部分”实现,则应当返回IRQ_WAKE_THREAD。
2.4.4. 中断的使能和屏蔽函数¶
2.4.4.1. enable_irq和disable_irq函数¶
通过函数使能、禁用某一个中断。
1 2 | void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
|
参数:
irq:指定的“内核中断号”
返回值:无
2.4.4.2. local_irq_xxx函数¶
local_irq_xxx函数关闭或打开当前CPU的所有中断,只影响当前正在运行的CPU,不会关闭其他CPU的中断。
1 2 3 4 | void local_irq_enable(void)
void local_irq_disable(void)
void local_irq_save(unsigned long flags)
void local_irq_restore(unsigned long flags)
|
由于“全局中断”的特殊性,通常情况下关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long类型的数据。
在armv8-arch64架构下,local_irq_disable()只是操作daif标志位,关闭当前cpu的异常,与具体中断控制器无关,而disable_irq()是通过控制中断控制器实现关闭中断。
2.5. Linux中断子系统实验¶
本实验在Pinctrl子系统和GPIO子系统读取引脚电平实验基础上进行修改, 彻底摒弃此前低效的定时器轮询模式,依托Linux中断子系统实现GPIO按键事件检测, 采用边沿触发中断机制搭配定时器软件消抖方案,解决机械按键硬件抖动而误触发问题, 实现按键按下立即响应、无操作时CPU零损耗的高效驱动设计。
本章的示例代码目录为: linux_driver/18_irq_subsystem
2.5.1. 设备树插件详解¶
设备树插件完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
fragment@0 {
target-path = "/";
__overlay__ {
led_test: led_test{
compatible = "fire,led_test";
led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
button-gpios= <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PB2 IRQ_TYPE_EDGE_FALLING>;
pinctrl-names = "default";
pinctrl-0 = <&led_button_pin>;
};
};
};
fragment@1 {
target = <&pinctrl>;
__overlay__ {
led_button_test {
led_button_pin: led_button_pin {
rockchip,pins =
<0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
<1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};
};
fragment@2 {
target = <&leds>;
__overlay__ {
status = "disabled";
};
};
};
|
第17行:指定中断控制器,按键引脚属于GPIO1组,故绑定GPIO1中断控制器;
第18行:第一参数为中断引脚索引,第二参数为下降沿触发(按键按下时触发,匹配上拉配置)。
2.5.2. 驱动代码详解¶
核心数据结构定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* 消抖时间 20ms */
#define DEBOUNCE_TIME 20
/* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 自旋锁 */
spinlock_t spinlock;
/* LED 状态 */
int led_state;
/* LED 的 GPIO 描述符 */
struct gpio_desc *led_gpio;
/* 按钮的 GPIO 描述符 */
struct gpio_desc *button_gpio;
/* 消抖定时器 */
struct timer_list debounce_timer;
/* 中断号 */
int irq;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
定义20ms消抖时间,避免误触发;
自定义结构体整合字符设备、GPIO资源、自旋锁与消抖定时器、中断号,统一管理硬件与业务数据,消抖定时器专门用于中断底半部处理,实现按键电平稳定检测。
驱动初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配(省略重复代码)
/* 第一步:提取平台设备提供的资源 */
if (pdev->dev.of_node) {
/* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
if (IS_ERR(led_cdev->led_gpio)) {
ret = PTR_ERR(led_cdev->led_gpio);
printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
return ret;
}
/* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(led_cdev->button_gpio)) {
ret = PTR_ERR(led_cdev->button_gpio);
printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
return ret;
}
} else {
printk("Platform device matching is not supported in this driver\n");
return -ENOMEM;
}
//字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
/* 初始化消抖定时器 */
timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);
/* 默认关闭 LED */
led_cdev->led_state = 0;
/* 获取按键GPIO对应的中断号 */
led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
if (led_cdev->irq < 0) {
ret = led_cdev->irq;
/* 打印获取中断号失败 */
printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
/* 申请中断,下降沿触发 */
ret = devm_request_irq(&pdev->dev, led_cdev->irq, button_irq_handler,
IRQF_TRIGGER_FALLING,
"button_irq", led_cdev);
if (ret < 0) {
/* 打印申请中断失败 */
printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
return 0;
// 错误处理(省略重复代码)
}
|
第34行:初始化消抖定时器,回调函数为button_debounce_callback;
第40行:通过gpiod_to_irq将GPIO描述符转换为内核中断号
第50-52行:通过devm_request_irq申请中断,指定触发方式、中断服务函数与设备私有数据,采用devm托管式API,驱动卸载时自动释放中断资源,避免内存泄漏。
中断顶半部:中断服务函数
1 2 3 4 5 6 7 8 9 | static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;
/* 重启消抖定时器,20ms后执行回调 */
mod_timer(&led_cdev->debounce_timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME));
return IRQ_HANDLED;
}
|
该函数属于中断顶半部,运行在中断上下文,严禁执行耗时操作、打印、休眠操作, 仅负责快速启动消抖定时器,将实际业务处理交给底半部,保证中断响应实时性,不阻塞其他中断执行。
中断底半部:消抖定时器回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static void button_debounce_callback(struct timer_list *t)
{
/* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);
/* 用于保存中断状态信息 */
unsigned long flags;
/* 存储按键状态 */
int btn_val;
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取稳定后的按键电平 */
btn_val = gpiod_get_value(led_cdev->button_gpio);
/* 低电平表示按键稳定按下 */
if(btn_val == 0){
/* 翻转LED电平状态 */
gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 按键按下打印日志 */
if(btn_val == 0){
printk(KERN_INFO "按键已按下,LED状态翻转\n");
}
}
|
该函数属于中断底半部,允许执行耗时操作,核心作用是等待20ms抖动结束后,再次读取按键电平,确认是有效按下后再翻转LED,解决硬件抖动问题。
2.5.3. 编译设备树和驱动¶
此部分和Pinctrl子系统和GPIO子系统读取引脚电平实验完全一致不作过多说明。
编译得到设备树插件lubancat-led-overlay.dtb、驱动模块irq_subsystem.ko。
2.5.4. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
2.5.4.1. 实验操作¶
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #加载驱动
sudo insmod irq_subsystem.ko
#信息输出如下
[ 70.570580] led platform driver init
[ 70.571410] led platform driver probe
[ 70.571607] major=236, minor=0
# 查看中断号
cat /proc/interrupts | grep button_irq
#信息打印如下
105: 349 0 0 0 gpio1 10 Edge button_irq
#信息说明如下
#105:内核中断号
#349:按键触发总次数,中断处理函数执行了349次
#gpio1 10:硬件引脚,按键接在 GPIO1_B2
#Edge:触发方式,IRQF_TRIGGER_RISING/FALLING
#button_irq:中断名字
|
使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:
1 2 3 4 | [ 83.795412] 按键已按下,LED状态翻转
[ 84.868826] 按键已按下,LED状态翻转
[ 86.078850] 按键已按下,LED状态翻转
[ 87.078942] 按键已按下,LED状态翻转
|
除了内核打印信息,也可以看到每一次连接GND引脚模拟按键按下都可以看到LED从亮转灭或从灭转亮, 说明中断触发正常;没有多次打印按键按下和LED频繁亮灭,说明定时器消抖功能正常。
2.5.5. 实验注意事项¶
中断触发方式必须与设备树一致:驱动中IRQF_TRIGGER_FALLING需与设备树IRQ_TYPE_EDGE_FALLING完全匹配,否则中断无法触发;
中断上下文严禁耗时操作:中断服务函数内禁止copy_to/from_user、休眠、延时操作,仅能做快速标记;
按键引脚必须配置上下拉:禁止悬空,否则电平不稳定,会误触发中断,本实验采用内置上拉,必须配置;
消抖时间合理设置:建议10-20ms,过短无法消抖,过长响应迟钝;
优先使用devm托管API:devm_request_irq、devm_gpiod_get等API,自动释放资源,避免内存泄漏;
中断号不可硬编码:必须通过gpiod_to_irq动态获取,禁止手动指定中断号,保证驱动兼容性。