不管是单片机裸机实验还是Linux下的驱动实验,中断都是频繁使用的功能,在裸机中使用中断需要做一大堆的工作,比如配置寄存器,使能IRQ等等。但是Linux内核提供了完善的中断框架,只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。
可以先来回顾一下裸机开发里中断的处理方法:
在Linux内核中也提供了大量的中断相关的API函数,来看一下这些跟中断有关的API函数:
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在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)
函数参数和返回值含义如下:
使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:
void free_irq(unsigned int irq,
void *dev)
函数参数和返回值含义如下:
使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数相应的中断号。第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型, irqreturn_t类型定义如下所示:
可以看出irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
常用的中断使用和禁止函数如下所示:
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函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候需要关闭当前处理器的整个中断系统,也就是在学习STM32的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
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全局中断,产生冲突,可能导致整个系统崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:
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内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代“BH”机制,完全可以使用软中断和tasklet来替代 BH,从2.5版本的 Linux内核开始BH已经被抛弃了。 Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:
示例代码31.1.2.1 softirq_action结构体
541 struct softirq_action
542 {
543 void (*action)(struct softirq_action *);
544 };
在kernel/softirq.c文件中一共定义了10个软中断,如下所示:
示例代码31.1.2.2 softirq_vec数组
static struct softirq_action softirq_vec[NR_SOFTIRQS];
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函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
函数参数和返回值含义如下:
注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:
从示例代码31.1.2.4可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。
tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议使用tasklet。tasklet_struct结构体如下所示:
第597行的func函数就是tasklet要执行的处理函数,用户实现具体的函数内容,相当于中断处理函数。如果要使用tasklet,必须先定义一个tasklet_struct变量,然后使用tasklet_init函
数对其进行初始化,taskled_init函数原型如下:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
函数参数和返回值含义如下:
也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化,DECLARE_TASKLET定义在include/linux/interrupt.h文件中,定义如下:
DECLARE_TASKLET(name, func, data)
其中name为要定义的tasklet名字,其实就是tasklet_struct类型的变量名,func就是tasklet的处理函数,data是传递给func函数的参数。
在上半部,也就是中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行,tasklet_schedule函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
关于tasklet的参考使用示例如下所示:
示例代码31.1.2.7 tasklet使用示例
/* 定义taselet */
struct tasklet_struct testtasklet;
/* tasklet处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet。
Linux内核使用work_struct结构体表示一个工作,内容如下(省略掉条件编译):
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,内容如下(省略掉
条件编译):
Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程, worker结构体内容如下:
从示例代码31.1.2.10可以看出,每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。简单创建工作很简单,直接定义一个work_struct结构体变量即可,然后使用INIT_WORK宏来初始化工作,INIT_WORK宏定义如下:
#define INIT_WORK(_work, _func)
_work表示要初始化的工作,_func是工作对应的处理函数。
也可以使用DECLARE_WORK宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n表示定义的工作(work_struct),f表示工作对应的处理函数。
和tasklet一样,工作也是需要调度才能运行的,工作的调度函数为 chedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
关于工作队列的参考使用示例如下所示:
示例代码31.1.2.11 工作队列使用示例
/* 定义工作(work) */
struct work_struct testwork;
/* work处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
STM32MP1有三个与中断有关的控制器:GIC、EXTI和NVIC,其中NVIC是Cortex-M4内核的中断控制器,正点原子的linux驱动开发教程只讲解Cortex-A7内核,因此就只剩下了GIC和EXTI。首先是GIC全称为:Generic Interrupt Controller。
GIC是ARM公司给Cortex-A/R内核提供的一个中断控制器,类似Cortex-M内核中的NVIC。目前GIC有4个版本 :V1-V4,V1是最老的版本,已经被废弃了。 V2-V4目前正在大量的使用。GIC V2是给ARMv7-A架构使用的,比如Cortex-A7、Cortex-A9、Cortex-A15等,V3和 V4是给ARMv8-A/R架构使用的,也就是64位芯片使用的。STM32MP1是Cortex-A7内核,因此主要讲解GIC V2。GIC V2最多支持8个核。ARM会根据GIC版本的不同研发
出不同的IP核,那些半导体厂商直接购买对应的IP核即可,比如ARM针对GIC V2就开发出了GIC400这个中断控制器IP核。当GIC接收到外部中断信号以后就会报给ARM内核,但是ARM内核只提供了四个信号给GIC来汇报中断情况:VFIQ、VIRQ、FIQ和IRQ,他们之间的关系如下图所示:
在上图中,GIC接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给ARM内核,这四个信号的含义如下:
VFIQ和VIRQ是针对虚拟化的,不讨论虚拟化,剩下的就是FIQ和IRQ了,正点原子这个linux驱动开发教程中只使用IRQ。所以相当于GIC最终向ARM内核就上报一个IRQ信号。GIC V2的逻辑图如下图所示:
上图中左侧部分就是中断源,中间部分就是GIC控制器,最右侧就是中断控制器向处理器内核发送中断信息。重点要看的肯定是中间的GIC部分,GIC将众多的中断源分为分为三类:
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一ID,这些ID就是中断ID。每一个CPU最多支持1020个中断ID,中断ID号为ID0-ID1019。这1020个ID包含了PPI、SPI和SGI,这1020个ID分配如下:
EXTI全称是:Extended interrupt and event controller,中文一般叫做外部中断和事件控制器。EXTI是ST自己设计的,用来辅助GIC管理 STM32MP1相应中断的。EXTI通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对CPU事件输入生成事件。EXTI唤醒请求可让系统从停止模式唤醒,以及让CPU从CSTOP和CSTANDBY模式唤醒。此外,EXTI还可以在运行模式下生成中断请求和事件请求,这个非常重要,因为在实际使用中EXTI主要是为STM32的GPIO中断服务的。
EXTI主要特性如下:
EXTI的异步输入事件可以分为2组:
对于GPIO中断来说,就是可配置事件,EXTI和GIC的关系如下图所示:
从上图可以看出STM32MP1的中断处理方式有5种:
Linux系统会用到这三种中断方式,一个外设最多可以有两种中断方式。
STM32MP1的所有GPIO都有中断功能,而GPIO中断是最常用的功能。STM32每一组GPIO最多有16个IO,比如PA0-PA15,因此每组GPIO就有16个中断,这16个GPIO事件输入对应EXTI0-15,其中PA0、PB0等都对应EXTI0,如下图所示:
如果要在Linux系统中使用中断,那么就需要在设备树中设置好中断属性信息,Linux内核通过读取设备树中的中断属性信息来配置中断,GIC控制器的设备树绑定信息参考文档Documentation/devicetree/bindings/interrupt-controller/arm,gic.yaml,EXTI控制器的设备树绑定
信息参考文档Documentation/devicetree/bindings/interrupt-controller/st,stm32-exti.txt。
打开stm32mp151.dtsi文件,其中的intc节点就是GIC的中断控制器节点,节点内容如下所示:
示例代码31.1.3.1 中断控制器intc节点
1 intc: interrupt-controller@a0021000 {
2 compatible = "arm,cortex-a7-gic";
3 #interrupt-cells = <3>;
4 interrupt-controller;
5 reg = <0xa0021000 0x1000>,
6 <0xa0022000 0x2000>;
7 };
第2行,compatible属性值为“arm,cortex-a7-gic”,在Linux内核源码中搜索“arm,cortex-a7-gic”即可找到GIC中断控制器驱动文件。
第3行,#interrupt-cells和#address-cells、#size-cells一样。表示此中断控制器下设备的cells大小,对于设备而言,会使用interrupts属性描述中断信息,#interrupt-cells描述了interrupts属性的cells大小,也就是一条信息有几个cells。每个cells都是32位整形值,对于ARM处理的GIC来说,一共有3个cells,这三个cells的含义如下:
第4行, interrupt-controller节点为空,表示当前节点是中断控制器。
来看一下STM32MP1的SPI6是如何在设备树节点中描述中断信息的,首先是查阅《 STM32MP157参考手册》第“21.2 GIC Interrupts”小节中的表117。找到SPI6对应的中断号,如下图所示:
从上图可以看出,第一列的“Num”就是SPI6的中断号:86,注意这里并没有算前面32个中断号,如果加上前面32个中断号的话就是第二列“ID”,为86+32=118。
打开stm32mp151.dtsi,找到SPI6节点内容,如下所示:
第6行,interrupts描述中断中断源的信息,第一个表示中断类型,为GIC_SPI,也就是共享中断。第二个表示中断号为86,来源就是上图。第三个表示中断触发类型是高电平触发。
对于GPIO中断而言,要用到EXTI,所以接下来看一下EXTI设备节点。打开stm32mp151.dtsi文件,其中的exti节点就是EXTI的中断控制器节点,节点内容如下所示:
第3行,表明exti节点是个中断控制器。
第4行,interrupt-cells=2,表明exti的子节点里面第一个cell表示为中断号,也可以叫EXTI事件编号,第二个cell表示中断标志位。其它的设备树属性和GIC控制器是一样的。
GPIO用到了EXTI,因此GPIO节点信息里面的EXTI相关内容,在stm32mp151.dtsi文件中找到如下所示内容:
第1-131行,pinctrl节点,此节点有11个子节点,gpioa-gpiok,分别对应GPIOA-GPIOK这11组 IO。
第8行,通过interrupt-parent属性指定pinctrl所有子节点的中断父节点为exti,这样GPIO的中断就和EXTI联系起来了。
第11-20行为gpioa节点,第14行表明gpioa节点也是个中断控制器,第15行设置interrupt-cells为2,那么对于具体的GPIO而言,interrupts属性第一个cell为某个IO在所处组的编号,第二个cell表示中断触发方式。
第133-154行,pinctrl_z节点,由于GPIOZ这一组对应的寄存器地址和GPIOA~GPIOK不是连续的,所以单独使用pinctrl_z来描述GPIOZ,含义和pinctrl一样。
可以找一个具体的应用,打开stm32mp15xx-dkx.dtsi文件,找到如下所示内容:
sii9022是ST官方在开发板上的一个HDMI芯片,上述代码就是sii9022的节点信息,sii9022a芯片有一个中断,此引脚链接到了STM32MP1的PG1上,此中断是下降沿触发。
第7行,interrupts设置中断信息,1表示本组内第一个IO,在这里就是PG1。IRQ_TYPE_EDGE_FALLING表示下降沿触发 。
第8行,interrupt-parent属性设置中断控制器,这里是有gpiog作为中断控制器。结合上面的interrupts属性,这两行的目的就是设置PG1为下降沿触发。
可以看出使用起来是非常的简单,在实际编写代码的时候,只需要通过interrupt-parent和interrupts这两个属性即可设置某个GPIO的中断功能。
简单总结一下与中断有关的设备树属性信息:
打开stm32mp157f-ev1-a7-examples.dts文件,里面有如下所示代码:
很明显,上述代码用于描述一个按键,此按键采用中断方式,这个按键使用到了PA13这个引脚。第13行直接通过interrupts-extended属性描述了所有的中断信息,第一个参数为gpioa,第二个参数为13,第三个参数表示下降沿触发。如果使用interrupts和interrupt-parent来描述的话就是:
interrupt-parent = <&gpioa>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
编写驱动的时候需要用到中断号,用到的中断号,中断信息已经写到了设备树里面,可以通过irq_of_parse_and_map函数从interupts属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
函数参数和返回值含义如下:
如果使用GPIO的话,可以使用gpio_to_irq函数来获取gpio对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
就是按键的原理图,之前的笔记中已经有过分析了,这里不再赘述。
本章实验驱动正点原子的STM32MP157开发板上的KEY0按键,不过采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。
本章实验使用到了按键KEY0,按键KEY0使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:
示例代码31.3.1.1 key节点信息
1 key {
2 compatible = "alientek,key";
3 status = "okay";
4 key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
5 interrupt-parent = <&gpiog>;
6 interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
7 };
第5行,设置interrupt-parent属性值为“gpiog”,因为KEY0所使用的GPIO为PG3,所以要设置KEY0的中断控制器为gpiog。
第6行,设置interrupts属性,也就是设置中断源,第一个cells的3表示GPIOG组的3号IO。IRQ_TYPE_EDGE_BOTH定义在文件include/linux/irq.h中,定义如下:
示例代码31.3.1.2 中断状态
75 enum {
76 IRQ_TYPE_NONE = 0x00000000,
77 IRQ_TYPE_EDGE_RISING = 0x00000001,
78 IRQ_TYPE_EDGE_FALLING = 0x00000002,
79 IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING),
80 IRQ_TYPE_LEVEL_HIGH = 0x00000004,
81 IRQ_TYPE_LEVEL_LOW = 0x00000008,
82 IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW |IRQ_TYPE_LEVEL_HIGH),
......
100 };
从示例代码31.3.1.2中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。
设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的stm32mp157d-atk.dtb文件启动Linux系统。
首先要定义一个枚举类型,来通过3个常量KEY_PRESS、KEY_RELEASE、KEY_KEEP来表征3种按键状态。
然后定义一个key_dev来完成设备结构体的编写,这里主要最后添加一个struct timer_list timer进行按键值的定时判断;int irq_num来表示中断号;最后加一个自旋锁spinlock_t spinlock。
之后具象化一个key表示按键设备,同时初始化一个static int status来表示按键状态,初始化为KEY_KEEP。
编写一个中断处理函数irqreturn_t key_interrupt,通过15ms的定时器延时来做按键防抖,直接mod_timer激活定时器即可,然后return一个IRQ_HANDLED。
之后编写key_parse_dt函数,该函数主要是对设备树中的属性进行解析,整个的初始化与之前的都差不多,显示走流程获取节点,读取status和compatible属性,然后获取设备树的gpio属性;这之后就有区别了,通过irq_of_parse_and_map来获取中断号并存入key.irq_num。
之后写一个key_gpio_init,里面通过gpio_request申请GPIO口,设置GPIO为输入模式,这些跟之前一样;之后,需要通过irq_get_trigger_type获取中断触发类型,之后通过request_irq申请中断,并设置key_interrupt为中断处理函数,且request_irq默认会使能中断(更安全一点,可以申请成功后先disable_irq,然后所有工作完成后再使能中断)。
之后编写key_timer_function,来判断按键状态,需要先上锁然后通过static一个last_val和current_val,gpio_get_value后比较来完成消抖和状态判断。
之后写key_read对应应用程序的read函数,这个就比较简单,直接copy_to_user把state发送给应用程序,发完重置按键状态即可。
最后是mykey_init来完成驱动入口函数,先spin_lock_init初始化自旋锁,然后通过key_parse_dt解析设备树,key_gpio_init初始化IO口,然后走流程注册字符设备驱动,最后加一下timer_setup设置定时器处理函数。
测试APP要实现的内容很简单,通过不断的读取/dev/key设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有3个值,分别为0、1、2。0表示按键按下时的这个状态,1表示按键松开时对应的状态,而2表示按键一直被按住或者松开。
这里就是open了之后,进入死循环来读取设备,直接read出来,然后判断读取的值打印就可以了。
这里同样的,直接修改一下Makefile的obj-m,改为keyirq.o,然后“make”以下就可以了。
输入如下命令即可:
arm-none-linux-gnueabihf-gcc keyirqApp.c -o keyirqApp |
将上一小节编译出来keyirq.ko和keyirqApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载keyirq.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令 modprobe keyirq.ko //加载驱动 |
驱动加载完成后可以通过查看/proc/interrupts文件夹检查时候有对应中断被注册:
cat /proc/interrupts |
结果如下图所示:
从上图可以看出keyirq.c驱动文件里面的KEY0中断已经存在了,触发方式为跳边沿(Edge)。
接下来使用如下命令来测试中断:
./keyirqApp /dev/key |
按下开发板的KEY0按键,会在终端输出按键值,如下图所示:
从上图可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:
rmmod keyirq.ko |
使用中断的时候,可以直接在对应的GPIO节点上面添加interrupt-parent设定开启中断的GPIO,然后通过interrupts设置具体的引脚号以及触发沿。
然后需要编写中断处理函数,然后需要写一个针对设备树进行解析的函数,通过of_get_named_gpio获取GPIO编号,之后通过irq_of_parse_and_map获取中断号,通过这个中断号申请和释放中断。
GPIO的初始化函数中,要添加中断的初始化,需要irq_get_trigger_type来获取中断触发类型,最后通过request_irq申请中断,其中传入之前写好的中断处理函数。