【正点原子Linux连载】 第十五章 Linux中断实验 摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南

1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第十五章 Linux中断实验

不管是单片机还是Linux下的驱动实验,中断都是频繁使用的功能,在单片机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能IRQ等等。但是Linux内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。本章我们就来学习一下如何在Linux下使用中断。

15.1 Linux中断简介
15.1.1 Linux中断API函数
先来回顾一下我们在做单片机开发的时候中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、编写中断服务函数,中断发生以后相应的中断服务函数就会执行。
在Linux内核中也提供了大量的中断相关的API函数,我们来看一下这些跟中断有关的API函数:
1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线,在Linux内核中使用一个int变量表示中断号。
2、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里面查看所有的中断标志,这里我们介绍几个常用的中断标志,如表15.1.1.1所示:
标志 描述
IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq函数的dev参数就是唯一区分他们的标志。
在这里插入图片描述

表15.1.1.1 常用的中断标志
比如在《第十三章 Linux按键输入实验》中使用GPIO3_C5这个IO,通过将此IO接到3.3V模拟按键按下,也就是按下以后为高电平,因此可以设置为上升沿触发,也就是将flags设置为IRQF_TRIGGER_RISING。表15.1.1.1中的这些标志可以通过“|”来实现多种组合。
name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。
dev:如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。
3、free_irq函数
使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:
void free_irq(unsigned int irq,
void *dev_id)
函数参数和返回值含义如下:
irq:要释放的中断。
dev_id:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
4、中断处理函数
使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev_id参数保持一致。用于区分共享中断的不同设备,dev_id也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:
示例代码15.1.1.1 irqreturn_t结构

11 enum irqreturn {
12  	IRQ_NONE        	= (0 << 0),
13  	IRQ_HANDLED     	= (1 << 0),
14  	IRQ_WAKE_THREAD  	= (1 << 1),
15 };
16 
17 typedef enum irqreturn irqreturn_t;
可以看出irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

return IRQ_RETVAL(IRQ_HANDLED)
5、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
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全局中断的愿望就破灭了,然后A任务就“生气了”,结果很严重,可能系统都要被A任务整崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对,local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。local_irq_restore用于恢复中断,将中断到flags状态。
15.1.2 上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生,SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是我们都知道IIC的速度最高也只有400Kbit/S,所以在中断中通过IIC读取数据就会浪费时间。我们可以将通过IIC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此,Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。
1、软中断
一开始Linux内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代“BH”机制,完全可以使用软中断和tasklet来替代BH,从2.5版本的Linux内核开始BH已经被抛弃了。Linux内核使用结构体softirq_action表示软中断, softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:
示例代码15.1.2.1 softirq_action结构体

491 struct softirq_action
492 {
493     void    (*action)(struct softirq_action *);
494 };
在kernel/softirq.c文件中一共定义了10个软中断,如下所示:

示例代码15.1.2.2 softirq_vec数组
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中,定义如下:
示例代码15.1.2.3 softirq_vec数组

enum
{
    HI_SOFTIRQ=0,           	/* 高优先级软中断 		*/
    TIMER_SOFTIRQ,          	/* 定时器软中断 			*/
    NET_TX_SOFTIRQ,         	/* 网络数据发送软中断 	*/
    NET_RX_SOFTIRQ,         	/* 网络数据接收软中断 	*/
    BLOCK_SOFTIRQ,          
    IRQ_POLL_SOFTIRQ,   
    TASKLET_SOFTIRQ,        	/* tasklet软中断 		*/
    SCHED_SOFTIRQ,          	/* 调度软中断 			*/
    HRTIMER_SOFTIRQ,        	/* 高精度定时器软中断	*/
    RCU_SOFTIRQ,            	/* RCU软中断			*/

    NR_SOFTIRQS
};

可以看出,一共有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:要开启的软中断,在示例代码15.1.2.3中选择要开启的软中断。
action:软中断对应的处理函数。
返回值:没有返回值。
注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
nr:要触发的软中断,在示例代码15.1.2.3中选择要注册的软中断。
返回值:没有返回值。
软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:
示例代码15.1.2.4 softirq_init函数内容

625 void __init softirq_init(void)
626 {
627     int cpu;
628 
629     for_each_possible_cpu(cpu) {
630         per_cpu(tasklet_vec, cpu).tail =
631             &per_cpu(tasklet_vec, cpu).head;
632         per_cpu(tasklet_hi_vec, cpu).tail =
633             &per_cpu(tasklet_hi_vec, cpu).head;
634     }
635 
636     open_softirq(TASKLET_SOFTIRQ, tasklet_action);
637     open_softirq(HI_SOFTIRQ, tasklet_hi_action);
638 }
从示例代码15.1.2.4可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。
2、tasklet
tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议大家使用tasklet。tasklet_struct结构体如下所示:

示例代码15.1.2.5 tasklet_struct结构体

542 struct tasklet_struct
543 {
544     struct tasklet_struct *next;    /* 下一个tasklet 			*/
545     unsigned long state;            	/* tasklet状态    			*/
546     atomic_t count;                 	/* 计数器,记录对tasklet的引用数 */
547     void (*func)(unsigned long);   	/* tasklet执行的函数 		*/
548     unsigned long data;             	/* 函数func的参数 			*/
549 };

第547行的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的参考使用示例如下所示:
示例代码15.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);
    ......
}
2、工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet。
Linux内核使用work_struct结构体表示一个工作,内容如下(省略掉条件编译):
示例代码15.1.2.8 work_struct结构体

struct work_struct {
    atomic_long_t data;    
    struct list_head entry; 
    work_func_t func;       	/* 工作队列处理函数 	*/
};
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,内容如下(省略掉条件编译):

示例代码15.1.2.9 workqueue_struct结构体

struct workqueue_struct {
    struct list_head    	pwqs;       
    struct list_head    	list;       
    struct mutex        	mutex;      
    int         				work_color; 
    int         				flush_color;    
    atomic_t        		nr_pwqs_to_flush; 
    struct wq_flusher   	*first_flusher; 
    struct list_head    	flusher_queue;  
    struct list_head    	flusher_overflow;
    struct list_head    	maydays;    
    struct worker       	*rescuer;   
    int         				nr_drainers;    
    int         				saved_max_active; 
    struct workqueue_attrs  *unbound_attrs; 
    struct pool_workqueue   *dfl_pwq;   
    char            			name[WQ_NAME_LEN]; 
    struct rcu_head     	rcu;
    unsigned int        	flags ____cacheline_aligned; 
    struct pool_workqueue __percpu *cpu_pwqs; 
    struct pool_workqueue __rcu *numa_pwq_tbl[]; 
};
Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程,worker结构体内容如下:

示例代码15.1.2.10 worker结构体

struct worker {
    union {
        struct list_head    	entry;  
        struct hlist_node   	hentry; 
    };

    struct work_struct  		*current_work;  
    work_func_t     			current_func;   
    struct pool_workqueue   	*current_pwq; 
    struct list_head    		scheduled;  
    struct task_struct  		*task;      
    struct worker_pool  		*pool;      
    struct list_head    		node;       
    unsigned long       		last_active;    
    unsigned int        		flags;      
    int         					id;     
    int         					sleeping;   
    char            				desc[WORKER_DESC_LEN];
    struct workqueue_struct 	*rescue_wq;
    work_func_t     			last_func;
};

从示例代码15.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一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:

bool schedule_work(struct work_struct *work)
	函数参数和返回值含义如下:
	work:要调度的工作。
	返回值:0 成功,其他值 失败。
	关于工作队列的参考使用示例如下所示:
示例代码15.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);
    ......
}

15.1.3 设备树中断信息节点
1、GIC中断控制器
GIC全称为:Generic Interrupt Controller,关于GIC的详细内容可以查看文档《ARM Generic Interrupt Controller(ARM GIC控制器)V2.0》,此文档已经放到了开发板光盘中,路径为:开发板光盘 4、参考资料 ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf。
GIC是ARM公司给Cortex-A/R内核提供的一个中断控制器,类似Cortex-M内核中的NVIC。目前GIC有4个版本:V1V4,V1是最老的版本,已经被废弃了。V2V4目前正在大量的使用。GIC V2是给ARMv7-A架构使用的,比如Cortex-A7、Cortex-A9、Cortex-A15等,V3和V4是给ARMv8-A/R架构使用的,也就是64位芯片使用的。RK3568是Cortex-A55内核,因此我们主要讲解GIC V2。GIC V2最多支持8个核。ARM会根据GIC版本的不同研发出不同的IP核,那些半导体厂商直接购买对应的IP核即可,比如ARM针对GIC V2就开发出了GIC400这个中断控制器IP核。当GIC接收到外部中断信号以后就会报给ARM内核,但是ARM内核只提供了四个信号给GIC来汇报中断情况:VFIQ、VIRQ、FIQ和IRQ,他们之间的关系如图15.1.3.1所示:
在这里插入图片描述

图15.1.3.1 中断示意图
在图15.1.3.1中,GIC接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给ARM内核,这四个信号的含义如下:
VFIQ:虚拟快速FIQ。
VIRQ:虚拟快速IRQ。
FIQ:快速中断IRQ。
IRQ:外部中断IRQ。
VFIQ和VIRQ是针对虚拟化的,我们讨论不虚拟化,剩下的就是FIQ和IRQ了,本教程我们只使用IRQ。所以相当于GIC最终向ARM内核就上报一个IRQ信号。那么GIC是如何完成这个工作的呢?GIC V2的逻辑图如图15.1.3.2所示:
在这里插入图片描述

图15.1.3.2 GIC V2总体框图
图15.1.3.2中左侧部分就是中断源,中间部分就是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中断来完成多核之间的通信。
2、中断ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一ID,这些ID就是中断ID。每一个CPU最多支持1020个中断ID,中断ID号为ID0~ID1019。这1020个ID包含了PPI、SPI和SGI,那么这三类中断是如何分配这1020个中断ID的呢?这1020个ID分配如下:
ID0~ID15:这16个ID分配给SGI。
ID16~ID31:这16个ID分配给PPI。
ID32~ID1019:这988个ID分配给SPI,像GPIO中断、串口中断等这些外部中断 ,至于具体到某个ID对应哪个中断那就由半导体厂商根据实际情况去定义了。比如RK3568的外设中断ID对应的中断源可以在《Rockchip RK3568 TRM Part1 V1.1(RK3568参考手册1)》中找到详细的解释。找到“1.3 System Interrupt Connection”小节,中断ID如图15.1.3.3所示(由于表太大,这里只是截取其中一部分):
在这里插入图片描述

图15.1.3.3 RK3568中断源
关于GIC就先讲到这里,我们接下来讲解一下EXTI。
3、GIC控制器节点
打开rk3568.dtsi文件,其中的gic节点就是GIC的中断控制器节点,节点内容如下所示:
示例代码15.1.3.1 中断控制器gic节点

1   gic: interrupt-controller@fd400000 {
2           compatible = "arm,gic-v3";
3           #interrupt-cells = <3>;
4           #address-cells = <2>;
5           #size-cells = <2>;
6           ranges;
7           interrupt-controller;
8 
9           reg = <0x0 0xfd400000 0 0x10000>, /* GICD */
10                <0x0 0xfd460000 0 0xc0000>; /* GICR */
11          interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
12          its: interrupt-controller@fd440000 {
13                  compatible = "arm,gic-v3-its";
14                  msi-controller;
15                  #msi-cells = <1>;
16                  reg = <0x0 0xfd440000 0x0 0x20000>;
17          };
18  };

第2行,compatible属性值为“arm,gic-v3”在Linux内核源码中搜索“arm,gic-v3”即可找到GIC中断控制器驱动文件。
第3行,interrupt-controller节点为空,表示当前节点是中断控制器。
第4行,#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中断来说中断号的范围为321019(具体取决于半导体厂商实际使用了多少个中断号),对于PPI中断来说中断号的范围为1631,但是该cell描述的中断号是从0开始。
第三个cells:标志,bit[3:0]表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit[15:8]为PPI中断的CPU掩码。
我们来看一下RK3568的SPI0是如何在设备树节点中描述中断信息的,首先是查阅《Rockchip RK3568 TRM Part1 V1.1(RK3568参考手册1)》第“1.3 System Interrupt Connection”小节中的表1-3。找到SPI0对应的中断号,如图15.1.3.4所示:
在这里插入图片描述

图15.1.3.4 SPI0中断
从图15.1.3.6可以看出,SPI0的中断号为135,注意这里是加上了前面32个PPI中断号,如果不算前面32个PPI中断号的话就是就是135-32=103。
打开rk3568.dtsi,找到SPI0节点内容,如下所示:
示例代码15.1.3.2 SPI0节点

1 spi0: spi@fe610000 {
2         compatible = "rockchip,rk3066-spi";
3         reg = <0x0 0xfe610000 0x0 0x1000>;
4         interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;
5         #address-cells = <1>;
6         #size-cells = <0>;
7         clocks = <&cru CLK_SPI0>, <&cru PCLK_SPI0>;
8         clock-names = "spiclk", "apb_pclk";
9         dmas = <&dmac0 20>, <&dmac0 21>;
10        dma-names = "tx", "rx";
11        pinctrl-names = "default", "high_speed";
12        pinctrl-0 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins>;
13        pinctrl-1 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins_hs>;
14        status = "disabled";
15};

第4行,interrupts描述中断中断源的信息,第一个表示中断类型,为GIC_SPI,也就是共享中断。第二个表示中断号为103,来源就是图15.1.3.4中的135-32=103。第三个表示中断触发类型是高电平触发。

我们来看一个具体的应用,打开rk3568-evb.dtsi文件,找到如下所示内容:
示例代码15.1.3.6 hdmi节点信息

1095    &i2c0 {
1096        status = "okay";
1097
......
1115
1116        rk809: pmic@20 {
1117            compatible = "rockchip,rk809";
1118            reg = <0x20>;
1119            interrupt-parent = <&gpio0>;
1120            interrupts = <3 IRQ_TYPE_LEVEL_LOW>;
1121
1122            pinctrl-names = "default", "pmic-sleep",
1123                    "pmic-power-off", "pmic-reset";
......
1378        };
1379    };

RK809是正点原子ATK-DLRK3568开发板上核心板的PMIC芯片,上述代码就是RK809的节点信息,RK809芯片有一个中断,此引脚链接到了RK3568的GPIO0_A3上,此中断是电平触发。
第1119行,interrupt-parent属性设置中断控制器,因为GPIO0_A3数据GPIO0组,所以这里设置中断控制器为GPIO0。
第1120行,interrupts设置中断信息,3表示GPIO0_A3属于GPIO0组的第4个IO,前3个为A0~A2。IRQ_TYPE_LEVEL_LOW表示下降沿触发。
结合第1119,1120这两行,目的就是设置GPIO0_A3为下低电平触发。可以看出使用起来是非常的简单,在我们实际编写代码的时候,只需要通过interrupt-parent和interrupts这两个属性即可设置某个GPIO的中断功能。
简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息cells个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。
15.1.4 获取中断号
编写驱动的时候需要用到中断号,我们用到的中断号,中断信息已经写到了设备树里面,因此可以通过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对应的中断号。
15.2 硬件原理图分析
本章实验硬件原理图参考13.2小节即可。本章我们依旧使用《第十三章 Linux按键输入实验》里面的GPIO3_C5引脚来模拟按键。
15.3 实验程序编写
本实验对应的例程路径为:开发板光盘01、程序源码Linux驱动例程12_irq。
本章实验我们驱动正点原子的ATK-DLRK3568开发板上的GPIO3_C5引脚来模拟按键,不过我们采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。通过本章我们可以学习到Linux内核中断的使用方法,以及对Linux内核定时器的回顾。
15.3.1 修改设备树文件
本章实验我们在第十三章的示例代码13.3.1.2创建的key节点基础上补充,添加GPIO3_C5这个引脚相关的中断属性,添加完成以后的“key”节点内容如下所示:
示例代码15.3.1.1 key节点信息

1 key {
2     compatible = "alientek,key";
3     pinctrl-names = "alientek,key";
4     pinctrl-0 = <&key_gpio>;
5     key-gpio = <&gpio3 RK_PC5 GPIO_ACTIVE_HIGH>;
6     interrupt-parent = <&gpio3>;
7     interrupts = <21 IRQ_TYPE_EDGE_BOTH>;
8     status = "okay";
9 };

第6行,设置interrupt-parent属性值为“gpio3”。
第7行,设置interrupts属性,也就是设置中断源,第一个cells的21表示GPIO3组的21号IO,也就是PC5。IRQ_TYPE_EDGE_BOTH定义在文件include/linux/irq.h中,定义如下:
示例代码15.3.1.2 中断状态

74  enum {
75      IRQ_TYPE_NONE       		= 0x00000000,
76      IRQ_TYPE_EDGE_RISING 		= 0x00000001,
77      IRQ_TYPE_EDGE_FALLING   	= 0x00000002,
78      IRQ_TYPE_EDGE_BOTH        = (IRQ_TYPE_EDGE_FALLING | 
IRQ_TYPE_EDGE_RISING),
79      IRQ_TYPE_LEVEL_HIGH 		= 0x00000004,
80      IRQ_TYPE_LEVEL_LOW  		= 0x00000008,
81      IRQ_TYPE_LEVEL_MASK 		= (IRQ_TYPE_LEVEL_LOW | 
IRQ_TYPE_LEVEL_HIGH),
......
99 };
从示例代码15.3.1.2中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于按下和释放都会触发中断。

设备树修改完成以后,重新编译内核,并且烧写到开发板中。
15.3.2 按键中断驱动程序编写
新建名为“12_irq”的文件夹,然后在12_irq文件夹里面创建vscode工程,工作区命名为“keyirq”。工程创建好以后新建keyirq.c文件,在keyirq.c里面输入如下内容:
示例代码15.3.2.1 keyirq.c文件代码

1   /***************************************************************
2   Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3   文件名 	: key.c
4   作者   	: 正点原子Linux团队
5   版本   	: V1.0
6   描述   	: Linux中断驱动实验
7   其他   	: 无
8   论坛   	: www.openedv.com
9   日志   	: 初版V1.0 2022/12/28 正点原子Linux团队创建
10  ***************************************************************/
11  #include <linux/types.h>
12  #include <linux/kernel.h>
13  #include <linux/delay.h>
14  #include <linux/ide.h>
15  #include <linux/init.h>
16  #include <linux/module.h>
17  #include <linux/errno.h>
18  #include <linux/gpio.h>
19  #include <linux/cdev.h>
20  #include <linux/device.h>
21  #include <linux/of.h>
22  #include <linux/of_address.h>
23  #include <linux/of_gpio.h>
24  #include <linux/semaphore.h>
25  #include <linux/of_irq.h>
26  #include <linux/irq.h>
27  //#include <asm/mach/map.h>
28  #include <asm/uaccess.h>
29  #include <asm/io.h>
30  
31  #define KEY_CNT         1       	/* 设备号个数  	*/
32  #define KEY_NAME        "key"   	/* 名字       	*/
33  
34  /* 定义按键状态 */
35  enum key_status {
36      KEY_PRESS = 0,      	/* 按键按下 		*/ 
37      KEY_RELEASE,        	/* 按键松开 		*/ 
38      KEY_KEEP,           	/* 按键状态保持 	*/ 
39  };
40  
41  /* key设备结构体 */
42  struct key_dev{
43      dev_t devid;                	/* 设备号     		*/
44      struct cdev cdev;           	/* cdev     		*/
45      struct class *class;        	/* 类      			*/
46      struct device *device;      	/* 设备    			*/
47      struct device_node  *nd;    	/* 设备节点 			*/
48      int key_gpio;               	/* key所使用的GPIO编号        */
49      struct timer_list timer;    	/* 按键值        	*/
50      int irq_num;                	/* 中断号        	*/
51      spinlock_t spinlock;        	/* 自旋锁        	*/
52  };
53  
54  static struct key_dev key;     	/* 按键设备 		*/
55  static int status = KEY_KEEP; 	/* 按键状态 		*/
56  
57  static irqreturn_t key_interrupt(int irq, void *dev_id)
58  {
59      /* 按键防抖处理,开启定时器延时15ms */
60      mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
61      return IRQ_HANDLED;
62  }
63  
64  /*
65   * @description 	: 初始化按键IO,open函数打开驱动的时候初始化按键所使
66   *                     用的GPIO引脚。
67   * @param       	: 无
68   * @return      	: 无
69   */
70  static int key_parse_dt(void)
71  {
72      int ret;
73      const char *str;
74      
75      /* 设置LED所使用的GPIO */
76      /* 1、获取设备节点:key */
77      key.nd = of_find_node_by_path("/key");
78      if(key.nd == NULL) {
79          printk("key node not find!\r\n");
80          return -EINVAL;
81      }
82  
83      /* 2.读取status属性 */
84      ret = of_property_read_string(key.nd, "status", &str);
85      if(ret < 0) 
86          return -EINVAL;
87  
88      if (strcmp(str, "okay"))
89          return -EINVAL;
90      
91      /* 3、获取compatible属性值并进行匹配 */
92      ret = of_property_read_string(key.nd, "compatible", &str);
93      if(ret < 0) {
94          printk("key: Failed to get compatible property\n");
95          return -EINVAL;
96      }
97  
98      if (strcmp(str, "alientek,key")) {
99          printk("key: Compatible match failed\n");
100         return -EINVAL;
101     }
102 
103     /* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */
104     key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
105     if(key.key_gpio < 0) {
106         printk("can't get key-gpio");
107         return -EINVAL;
108     }
109 
110     /* 5 、获取GPIO对应的中断号 */
111     key.irq_num = irq_of_parse_and_map(key.nd, 0);
112     if(!key.irq_num){
113         return -EINVAL;
114     }
115 
116     printk("key-gpio num = %d\r\n", key.key_gpio);
117     return 0;
118 }
119 
120 static int key_gpio_init(void)
121 {
122     int ret;
123     unsigned long irq_flags;
124     
125     ret = gpio_request(key.key_gpio, "KEY0");
126     if (ret) {
127         printk(KERN_ERR "key: Failed to request key-gpio\n");
128         return ret;
129     }   
130     
131     /* 将GPIO设置为输入模式 */
132     gpio_direction_input(key.key_gpio);
133 
134    /* 获取设备树中指定的中断触发类型 */
135     irq_flags = irq_get_trigger_type(key.irq_num);
136     if (irq_flags == IRQF_TRIGGER_NONE)
137         irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
138         
139     /* 申请中断 */
140     ret = request_irq(key.irq_num, key_interrupt, irq_flags, 
"Key0_IRQ", NULL);
141     if (ret) {
142         gpio_free(key.key_gpio);
143         return ret;
144     }
145 
146     return 0;
147 }
148 
149 static void key_timer_function(struct timer_list *arg)
150 {
151     static int last_val = 0;
152     unsigned long flags;
153     int current_val;
154 
155     /* 自旋锁上锁 */
156     spin_lock_irqsave(&key.spinlock, flags);
157 
158     /* 读取按键值并判断按键当前状态 */
159     current_val = gpio_get_value(key.key_gpio);
160     if (1 == current_val && !last_val)    	/* 按下 	*/ 
161         status = KEY_PRESS;
162     else if (0 == current_val && last_val)
163         status = KEY_RELEASE;               	/* 松开 	*/ 
164     else
165         status = KEY_KEEP;                  	/* 状态保持 	*/ 
166 
167     last_val = current_val;
168 
169     /* 自旋锁解锁 */
170     spin_unlock_irqrestore(&key.spinlock, flags);
171 }
172 
173 /*
174  * @description 	: 打开设备
175  * @param – inode	: 传递给驱动的inode
176  * @param – filp	: 设备文件,file结构体有个叫做private_data的成员变量
177  *                    一般在open的时候将private_data指向设备结构体。
178  * @return       	: 0 成功;其他 失败
179  */
180 static int key_open(struct inode *inode, struct file *filp)
181 {
182     return 0;
183 }
184 
185 /*
186  * @description 	: 从设备读取数据 
187  * @param – filp 	: 要打开的设备文件(文件描述符)
188  * @param – buf 	: 返回给用户空间的数据缓冲区
189  * @param – cnt 	: 要读取的数据长度
190  * @param – offt	: 相对于文件首地址的偏移
191  * @return       	: 读取的字节数,如果为负值,表示读取失败
192  */
193 static ssize_t key_read(struct file *filp, char __user *buf,
194             size_t cnt, loff_t *offt)
195 {
196     unsigned long flags;
197     int ret;
198 
199     /* 自旋锁上锁 */
200     spin_lock_irqsave(&key.spinlock, flags);
201 
202     /* 将按键状态信息发送给应用程序 */
203     ret = copy_to_user(buf, &status, sizeof(int));
204 
205     /* 状态重置 */
206     status = KEY_KEEP;
207 
208     /* 自旋锁解锁 */
209     spin_unlock_irqrestore(&key.spinlock, flags);
210 
211     return ret;
212 }
213 
214 /*
215  * @description 	: 向设备写数据 
216  * @param - filp 	: 设备文件,表示打开的文件描述符
217  * @param - buf 	: 要写给设备写入的数据
218  * @param - cnt 	: 要写入的数据长度
219  * @param - offt 	: 相对于文件首地址的偏移
220  * @return        	: 写入的字节数,如果为负值,表示写入失败
221  */
222 static ssize_t key_write(struct file *filp, const char __user *buf, 
size_t cnt, loff_t *offt)
223 {
224     return 0;
225 }
226 
227 /*
228  * @description 	: 关闭/释放设备
229  * @param - filp 	: 要关闭的设备文件(文件描述符)
230  * @return       	: 0 成功;其他 失败
231  */
232 static int key_release(struct inode *inode, struct file *filp)
233 {
234     return 0;
235 }
236 
237 /* 设备操作函数 */
238 static struct file_operations key_fops = {
239     .owner = THIS_MODULE,
240     .open = key_open,
241     .read = key_read,
242     .write = key_write,
243     .release =  key_release,
244 };
245 
246 /*
247  * @description 	: 驱动入口函数
248  * @param       	: 无
249  * @return      	: 无
250  */
251 static int __init mykey_init(void)
252 {
253     int ret;
254     
255     /* 初始化自旋锁 */
256     spin_lock_init(&key.spinlock);
257     
258     /* 1、初始化timer,设置定时器处理函数,还未设置周期,所以不会激活定时器 */
259     timer_setup(&key.timer, key_timer_function, 0);
260 
261     /* 2、设备树解析 */
262     ret = key_parse_dt();
263     if(ret)
264         return ret;
265         
266     /* 3、GPIO中断初始化 */
267     ret = key_gpio_init();
268     if(ret)
269         return ret;
270         
271     /* 注册字符设备驱动 */
272     /* 1、创建设备号 */
273     ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);    
274     if(ret < 0) {
275         pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", 
KEY_NAME, ret);
276         goto free_gpio;
277     }
278     
279     /* 2、初始化cdev */
280     key.cdev.owner = THIS_MODULE;
281     cdev_init(&key.cdev, &key_fops);
282     
283     /* 3、添加一个cdev */
284     ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
285     if(ret < 0)
286         goto del_unregister;
287         
288     /* 4、创建类 */
289     key.class = class_create(THIS_MODULE, KEY_NAME);
290     if (IS_ERR(key.class)) {
291         goto del_cdev;
292     }
293 
294     /* 5、创建设备 */
295     key.device = device_create(key.class, NULL, key.devid, NULL, 
KEY_NAME);
296     if (IS_ERR(key.device)) {
297         goto destroy_class;
298     }
299     
300     return 0;
301 
302 destroy_class:
303     class_destroy(key.class);
304 del_cdev:
305     cdev_del(&key.cdev);
306 del_unregister:
307     unregister_chrdev_region(key.devid, KEY_CNT);
308 free_gpio:
309     free_irq(key.irq_num, NULL);
310     gpio_free(key.key_gpio);
311     return -EIO;
312 }
313 
314 /*
315  * @description 	: 驱动出口函数
316  * @param       	: 无
317  * @return      	: 无
318  */
319 static void __exit mykey_exit(void)
320 {
321     /* 注销字符设备驱动 */
322     cdev_del(&key.cdev);				/* 删除cdev 	*/
323     unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */
324     del_timer_sync(&key.timer);     /* 删除timer 	*/
325     device_destroy(key.class, key.devid);/*注销设备 */
326     class_destroy(key.class);       	/* 注销类 		*/
327     free_irq(key.irq_num, NULL);  	/* 释放中断 		*/
328     gpio_free(key.key_gpio);        	/* 释放IO 		*/
329 }
330 
331 module_init(mykey_init);
332 module_exit(mykey_exit);
333 MODULE_LICENSE("GPL");
334 MODULE_AUTHOR("ALIENTEK");
335 MODULE_INFO(intree, "Y");

第35~39行,定义了一个枚举类型,包含3个常量KEY_PRESS、KEY_RELEASE、KEY_KEEP,分别用来表示按键的3种不同的状态,即按键按下、按键松开以及按键状态保持。
第42~52行,结构体key_dev为按键设备所对应的结构体,key_gpio为按键GPIO编号,irq_num为按键IO对应的中断号;除此之外,结构体当中还定义了一个定时器用于实现按键的去抖操作,还定义了一个自旋锁用于实现对关键代码的保护操作。
第54行,定义一个按键设备key。
第55行,定义一个int类型的静态全局变量status用来表示按键的状态。
第57~62行,key_interrupt函数是中断处理函数,参数dev_id是一个void类型的指针,本驱动程序并没使用到这个参数;这个中断处理函数很简单直接开启定时器,延时15毫秒,用于实现按键的软件防抖。
第70~118行,key_parse_dt函数中主要是对设备树中的属性进行了解析,获取设备树中的key节点,通过of_get_named_gpio函数得到按键的GPIO编号,通过irq_of_parse_and_map函数获取按键的中断号,irq_of_parse_and_map函数会解析key节点中的interrupt-parent和interrupts属性然后得到一个中断号,后面就可以使用这个中断号去申请以及释放中断了。
第120~147行,key_gpio_init函数中主要对GPIO以及中断进行了相关的初始化。使用gpio_request函数申请GPIO使用权,通过gpio_direction_input将GPIO设置为输入模式;irq_get_trigger_type函数可以获取到key节点中定义的中断触发类型,最后使用request_irq申请中断,并设置key_interrupt函数作为我们的按键中断处理函数,当按键中断发生之后便会跳转到该函数执行;request_irq函数会默认使能中断,所以不需要enable_irq来使能中断,当然,我们也可以在申请成功之后先使用disable_irq函数禁用中断,等所有工作完成之后再来使能中断,这样会比较安全,建议大家这样使用。
第149171行,key_timer_function函数为定时器定时处理函数,它的参数arg在本驱动程序中我们并没有使用到;该函数中定义了一个静态局部变量last_val用来保存按键上一次读取到的值,变量current_val用来存放当前按键读取到的值;第159165行,通过读取到的按键值以及上一次读取到的值来判断按键当前所属的状态,如果本次读取的值为1,而上一次读取的值0,则表示按键按下;如果本次读取的值为0,而上一次读取的值1,则表示按键松开;如果本次读取的值为1,而上一次读取的值也是1,则表示按键一直被按着;如果本次读取的值0,而上一次读取的值也是0,则表示没有触碰按键。第167行,当状态判断完成之后,会将current_val的值赋值给last_val。本函数中也使用自旋锁对全局变量status进行加锁保护!
第193~212行,key_read函数,对应应用程序的read函数。此函数向应用程序返回按键状态信息数据;这个函数其实很简单,使用copy_to_user函数直接将statue数据发送给应用程序,status变量保存了按键当前的状态,发送完成之后再将按键状态重置即可!需要注意的是,该函数中使用了自旋锁进行保护。
第238~244行,按键设备的file_operations结构体。
第251~312行,mykey_init是驱动入口函数,第256行调用spin_lock_init初始化自旋锁变量,259行对定时器进行初始化并将key_timer_function函数绑定为定时器定时处理函数,当定时时间到了之后便会跳转到该函数执行。
第319~329行,mykey_exit驱动出口函数,第324行调用del_timer_sync函数删除定时器,代码中已经注释得非常详细了,这里便不再多说!
15.3.2 编写测试APP
测试APP要实现的内容很简单,通过不断的读取/dev/key设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有3个值,分别为0、1、2;0表示按键按下时的这个状态,1表示按键松开时对应的状态,而2表示按键一直被按住或者松开;搞懂数据代表的意思之后,我们开始编写测试程序,在12_irq目录下新建名为keyirqApp.c的文件,然后输入如下所示内容:
示例代码15.3.3.1 keyrqApp.c文件代码

1   /***************************************************************
2   Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3   文件名    	: keyApp.c
4   作者        	: 正点原子Linux团队
5   版本       	: V1.0
6   描述        	: Linux中断驱动实验
7   其他      	: 无
8   使用方法   	: ./keyirqApp /dev/key
9   论坛       	: www.openedv.com
10  日志       	: 初版V1.0 2022/12/29 正点原子Linux团队创建
11  ***************************************************************/
12
13  #include <stdio.h>
14  #include <unistd.h>
15  #include <sys/types.h>
16  #include <sys/stat.h>
17  #include <fcntl.h>
18  #include <stdlib.h>
19  #include <string.h>
20
21  /*
22   * @description  	: main主程序
23   * @param – argc 	: argv数组元素个数
24   * @param – argv 	: 具体参数
25   * @return        	: 0 成功;其他 失败
26   */
27  int main(int argc, char *argv[])
28  {
29      int fd, ret;
30      int key_val;
31
32      /* 判断传参个数是否正确 */
33      if(2 != argc) {
34          printf("Usage:\n"
35               "\t./keyApp /dev/key\n"
36              );
37          return -1;
38      }
39
40      /* 打开设备 */
41      fd = open(argv[1], O_RDONLY);
42      if(0 > fd) {
43          printf("ERROR: %s file open failed!\n", argv[1]);
44          return -1;
45      }
46
47      /* 循环读取按键数据 */
48      for ( ; ; ) {
49
50          read(fd, &key_val, sizeof(int));
51          if (0 == key_val)
52              printf("Key Press\n");
53          else if (1 == key_val)
54              printf("Key Release\n");
55      }
56
57      /* 关闭设备 */
58      close(fd);
59      return 0;

60 }
第48~55行使用for循环不断的读取按键值,如果读取到的值是0则打印“Key Press”字符串,而过读取到的值是1则打印“Key Release”字符串。
15.4 运行测试
15.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第五章实验基本一样,只是将obj-m变量的值改为keyirq.o,Makefile内容如下所示:
示例代码15.4.1.1 Makefile文件

1  KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
...... 
4  obj-m := keyirq.o
......
11 clean:
12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,设置obj-m变量的值为keyirq.o。
输入如下命令编译出驱动模块文件:

make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译成功以后就会生成一个名为“keyirq.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试keyirqApp.c这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc keyirqApp.c -o keyirqApp
编译成功以后就会生成keyirqApp这个应用程序。
15.4.2 运行测试
在Ubuntu中将上一小节编译出来的keyirq.ko和keyirqApp这两个文件通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push keyirq.ko keyirqApp /lib/modules/4.19.232
发送成功以后进入到开发板目录lib/modules/4.19.232中,输入如下命令加载keyirq.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe keyirq //加载驱动
驱动加载成功以后可以通过查看/proc/interrupts文件来检查一下对应的中断有没有被注册上,输入如下命令:
cat /proc/interrupts
结果如图15.4.2.1所示:
在这里插入图片描述

图15.4.2.1 proc/interrupts文件内容
从图15.4.2.1可以看出keyirq.c驱动文件里面的KEY0中断已经存在了,触发方式为跳边沿(Edge)。
接下来使用如下命令来测试中断:
./keyirqApp /dev/key
使用杜邦线将图13.2.1中GPIO3_C5这个IO接到开发板的3.3V电压上,模拟按键被按下,终端就会输出按键值,如图15.4.2.2所示:
在这里插入图片描述

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值