Linux 中断简介
I.MX 中断机制
1、中断向量表
中断向量表中存放着中断向量。中断服务程序的入口地址或存放中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个终端被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程序的最前面。Cortex-A7 内核有 8 个异常中断,这8个异常中断向量如下所示:
| 向量地址 | 中断类型 | 中断模式 |
|---|---|---|
| 0x00 | 复位中断 (Reset) | 特权模式 (SVC) |
| 0x04 | 未定义指令中断 (Undefined Instruction) | 未定义指令中断模式 (Undef) |
| 0x08 | 软件中断 (Software Interrupt, SWI) | 特权模式 (SVC) |
| 0x0C | 指令预取中止中断 (Prefetch Abort) | 中止模式 |
| 0x10 | 数据访问中止中断 (Data Abort) | 中止模式 |
| 0x14 | 未使用 (Not Used) | 未使用 |
| 0x18 | IRQ 中断 (IRQ Interrupt) | 外部中断模式 (IRQ) |
| 0x1C | FIQ 中断 (FIQ Interrupt) | 快速中断模式 (FIQ) |
中断向量表里面都是终端服务函数的入口地址,因此一款芯片中有什么中断都是可以从中断向量表中看出来的。在上面七个中断中,我们最常用的就是复位中断和 IRQ 中断。
2、GIC 控制器
当 GPIC 接收到外部中断信号以后就会报给 ARM 内核,但是 ARM 内核只提供了四个信号给 GIC 来汇报,这四个信号的含义如下:
VFIQ:虚拟快速 FIQ。
VIRQ:虚拟外部 IRQ。
FIQ:快速中断 IRQ。
IRQ:外部中断 IRQ。
VFIQ 和 VIRQ 都是针对虚拟化的,我们不学习虚拟化,剩下的就是 FIQ 和 IRQ。GICV2 的总体框图如下

如图所示,左侧部分就是中断源,中该那部分是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。在中间部分,GIC 将众多的中断源分为三类:
①、SPI,共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,外部中断都属于 SPI 中断(并非 SPI 通讯协议)。比如按键中断、串口中断等等非,这些中断所有的 Core 都可以处理,不限定特定的 Core。
②、PPI,私有中断,GIC 是支持多核的,每个核肯定有自己独有的中断,这些独有的终端肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI,软件中断,由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通讯。
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0-ID1019。这 1020 个 ID 包含了 PPI、SPI和 SGI。ID0-15,这16个 ID 分配给 SGI,ID16-31 分配给 PPI,ID32-1019 分配给 SPI。
GIC 的架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。这两个逻辑块的含义如下:
Distributor(分发器端):从上图可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上。分发器收集所有的中断源,可以控制每个中断的优先级。分发器端主要做的工作如下:
1、全局中断使能控制
2、控制每一个中断的使能或关闭
3、设置每一个中断的优先级
4、设置每个中断的目标处理器列表
5、设置每个外部中断的触发模式:电平触发或边沿触发
6、设置每个中断属于组 0 还是组 1
**CPU Interface(CPU 接口端)**:CPU 接口端是与 CPU Core 相连接的,因此在图中每个 CPU Core都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端就是分发器和 CPU Core 之间的桥梁,其主要工作如下:
1、使能或关闭发送的 CPU Core 的中断请求信号
2、应答中断
3、通知中断处理完成
4、设置优先级掩码
5、定义抢占策略
6、当多个中断来时,选择优先级最高的中断通知给 CPU Core
3、中断使能
中断使能包括两部分,一个是 IRQ 或 FIQ 总中断使能,另一个就是 ID0-1019 这 1020 个中断源的使能。
IRQ 和 FIQ 总中断使能:寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。
ID0-1019 中断使能和禁止:GIC 寄存器 GICD_ISENABLERn】 和 GICD_ICENABLERn 用来完成外部终端的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断的使能,那么就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个 GICD_ICENABLER 寄存器来完成中断的禁止。其中 GICD_ISENABLER0 的 bit[15:0]对应 ID150 的 SGI 中断,GICD_ISENABLER0 的 bit[31:16]对应 ID3116 的 PPI 中断。剩下的 GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
4、中断优先级
Cortex-A7 的中断优先级分为抢占优先级和子优先级。抢占优先级决定的是是否允许打断正在运行的中断处理程序,如果一个新的中断到来,它的抢占优先级比当前正在执行的中断更高,那么 CPU 会立刻暂停当前中断处理,转去处理新的中断。处理完再回来继续。子优先级则完全不同,它不能打断正在执行的中断,只用于解决多个中断同时 pending 时谁先处理。
GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高。Cortex-A7 选择了 32 个优先级,在使用终端的时候需要初始化 GICC_PMR 寄存器,此寄存器来决定使用几级优先级,其寄存器只有低八位有效,表格如下,I.MX6U 是 Cortex-A7 内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000。
| bit7:0 | 含义 |
|---|---|
| 11111111 | 256 个优先级 |
| 11111110 | 128 个优先级 |
| 11111100 | 64 个优先级 |
| 11111000 | 32 个优先级 |
| 11110000 | 16 个优先级 |
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的,GICC_BPR 寄存器只有低三位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如下表所示:
| Binary Point | 抢占优先级域 | 子优先级域 | 描述 |
|---|---|---|---|
| 0 | [7:1] | [0] | 7级抢占优先级,1级子优先级 |
| 1 | [7:2] | [1:0] | 6级抢占优先级,2级子优先级 |
| 2 | [7:3] | [2:0] | 5级抢占优先级,3级子优先级 |
| 3 | [7:4] | [3:0] | 4级抢占优先级,4级子优先级 |
| 4 | [7:5] | [4:0] | 3级抢占优先级,5级子优先级 |
| 5 | [7:6] | [5:0] | 2级抢占优先级,6级子优先级 |
| 6 | [7:7] | [6:0] | 1级抢占优先级,7级子优先级 |
| 7 | 无 | [7:0] | 0级抢占优先级,8级子优先级 |
前面已经说过了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。具体要用某个中断的时候就可以设置其优先级为 0 - 31。某个中断 ID 的中断优先级设置由寄存器 D_IPRIORITYR 来完成,前面说了 Cortex-A7 用了 512 个中断 ID ,每个中断 ID 配有一个优先级寄存器所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置 ID40 中断的优先级为 5,示例代码如下:
GICD_IPRIORITYR[40] = 5 << 3;
Linux 中断 API 函数
我们先回顾一下裸机中中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数,在 IRQ 终端服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
在 Linux 内核中提供了大量的中断相关的 API 函数,如下:
1、request_irq 函数
在 Linux 内核中想要使用某个中断是需要申请的,这个函数用于申请中断。值得注意的是,request_irq 函数可能会导致睡眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用此函数。request_irq 会使能中断,不需要再次手动激活,此函数原型如下:
1 | request_irq(unsigned int irq, |
函数的参数和返回值含义如下:
irq:表示要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件 include/linux/interrupt.h 里查看所有的中断标志,这里我们说几个常用的,如下表所示:
| 标志 | 描述 |
|---|---|
| IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq 函数的 dev 参数就是唯一标识分配的标志。 |
| IRQF_ONESHOT | 单次中断,中断执行一次就结束。 |
| IRQF_TRIGGER_NONE | 无触发。 |
| IRQF_TRIGGER_RISING | 上升沿触发。 |
| IRQF_TRIGGER_FALLING | 下降沿触发。 |
| IRQF_TRIGGER_HIGH | 高电平触发。 |
| IRQF_TRIGGER_LOW | 低电平触发。 |
比如我们的开发板上的 KEY0 使用的是 GPIO1_IO18,按下 KEY0 以后为低电平,因此我们可以设置为下降沿触发,也就是将 flags 设置为 IRQF_TRIGGER_FALLING。多种标志可以通过 “|” 实现组合。
name:中断名,设置以后可以在 /proc/interrupts 文件中看到对应的中断名。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handlr_t 的第二个参数。
返回值:0中断申请成功,其他负值申请失败,如果返回 -EBUSY 表示中断已被申请。
2、free_irq 函数
使用中断的时候使用 request_irq 函数进行申请,使用完成后就需要通过 free_irq 函数释放相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并禁止中断。free_irq 函数原型如下:
1 | void *free_irq(unsigned int, void *); |
函数的参数和返回值含义如下
irq:要释放的中断。
dev:如果中断设置为共享的话,此函数用来区分具体的中断。共享中断只有在释放掉最后的中断处理函数后才会被禁止。
3、中断处理函数
使用 request_irq 函数申请中断时需要设置中断处理函数,其格式如下:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号,第二杆参数是一个指向 void 的指针,也就是一个通用指针,需要与 request_irq 函数的 dev 参数保持一致,用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,其定义如下
1 | enum irqreturn { |
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
4、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。disable_irq 函数要等到当前执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理函数程序已经全部退出,在这种情况下可以使用另一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。这三个函数都是使能或禁止某一个中断,当我们需要开启或关闭整个中断系统的时候可以使用下面这两个函数
local_irq_enable()
local_irq_disable()
local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断系统。还有两个函数,可以在关闭中断使保存中断状态,开启中断后恢复中断状态,这样就要用到下面两个函数:
local_irq_save(flags)
local_irq_restor(flags)
这两个函数是一对,local_irq_save用于禁止中断,并将中断状态保存在 flags 中,local_irq_restore 用于恢复中断,将中断恢复到 flags 状态。
上半部与下半部
我们在使用 request_irq 申请中断时注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往不能做到,有些中断处理过程费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断后通过 IIC 读取数据。但是 IIC 通讯协议较慢,此时我们可以将通过 IIC 读取数据的操作暂后执行,中断处理函数仅仅响应中断然后清除标志位即可。这时中断处理过程分为两部分:
上半部:上半部是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以在上半部完成。
下半部:如果中断处理比较耗时,那么就将比较耗时间的代码提取出来,交给下半部执行,这样中断处理函数就会快进快出。
因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,比如在上半部中将数据宝贝到内存中,关于数据的解算就可以放到下半部执行。以下有一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他终端打断,就可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务和硬件有关,可以放到上半部。
④、除了上述三点以外的任务,有限考虑放到下半部。
上半部处理很简单,下半部该怎么做呢?Linux 内核提供了多种下半部机制。一开始 Linux 内核提供了 “bottom half” 机制来实现下半部,简称”BH”,后面引入了软中断和 tasklet 来代替 BH ,从2.5版本后的 Linux 内核中 BH 已经被抛弃了。
1、软中断
Linux 内核使用结构体 softirq_action 表示软中断,其定义在文件 include/linux/interrupt.h 中,内容如下:
1 | struct softirq_action |
在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
1 | enum |
可以看出一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。softirq_vec 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU 都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断,但是各个 CPU 所执行的软中断服务函数是相同的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,其原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
函数参数和返回值含义如下:
**nr:**要开启的软中断,在 softirq_vec 中选择一个。
**action:**软中断对应的处理函数。
注册好软中断后需要使用 raise_softirq 函数触发,raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
nr 表示要触发的软中断,可以在 softirq_vec 中选择一个。软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断,softirq_init 函数定义在 kernel/softirq.c 文件中,内容如下
1 | void __init softirq_init(void) |
从代码中可以看出,softirq_init 会默认打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ
2、tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议使用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet
1 | struct tasklet_struct |
其中,func 函数就是 tasklet 要执行的处理函数,用户自定义函数内容,相当于中断处理函数。如果要使用 tasklet,必须先定义,然后使用 tasklet_init 函数初始化,这个函数原型如下:
1 | void tasklet_init(struct tasklet_struct *t, |
函数参数含义如下:
t:要初始化的 tasklet
func:tasklet 的处理函数
data:要传递给 func 函数的参数。
也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化,这个宏定义在 include/linux/interrupt.h 文件中,定义如下
DECLARE_TASKLET(name, func, data)
其中 name 为要定义的那个 tasklet 名字,这个名字就是一个 tasklet_struct 类型的变量,func 就是 tasklet 的处理函数 data 是传递给 func 函数的参数。
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就可以让 tasklet 在合适的时间运行,此函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
t 表示要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
3、工作队列
工作队列是另一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给另一个内核线程去执行,因为工作队列工作都在进程上下文,因此工作队列允许睡眠或重新调度。因此如果要推后的工作可以睡眠就可以选择工作队列,否则就只能选择软中断或 tasklet。
Linux 内核使用 work_struct 结构体表示一个工作,内容如下:
1 | struct work_struct { |
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,这个宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,其定义如下:
#define DECLARE_WORK(n,f)
n 表示定义的工作,f表示工作对应的处理函数。和 tasklet 一样,工作也需要调度才能运行,其调度函数为 schedule_work,函数原型如下所示
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值失败。
设备树中断信息节点
如果我们使用设备树的话就需要在设备书中设置好属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断,对于终端控制器而言,设备树绑定信息参考文档为 Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件,其中的 intc 节点就是 IMX6ULL 的中断控制器节点,内容如下:
1 | intc: interrupt-controller@a01000 { |
其中,compatible 属性值为 “arm,cortex-a7-gic”,可以在 Linux 内核源码中找到 GIC 中断控制器驱动文件。#interrupt-cells 和 #address-cells、#size-cells 一样,表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息,#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整型值,对于 ARM 处理的 GIC 来说,一共有 3 个 cells,其含义如下:
第一个:中断类型,0 表示 SPI,1 表示 PPI。
第二个:中断号,对于 SPI 来说中断号范围是 0-987,对于 PPI 来说中断号范围是 0-15。
第三个:标志,bit[3:0]表示中断触发类型,为1时上升沿触发,为2时下降沿触发,为4时高电平触发,为8时低电平触发。bit[15:8] 为 PPI 中断的 CPU 掩码。
对于 GPIO 来说,gpio 节点也可以作为中断控制器,比如同文件的 gpio5 节点内容如下所示
1 | gpio5: gpio@20ac000 { |
其中,interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75。从数据手册中可以看到,GPIO5 一共用了两个中断号,一个是 74,一个是 75.其中 74 对应 GPIO5_IO00 - GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16 - GPIO5_IO31 这低 16 个 IO。interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断。
这时我们注意到,为什么 GIC 控制器和 GPIO 控制器中都有 interrupt-controller 呢?两个 #interrupt-cells 还不一样,为什么呢?在现代 SOC 中,中断系统其实是分层结构,像一颗树,GIC 在树根,GPIO 在中间层。两者都能控制终端,只是层级不同。当 GPIO 检测到信号变化时,产生一个中断事件给 GIC。这里的 interrupt-controller 表示 GPIO 也可以给其他设备提供中断。同时,Linux 需要知道如果设备使用 GPIO 产生中断的话,需要提供哪些参数,所以 GPIO 声明 #interrupt-cells = <2> 表示 一个 GPIO 中断描述需要两个参数。查阅文档得知其含义为 GPIO编号 和 触发方式。
如下代码
1 | etnphy0: ethernet-phy@0 { |
这里 etnphy0 是一个网卡设备,其中有一个中断引脚连接到了 GPIO5_IO04,下降沿触发中断。
获取中断号
编写驱动时需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
其中,dev 表示设备节点,index 表示索引号,返回值为中断号。如果使用 GPIO 的话,可以通过 gpio_to_irq 函数来获取对应的中断号,函数原型如下
int gpio_to_irq(unsigned int gpio)
其中,gpio表示要获取的 GPIO 编号,返回值为 GPIO 对应的中断号。
实验程序编写
设备树修改
本章实验将 KEY0 设置成中断模式。IMX开发板上的 KEY 使用了 UART1_CTS_B 这个 PIN,打开 imx6ull-hcw-emmc.dtsi,在 iomuxc 节点下创建一个名为 pinctrl_key 子节点,内容如下所示:
1 | pinctrl_camera_clock: cameraclockgrp { |
在根节点下添加 key 设备,如下所示
1 | key{ |
使用新编译的 dtb 文件启动 kernel,在设备树文件中看到 key 被成功创建。

程序编写
新建文件夹,创建c文件,写入以下代码。
1 | /* |
编译出 ko 文件,并加载到根文件系统中。启动并装载 ko 文件。
