10_Linux中断

目录

Linux中断API函数

中断号

上半部与下半部

软中断

tasklet

工作队列

设备树中断信息节点

获取中断号

修改设备树文件

按键中断驱动程序编写

编写测试APP


Linux中断API函数

先来回顾一下裸机实验里面中断的处理方法:

1.使能中断,初始化相应的寄存器。

2.注册中断服务函数,也就是向irqTable数组的指定标号处写入中断服务函数

3.中断发生以后进入IRQ中断服务函数,在IRQ中断服务函数在数组irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

在Linux内核中也提供了大量的中断相关的API函数,来看一下这些跟中断有关的API函数:

中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在Linux内核中使用一个int变量表示中断号。

request_irq函数

在Linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用requestirq函数。request_irq函数会激活(使能)中断,所以不需要我们手动去使能中断, request_irq函数原型如下:

 irq:要申请中断的中断号。

handler:中断处理函数,当中断发生以后就会执行此中断处理函数。

flags:中断标志,可以在文件include/linux/interrupt.h里面查看所有的中断标志,这里我们介绍几个常用的中断标志,如表所示:

 比如I.MX6U-ALPHA开发板上的KEY0使用GPIO1_1O18,按下KEYO以后为低电平,因此可以设置为下降沿触发,也就是将flags设置为IRQF_TRIGGER_FALLING。表中的这些标志可以通过“1”来实现多种组合。

Name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。

dev:如果将flags设置为IRQF_SHARED 的话, dev用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。

返回值:0中断申请成功,其他负值中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。

free_irq函数

使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:

 Irq:要释放的中断。

dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。

返回值:无。

中断处理函数

使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

 第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:

 可以看出irgreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

 

中断使能与禁止函数

常用的中断使用和禁止函数如下所示:

 enable_irq和disable_irq用于使能和禁止指定的中断,irq就是要禁止的中断号。disable_irq

函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:

 disable_irqnosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候需要关闭当前处理器的整个中断系统,也就是在学习STM32的时候常说的关闭全局中断,这个时候可以使用如下两个函数:

local_irq_enable用于使能当前处理器中断系统,local_irq_disable用于禁止当前处理器中断系统。假如A任务调用local_irq_disable关闭全局中断10S,当关闭了2S的时候B任务开始运行, B任务也调用local_irq_disable关闭全局中断3S, 3秒以后B任务调用 local_irq_enable 函数将全局中断打开了。此时才过去2+3-5秒的时间,然后全局中断就被打开了,此时A任务要关闭 10S 全局中断的愿望就破灭了,然后A任务就“生气了”,结果很严重,可能系统都要被A 任务整崩溃。为了解决这个问题,B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:

 

这两个函数是一对, local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。local_irq_restore用于恢复中断,将中断到flags状态。

上半部与下半部

在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生, SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是我们都知道ⅡC的速度最高也只有400KbitS.所以在中断中通过IIC读取数据就会浪费时间。我们可以将通过IC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:

上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。

下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

因此, Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:

1.如果要处理的内容不希望被其他中断打断,那么可以放到上半部。

2.如果要处理的任务对时间敏感,可以放到上半部。

3.如果要处理的任务与硬件有关,可以放到上半部

4.除了上述三点以外的其他任务,优先考虑放到下半部。

上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢? Linux内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。

软中断

一开始Linux内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代"BH”机制,完全可以使用软中断和tasklet来替代BH,从2.5版本的Linux内核开始BH 已经被抛弃了。Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:

 在kernel/softirq.c文件中一共定义了10个软中断,如下所示:

 NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中,定义如下:

 可以看出,一共有10个软中断,因此NR_SOFTIRQS为10,因此数组softirq_vec有10个元素。softirq_action结构体中的action成员变量就是软中断的服务函数,数组softirq_vec是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个CPU所执行的软中断服务函数确是相同的,都是数组softirq_vec中定义的action函数。要使用软中断,必须先使用open-softirq函数注册对应的软中断处理函数, open-softirq函数原型如下:

 nr:要开启的软中断,在示例代码中选择一个。

action:软中断对应的处理函数。

返回值:没有返回值。

注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:

 nr:要开启的软中断,在示例代码中选择一个。

action:软中断对应的处理函数。

返回值:没有返回值。

软中断必须在编译的时候静态注册!Linux内核使用softirg init函数初始化软中断,softirq_init 函数定义在kernel/softirq.c文件里面,函数内容如下:

 从示例代码可以看出, softirq_init 函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

tasklet

tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议大家使用tasklet。Linux内核使用tasklet_struct结构体来表示tasklet:

 第489行的func函数就是tasklet要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用tasklet,必须先定义一个tasklet,然后使用tasklet init函数初始化tasklet,taskled_init 函数原型如下:

 t:要初始化的tasklet

func: tasklet的处理函数。

data:要传递给func函数的参数

返回值:没有返回值。

也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化,DECLARE-TASKLET定义在include/linux/interrupt.h文件中,定义如下:

 其中name为要定义的tasklet名字,这个名字就是一个tasklet_struct类型的时候变量, func就是tasklet的处理函数, data是传递给func函数的参数。

在上半部,也就是中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行,tasklet schedule函数原型如下:

 t:要调度的tasklet,也就是DECLARE_TASKLET宏里面的name.

返回值;:没有返回值。

关于tasklet的参考使用示例如下所示:

 

工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。

Linux内核使用work_struct结构体表示一个工作,内容如下(省略掉条件编译):

 这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,内容如下(省略掉条件编译):

 Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux内核使用worker结构体表示工作者线程,worker结构体内容如下:

 从示例代码可以看出,每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个work_struct结构体变量即可,然后使用INIT_WORK宏来初始化工作,INIT WORK宏定义如下:

 work表示要初始化的工作, func是工作对应的处理函数。

也可以使用DECLARE WORK宏一次性完成工作的创建和初始化,宏定义如下:

 n表示定义的工作(work_struct), f表示工作对应的处理函数。和tasklet一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:

 work:要调度的工作。

返回值: 0成功,其他值失败。

关于工作队列的参考使用示例如下所示:

 

设备树中断信息节点

如果使用设备树的话就需要在设备树中设置好中断属性信息, Linux内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开imx6ull.dtsi 文件,其中的 intc 节点就是I.MX6ULL 的中断控制器节点,节点内容如下所示:

 第2行, compatible属性值为"arm,cortex-a7-gic"在Linux内核源码中搜索"arm,cortex-a7gic"即可找到GIC中断控制器驱动文件。

第3行,#interrupt-cells和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells大小,对于设备而言,会使用interrupts属性描述中断信息,#interrupt-cells描述了interrupts属性的cells大小,也就是一条信息有几个cells。每个cells都是32位整形值,对于ARM处理的GIC来说,一共有3个cells,这三个cells的含义如下:

第一个cells:中断类型,0表示SPI中断,1表示PPI中断。

第二个cells:中断号,对于SPI中断来说中断号的范围为0-987,对于PPI中断来说中断号的范围为0-15。

第三个cells:标志,bit[3:0]表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit[15:8]为PPI中断的 CPU掩码。

对于gpio来说, gpio节点也可以作为中断控制器,比如imx6ull.dtsi文件中的gpio5节点内容如下所示:

 第4行, interrupts描述中断源信息,对于gpio5来说一共有两条信息,中断类型都是SP1,触发电平都是IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是74,一个是75,打开可以打开《IMX6ULL参考手册》的"Chapter 3 Interrupts and DMA Events"章节,找到表3-1,有如图所示的内容:

 从图可以看出,GPIO5一共用了2个中断号,一个是74,一个是75。其中74对应GPIO5_IO00~GPIO5_IO15这低16个IO,75对应GPIO5_IO16-GPIO15_IO31这高16位IO。第8行, interrupt-controller表明了gpio5节点也是个中断控制器,用于控制gpio5所有IO的中断。

第9行,将#interrupt-cells修改为2。

打开imx6ull-alientek-emmc.dts文件,找到如下所示内容:

 fxls8471是NXP官方的6ULL开发板上的一个磁力计芯片, fxls8471有一个中断引脚链接到了I.MX6ULL的SNVS_TAMPERO因脚上,这个引脚可以复用为GPIOS_IO00

第5行, interrupt-parent属性设置中断控制器,这里使用gpio5作为中断控制器。

第6行, interrupts设置中断信息, 0表示GPIO5_1000, 8表示低电平触发。

简单总结一下与中断有关的设备树属性信息:

1.#interrupt-cells,指定中断源的信息cells个数。

2.interrupt-controller,表示当前节点为中断控制器。

3.interrupts,指定中断号,触发方式等。

4.interrupt-parent,指定父中断,也就是中断控制器。

获取中断号

编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过irq_of_parse_and_map函数从interupts属性中提取到对应的设备号,函数原型如下:

 dev:设备节点。

index:索引号, interrupts属性可能包含多条中断信息,通过index指定要获取的信息。

返回值:中断号。如果使用GPIO的话,可以使用gpio_to_irq 函数来获取gpio 对应的中断号,函数原型如下:

 gpio:要获取的 GPIO 编号。

返回值:GPIO对应的中断号。

修改设备树文件

实验使用到了按键KEY0,按键KEY0使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的"key”节点内容如下所示:

 第8行,设置interrupt-parent属性值为"gpio1",因为KEYO所使用的GPIO为GPIO1_IO18,也就是设置KEY0的GPIO中断控制器为gpio1

第9行,设置interrupts属性,也就是设置中断源,第一个cells的18表示GPIO1组的18号IO。 IRQTYPE_EDGE_BOTH定义在文件include/linux/irq.h中,定义如下:

 

从示例代码中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。设备树编写完成以后使用"make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb文件启动Linux系统。

按键中断驱动程序编写

 

 

 第38-43行,结构体irq_keydesc为按键的中断描述结构体, gpio为按键GPIO编号, irqnum为按键IO对应的中断号,value为按键对应的键值,name为按键名字,handler为按键中断服务函数。使用irq_keydese结构体即可描述一个按键中断。

第47~60行,结构体imx6uirq_dev为本例程设备结构体,第55行的keyvalue保存按键值,

第56行的releasekey表示按键是否被释放,如果按键被释放表示发生了一次完整的按键过程。第57行的timer为按键消抖定时器,使用定时器进行按键消抖的原理。

第58行的数组irqkeydesc为按键信息数组,数组元素个数就是开发板上的按键个数,I.MX6U-ALIPHA开发板上只有一个按键,因此irqkeydesc数组只有一个元素。

第59行的curkeynum表示当前按键。

第62行,定义设备结构体变量imx6uirq。

第70~78行,key0_handler函数,按键KEY0中断处理函数,参数dev_id为设备结构体,也就是imx6uirq。

第74行设置curkeynum=0,表示当前按键为KEY0,第76行使用mod_timer函数启动定时器,定时器周期为10ms。

第85~103, timer_function函数,定时器定时处理函数,参数arg是设备结构体,也就是imx6uirq,在此函数中读取按键值。

第95行通过gpio_get_value函数读取按键值。如果为0的话就表示按键被按下去了,按下去的话就设置imx6uirq结构体的keyvalue成员变量为按键的键值,比如KEY0按键的话按键值就是KEYOVALUE=0。如果按键值为1的话表示按键被释放了,按键释放了的话就将imx6uirq结构体的keyvalue成员变量的最高位置1,表示按键值有效,也就是将keyvalue与0x80进行或运算,表示按键松开了,并且设置imx6uirq结构体的releasekey成员变量为1,表示按键释放,一次有效的按键过程发生。

第110~159行,keyio_init函数,按键IO初始化函数,在驱动入口函数里面会调用keyio_init来初始化按键 IO。

第131~142行轮流初始化所有的按键,包括申请IO、设置IO为输入模式、从设备树中获取10的中断号等等。

第136行通过irq_of_parse_and_map函数从设备树中获取按键IO对应的中断号。也可以使用gpio_to_irq 函数将某个IO设置为中断状态,并且返回其中断号。

第144和145行设置KEYO按键对应的按键中断处理函数为key0_handler、KEY0的按键值为 KEYOVALUE。

第147~153行轮流调用request_irq函数申请中断号,设置中断触发模式为IRQF-TRIGGER FALLING和IRQF_TRIGGER-RISING,也就是上升沿和下降沿都可以触发中断。最后,第156行初始化定时器,并且设置定时器的定时处理函数。

第168~172行,imx6uirq_open函数,对应应用程序的open函数。

第182~207行,imx6uirq_read 函数,对应应用程序的 read函数。此函数向应用程序返回按键值。首先判断imx6uirq结构体的releasekey成员变量值是否为1,如果为1的话表示有一次有效按键发生,否则的话就直接返回-EINVAL。当有按键事件发生的话就要向应用程序发送按键值,首先判断按键值的最高位是否为1,如果为1的话就表示按键值有效。如果按键值有效的话就将最高位清除,得到真实的按键值,然后通过copy_to_user函数返回给应用程序。向应用程序发送按键值完成以后就将imx6uirq结构体的releasekey成员变量清零,准备下一次按键操作。

第210-214行,按键中断驱动操作函数集imx6uirq_fops

第221-253行,驱动入口函数,第250和251行分别初始化imx6uirq结构体中的原子变量keyvalue和releasekey,第252行调用keyio_init函数初始化按键所使用的IO。

第261-276行,驱动出口函数,第265行调用del_timer_sync函数删除定时器,第268~270行轮流释放申请的所有按键中断。

编写测试APP

 

 第45-53行的while循环用于不断的读取按键值,如果读取到有效的按键值就将其输出到终端上。

运行测试

 驱动加载成功以后可以通过查看/proc/interrupts文件来检查一下对应的中断有没有被注册上,输入如下命令:

 从图可以看出imx6uirq.c驱动文件里面的KEY0中断已经存在了,触发方式为跳边沿(Edge),中断号为49。

接下来使用如下命令来测试中断:

 按下开发板上的KEY0,终端就会输出按键值,如图所示:

 从图可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值