在 Linux 内核中使用一个 int 变量表示中断号,每个中断都有一个中断号,通过中断号即可区分不同的中断。
注意:中断号取决于硬件,芯片设计时候已经固定。
在 Linux 内核中使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。
request_irq 函数会激活(使能)中断,所以不需要我们手动去使能,request_irq 函数原型如下:
int request_irq( unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev )irq:要申请中断的中断号;
handler:中断处理函数;
flags:中断标志;(上升沿、下降沿、高低电平触发,单次中断,无触发,共享中断等,这些标志可以通过“|”来实现多种组合)
name:中断名字;
dev:中断边沿设置为共享中断的时候,为了区分不同的中断,将此设备结构体传递给中断处理函数;
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经
被申请了。
为什么会休眠?
request_irq()可能会引起睡眠的原因是在注册过程中,内核需要在/proc/irq文件中创建一个与中断对应的项,函数proc_mkdir()就是用来创建这个新的procfs项的,proc_mkdir()会调用proc_create()对这个新的profs项进行设置,而proc_create()对调用kmalloc()来请求分配内存,而kmalloc()是可以睡眠的,因此request_irq()是可能会睡眠的。
使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。
void free_irq(unsigned int irq, void *dev)
irq:要释放的中断;
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉;
返回值:无。
irqreturn_t (*irq_handler_t) (int, void *)
形参1:int类型,中断处理函数对应的中断号;
形参2:void*,需要与 request_irq 函数的 dev 参数保持一致,以区分共享中断的不同设备。
返回值为 irqreturn_t 类型,枚举类型。
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证使用此函数时不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
local_irq_enable()
local_irq_disable()
以上两个函数用于打开或关闭全局中断。
local_irq_save(flags)
local_irq_restore(flags)
local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。
我们在使用 request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。
我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。
比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出。那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
哪些代码放到上半部?哪些代码放到下半部?
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部,因为中断内要暂时关闭中断,防止中断嵌套。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 内核提供了多种下半部机制,此处我们讲解三种实现方式。
Linux 内核使用结构体 softirq_action 表示软中断,softirq_action 结构体中的 action 成员变量就是软中断的服务函数。
// 软中断结构体
struct softirq_action
{
void (*action)(struct softirq_action *); // 软中断服务函数
};
在 kernel/softirq.c 文件中一共定义了软中断,
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
enum
{
HI_SOFTIRQ=0, /* 高优先级软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断 */
SCHED_SOFTIRQ, /* 调度软中断 */
HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
RCU_SOFTIRQ, /* RCU 软中断 */
NR_SOFTIRQS
};
可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,在上面10个中选择一个。
action:软中断对应的处理函数。
返回值:没有返回值。
注册好软中断以后需要通过 raise_softirq 函数触发(调度),raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
nr:要触发的软中断,在上面10个中选择一个。
返回值:没有返回值。
软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断:
void __init softirq_init(void)
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。
Linux 内核使用 tasklet_struct 结构体来表示 tasklet:
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数,相当于中断处理函数 */
unsigned long data; /* 函数 func 的参数 */
};
如果要使用 tasklet 下半部实现方式,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:
void tasklet_init( struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
返回值:没有返回值。
也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化:
DECLARE_TASKLET(name, func, data)
name 为要定义的 tasklet 名字;
func 就是 tasklet 的处理函数;
data 是传递给 func 函数的参数。
在上半部(中断处理函数)中调用 tasklet_schedule 函数,就能使 tasklet 在合适的时间运行,tasklet_schedule 调度(触发)函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值:没有返回值。
如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。 这是因为:软中断和tasklet是在中断上下文中执行的,不能进行睡眠等可能导致进程挂起的操作,否则可能会引发系统死锁等问题。工作队列是在进程上下文中执行的,可以进行睡眠等可能导致进程挂起的操作,因此可以执行较为复杂的任务。
Linux 内核使用 work_struct 结构体表示一个工作:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
把这些工作(work_struct 结构体)组织成工作队列( workqueue_struct 结构体表示),Linux 内核使用工作者线程(worker 结构体表示)来处理工作队列中的各个工作。
在实际的驱动开发中,我们只需要定义工作 (work_struct) 即可,关于工作队列和工作者线程我们基本不用去管。相当于我们只需要把“工作”放到指定的地方,会有制定好的线程处理。
创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作;
_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化:
#define DECLARE_WORK(n, f)
n 表示定义的工作的名字;
f 表示工作对应的处理函数。
和 tasklet 一样,工作也是需要调度才能运行的(类似于上面的tasklet_schedule),工作的调度函数为 schedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)
work:要调度的工作。
返回值:0 成功,其他值 失败。
配置好软中断后,什么时候触发呢?以下是一些触发软中断的常见场景:
网络数据包到达:当网络数据包到达时,网络协议栈会触发软中断,以处理数据包。
定时器到期:当定时器到期时,内核会触发软中断,以执行与定时器相关的任务。
中断处理函数:当中断处理函数需要异步执行一些任务时,可以触发软中断。
调度器:当进程需要被调度时,调度器可以触发软中断,以执行与调度相关的任务。
需要注意的是,软中断被触发后,软中断处理函数会在适当的时间被调用执行。软中断处理函数的执行时间应尽量短,以避免影响系统性能。因此,通常只有轻量级的任务才适合在软中断中处理。
①、SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。共享中断是指多个设备共享同一个中断线的情况。当共享中断被触发时,中断处理器会按照优先级顺序调用每个设备的中断处理函数。每个设备的中断处理函数会检查该设备是否是本次中断的源设备,如果是,则执行相应的处理操作;如果不是,则返回并等待下一个中断。
②、PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
Linux 内核通过读取设备树中的中断属性信息来配置中断。imx6ul.dtsi 文件中的 intc 节点就是I.MX6ULL 的中断控制器节点,节点内容如下所示:
intc: interrupt-controller@a01000 {
compatible = "arm,gic-400", "arm,cortex-a7-gic";
interrupts = ;
#interrupt-cells = <3>;
interrupt-controller;
interrupt-parent = <&intc>;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x2000>,
<0x00a04000 0x2000>,
<0x00a06000 0x2000>;
};
对于 gpio 来说,gpio 节点也可以作为中断控制器,上面的节点 intc 可以说是总的(系统的)中断控制器,这里的 gpio 理解为某个特定设备的中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点如下所示。
该节点没有 interrupt-parent 属性,因此自动依附父节点parent,即gpc节点(而gpc的parent是intc),因此 gpio5 节点的interrupts 属性受 gpc节点中的 #interrupt-cells = <3> 限定,也就是有3个cell。
gpio5: gpio@20ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = ,
;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-ranges = <&iomuxc 0 7 10>, <&iomuxc 10 5 2>;
};
三个cell的含义:
第一个 cell:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。
第二个 cell:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断号的范围为 0~15。
第三个 cell:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。bit[15:8]为 PPI 中断的 CPU 掩码。
可以看出:
- GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应 GPIO5_IO00~GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
- interrupt-controller 表明 gpio5 节点也是中断控制器,用于控制 gpio5 所有 IO 的中断。
- 将#interrupt-cells 修改为 2。因为所有的外部中断都是SPI中断,没必要再次传递。
打开 imx6ull-alientek-emmc.dts 文件(正点),找到如下所示内容:
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;// 该属性用来设置此设备节点属于哪个中断控制器,一般应该是用不到intc这个宏观的中断控制器
interrupts = <0 8>;
};
interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
interrupts 设置中断信息,0 表示 GPIO5_IO00(用于确定74还是75中断号),8 表示低电平触发。
简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
dev:设备节点。
index:索引号(类似数组索引),interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。