不管是单片机裸机实验还是Linux下的驱动实验,中断都是频繁使用的功能,在裸机中使用中断需要做一大堆的工作,比如配置寄存器,使能IRQ等等。但是Linux内核提供了完善的中断框架,只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。
Linux中断简介
Linux中断API函数
可以先来回顾一下裸机开发里中断的处理方法:
- 使能中断,初始化相应的寄存器。
- 注册中断服务函数,也就是向irqTable数组的指定标号处写入中断服务函数。
- 中断发生以后进入IRQ中断服务函数,在IRQ中断服务函数在数组irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
在Linux内核中也提供了大量的中断相关的API函数,来看一下这些跟中断有关的API函数:
中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在Linux内核中使用一个int变量表示中断号。
request_irq函数
在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:中断标志,可以在文件include/linux/interrupt.h里面查看所有的中断标志,这里介绍几个常用的中断标志,如下图所示:
比如STM32MP157开发板上的KEY0使用PG3,按下KEY0以后为低电平,因此可以设置为下降沿触发,也就是将flags设置为 IRQF_TRIGGER_FALLING。上图中这些标志位可以通过“|”来实现多种组合。 - 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函数原型如下所示:
void free_irq(unsigned int irq,
void *dev)
函数参数和返回值含义如下:
- irq:要释放的中断。
- dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
- 返回值:无。
中断处理函数
使用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 *))
函数参数和返回值含义如下:
- nr:要开启的软中断,在示例代码31.1.2.3中选择要开启的软中断。
- action:软中断对应的处理函数。
- 返回值:没有返回值。
注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
- nr:要触发的软中断,在示例代码31.1.2.3中选择要注册的软中断。
- 返回值:没有返回值。
软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:
从示例代码31.1.2.4可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。
tasklet
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);
函数参数和返回值含义如下:
- t:要初始化的tasklet。
- func:tasklet的处理函数。
- data:要传递给func函数的参数。
- 返回值:没有返回值。
也可以使用宏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)
函数参数和返回值含义如下:
- t:要调度的 tasklet,也就是DECLARE_TASKLET宏里面的name。
- 返回值:没有返回值。
关于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)
函数参数和返回值含义如下:
- work:要调度的工作。
- 返回值:0,成功,其他值,失败。
关于工作队列的参考使用示例如下所示:
示例代码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);
......
}
设备树中断信息节点
GIC中断控制器
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:虚拟快速FIQ。
- VIRQ:虚拟快速IRQ。
- FIQ:快速中断IRQ。
- IRQ:外部中断IRQ。
VFIQ和VIRQ是针对虚拟化的,不讨论虚拟化,剩下的就是FIQ和IRQ了,正点原子这个linux驱动开发教程中只使用IRQ。所以相当于GIC最终向ARM内核就上报一个IRQ信号。GIC V2的逻辑图如下图所示:
上图中左侧部分就是中断源,中间部分就是GIC控制器,最右侧就是中断控制器向处理器内核发送中断信息。重点要看的肯定是中间的GIC部分,GIC将众多的中断源分为分为三类:
- SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有Core共享的中断,这个是最常见的,那些外部中断都属于SPI中断(注意!不是SPI总线那个中断) 。比如GPIO中断、串口中断等等,这些中断所有的Core都可以处理,不限定特定Core。
- PPI(Private Peripheral Interrupt),私有中断,GIC是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
- SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR写入数据来触发,系统会使用SGI中断来完成多核之间的通信。
中断ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一ID,这些ID就是中断ID。每一个CPU最多支持1020个中断ID,中断ID号为ID0-ID1019。这1020个ID包含了PPI、SPI和SGI,这1020个ID分配如下:
- ID0-ID15:这16个ID分配给SGI。
- ID16-ID31:这16个ID分配给PPI。
- ID32-ID1019:这988个ID分配给SPI,像GPIO中断、串口中断等这些外部中断 ,至于具体到某个ID对应哪个中断那就由半导体厂商根据实际情况去定义了。比如STM32MP1系列总共分配了265个中断ID(有很多并未使用,只是保留着),加上前面属于PPI和SGI的32个ID,STM32MP1的中断源共有256+32=288个,这288个中断ID对应的中断源可以在《 STM32MP157参考手册》中找到详细的解释。重点关注的是从ID32开始的SPI中断,因为这些才是STM32MP1的外设中断,其中部分如下图所示:
EXTI——外部中断和事件控制器
EXTI全称是:Extended interrupt and event controller,中文一般叫做外部中断和事件控制器。EXTI是ST自己设计的,用来辅助GIC管理 STM32MP1相应中断的。EXTI通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对CPU事件输入生成事件。EXTI唤醒请求可让系统从停止模式唤醒,以及让CPU从CSTOP和CSTANDBY模式唤醒。此外,EXTI还可以在运行模式下生成中断请求和事件请求,这个非常重要,因为在实际使用中EXTI主要是为STM32的GPIO中断服务的。
EXTI主要特性如下:
- 支持76个输入事件。
- 两个CPU内核都支持。
- 所有事件输入均可让CPU唤醒。
EXTI的异步输入事件可以分为2组:
- 可配置事件(来自能够生成脉冲的I/O或外设的信号),这类事件具有以下特性:
- 可选择的有效触发边沿。
- 中断挂起状态寄存器位。
- 单独的中断和事件生成屏蔽。
- 支持软件触发。
- 直接事件(来自其他外设的中断和唤醒源,需要在外设中清除),这类事件具有以下特性:
- 固定上升沿有效触发。
- EXTI中无中断挂起状态寄存器位(中断挂起状态由生成事件的外设提供)。
- 单独的中断和事件生成屏蔽。
- 不支持软件触发。
对于GPIO中断来说,就是可配置事件,EXTI和GIC的关系如下图所示:
从上图可以看出STM32MP1的中断处理方式有5种:
- 外设直接产生中断到GIC,然后由GIC通知CPU内核。
- GPIO或外设产生中断到EXTI,EXTI将信号提交给GIC,最终再将中断信号提交给CPU。
- GPIO或外设产生中断到EXTI,EXTI直接将中断信号提交给CPU。
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。
GIC控制器节点
打开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的含义如下:
- 第一个cells:中断类型0表示SPI中断,1表示PPI中断。
- 第二个cells:中断号,对于SPI中断来说中断号的范围为32-287(256个),对于PPI中断来说中断号的范围为16-31,但是该cell描述的中断号是从0开始。
- 第三个cells:标志bit[3:0]表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit[15:8]为PPI中断的CPU掩码。
第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,来源就是上图。第三个表示中断触发类型是高电平触发。
EXTI控制器节点
对于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的中断功能。
简单总结一下与中断有关的设备树属性信息:
- #interrupt-cells,指定中断源的信息cells个数。
- interrupt-controller,表示当前节点为中断控制器。
- interrupts,指定中断号,触发方式等。
- interrupt-parent,指定父中断,也就是中断控制器。
- interrupts-extended,指定中断控制器、中断号、中断类型和触发方式,这个属性比较特殊,是新加入的。前面说了,要通过interrupts和interrupt-parent一起设置某个IO的中断方式。这里也可以只使用interrupts-extended属性一次性指定中断父节点,IO编号,中断方式等。
打开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)
函数参数和返回值含义如下:
- dev:设备节点。
- index:索引号interrupts属性可能包含多条中断信息,通过index指定要获取的信息。
- 返回值:中断号。
如果使用GPIO的话,可以使用gpio_to_irq函数来获取gpio对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
- gpio:要获取的GPIO编号。
- 返回值: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
测试APP要实现的内容很简单,通过不断的读取/dev/key设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有3个值,分别为0、1、2。0表示按键按下时的这个状态,1表示按键松开时对应的状态,而2表示按键一直被按住或者松开。
这里就是open了之后,进入死循环来读取设备,直接read出来,然后判断读取的值打印就可以了。
运行测试
编译驱动程序和测试APP
编译驱动程序
这里同样的,直接修改一下Makefile的obj-m,改为keyirq.o,然后“make”以下就可以了。
编译测试APP
输入如下命令即可:
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申请中断,其中传入之前写好的中断处理函数。