您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
   
 
 订阅
万字长文讲透linux的中断管理机制
 
 
  63  次浏览      5 次
 2025-1-20
 
编辑推荐:
本文从中断相关概念开始,并介绍Linux中的中断机制,最后以实现一个简单的按键中断驱动程序结束。 希望对您的学习有所帮助。
本文来自于微信公众号Linux开发架构之路,由火龙果软件Linda编辑、推荐。

不管是裸机实验还是 Linux 下的驱动实验,中断都是频繁使用的功能,在裸机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能 IRQ 等等。Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。这篇文章将从中断相关概念开始,并介绍Linux中的中断机制,最后以实现一个简单的按键中断驱动程序结束,话不多说,马上开始。

异常与中断

中断

中断是一种事件,它改变程序的正常执行流,可以由硬件设备甚至CPU本身生成。当中断发生时,当前的执行流将被暂停,中断处理程序运行。中断处理程序运行完毕后,先前的执行流将恢复。例如,每当你按下键盘上的一个键时,CPU会被中断,从而使计算机可以从键盘读取用户输入。这发生得非常快,所以你不会注意到任何变化或损害用户体验。

这里有一个类比加深对中断的理解:

假设你正在家里专心地写作(相当于CPU在执行当前的程序)。这时,电话铃声响起(相当于硬件中断信号),你需要暂停写作去接电话。

你在写作(CPU正在执行当前的指令流)。

电话铃声响起(硬件设备触发中断信号)。

你暂时停下写作(CPU停止当前指令的执行)。

你记住你在写作的内容和位置(CPU保存当前执行状态和上下文)。

你去接电话(CPU调用中断服务程序ISR处理中断)。

你处理完电话上的事情(ISR完成中断处理)。

你挂断电话(中断处理结束)。

你回到书桌继续写作(CPU恢复之前保存的执行状态和上下文,继续执行被中断的程序)。

在了解异常之前,我想先讲下广义和狭义的中断概念,因为这个概念混淆了我很久,只有区分清楚,才能知道如何正确看待中断与异常的关系。

广义的中断:在广义上,中断指的是任何打断正常程序执行流程的事件。这包括由外部设备触发的硬件中断,也包括由CPU内部检测到的错误或特殊情况触发的异常。因此,从这个角度来看,异常可以被视为中断的一个子类。

狭义的中断:在狭义上,中断专指由外部硬件设备触发的事件,与异常区分开来。在这种定义下,中断和异常是并列的两类事件,分别处理不同的情况。

接下来将以广义的角度讲解中断,但是我们要知道一点,真实的异常与中断的处理程序是不一样的,中断使用中断处理程序(Interrupt Service Routine, ISR),ISR通常由设备驱动程序定义,每种中断类型(例如键盘中断、网络中断)有专门的ISR。异常使用异常处理程序(Exception Handler),通常由操作系统内核定义,每种异常类型(例如除零异常、页面错误)有专门的处理程序。

异常

异常通常与中断一起讨论。与中断不同,异常是相对于处理器时钟同步发生的;它们通常被称为同步中断。异常是处理器在执行指令时产生的,可能是由于编程错误(如除零)或必须由内核处理的异常情况(如页面错误)。由于许多处理器架构以类似于中断的方式处理异常,因此内核处理两者的基础设施相似。

中断类型

中断类型有三种,分别是:

硬件中断(Hardware Interrupts)

软件中断(Software Interrupts)

异常(Exceptions)

硬件中断

硬件中断由外部硬件设备触发,用于通知CPU处理外部事件或设备请求。

外部设备中断:由外围设备(如键盘、鼠标、硬盘、网络接口卡等)触发。例如,当键盘按键被按下时,会产生键盘中断。

定时器中断:由系统定时器触发,用于实现定时任务或系统时钟的更新。

电源管理中断:由电源管理设备触发,用于处理电源状态变化,如电池电量低、电源插入或拔出等。

软件中断

软件中断由程序指令触发,用于实现系统调用或用户态与内核态的切换。

系统调用中断:程序通过特定指令(如x86架构中的INT指令)触发,进入内核态执行系统调用。

用户定义中断:由用户程序显式触发,用于实现特定的功能或异常处理。

异常

异常是由CPU在执行指令过程中检测到的错误或特殊情况触发的,用于处理程序错误或特殊事件。

异常分为三种:陷阱、故障和终止。

陷阱(Traps):

来源:由指令执行过程中预期的事件触发,如系统调用。

处理结果:通常处理后继续执行下一条指令。

示例:系统调用陷阱。

故障(Faults):

来源:由指令执行过程中检测到的可恢复错误触发。

处理结果:处理后通常可以恢复并重新执行导致故障的指令。

示例:页面错误(Page Fault)、除零错误(Divide-by-zero Fault)。

终止(Aborts):

来源:由指令执行过程中检测到的不可恢复错误触发。

处理结果:通常无法恢复,进程终止。

示例:硬件故障、内存校验错误(Machine Check Abort)。

中断控制器

中断控制器(Interrupt Controller)是一个硬件模块,它负责接收多个外部和内部中断源的中断请求,并按照优先级和配置将这些中断请求发送给处理器。对于复杂系统,尤其是多处理器系统,中断控制器是不可或缺的组件,因为它能够高效地处理和调度大量的中断请求。

CPU每执行完一条指令后,就会检查中断控制器(如Arm架构的GIC)是否有中断请求。此时,CPU可以决定是否中断当前程序的执行(如果设置了屏蔽中断),跳转到中断处理程序。

这里还是用一个类比来加深对中断控制器的理解:想象一个繁忙的办公室,前台接待员负责接收并处理来自不同访客和电话的请求。办公室里的员工(处理器)正在处理各种各样的工作(指令执行)。

接收请求:接待员(中断控制器)接收并记录所有访客和电话的请求(中断信号)。

判断优先级:接待员根据请求的紧急程度决定处理顺序(中断控制器判断中断优先级)。

分配请求:接待员将高优先级请求分配给相应的员工(中断控制器将中断请求发送到目标处理器)。

处理请求:员工暂停当前工作,处理访客或电话(处理器暂停当前指令,执行中断服务程序)。

完成请求:员工处理完访客或电话后,返回继续工作(处理器处理完中断后,恢复执行被中断的指令)。

以上介绍的知识与操作系统无相关联,它属于计算机体系结构层面的概念,所以不管是裸机、RTOS还是Linux,都同样适用。

Linux 系统对中断的处理

进程上下文和中断上下文

内核使用进程上下文和中断上下文的组合方式来完成各种任务。

什么是上下文

上下文是指在某个特定时刻,CPU的状态和执行环境所需的信息集合。它包括了执行程序所需的所有寄存器、程序计数器、堆栈指针,以及与执行状态相关的数据。

因此我们可以得知,进程上下文和中断上下文拥有不同的执行环境和状态信息。

进程上下文

进程上下文是指内核代码在代表用户进程执行时的状态。服务于用户应用程序发出的系统调用的内核代码是在进程上下文中运行的。

它拥有如下特性:

代表用户进程执行:在进程上下文中,内核代码代表某个具体的用户进程执行,因此拥有该进程的上下文(如进程控制块、内存地址空间)。

可以睡眠和阻塞:在进程上下文中,内核代码可以调用可能导致睡眠或阻塞的函数,因为进程可以被调度器调度并在适当的时候重新唤醒。

特权级别:内核代码在进程上下文中执行时具有内核态特权,可以访问内核数据结构和资源。

继续通过一个类比来加深理解:员工(代表一个进程)在办公室里工作,处理日常任务和项目。

1.日常工作:员工按照日常安排和优先级处理手头的任务(系统调用、用户进程的请求)。

2.可以打断和等待:

员工可以被打断去开会(阻塞),会后继续工作。

如果需要某个同事的帮助,员工可以等待同事的回复(睡眠),在等待期间做其他事情或休息(调度其他进程)。

3.上下文信息:员工的办公桌上有他们的工作文件和工具(进程控制块、内存地址空间等),确保他们可以有效地完成工作。

4.权限和资源:员工可以访问公司资源(内核数据结构和资源),但需要遵循公司的规章制度(内核态特权)。

中断上下文

中断上下文是指内核代码在处理中断请求时的状态。中断处理程序(ISR)在中断上下文中异步运行。

它拥有如下特性:

无特定进程关联:中断上下文不代表任何具体的用户进程,因此没有与特定进程相关的上下文。

不能睡眠或阻塞:在中断上下文中,内核代码不能调用可能导致睡眠或阻塞的函数,因为中断处理程序需要尽快完成以恢复正常执行。

高优先级:中断处理程序通常具有高优先级,以便迅速响应硬件事件。

中断上下文一旦运行就不能终止,它必须尽可能快的完成,这个过程中不能被其他中断信号中断。中断上下文中的代码不能执行以下操作:

进入睡眠状态或阻塞

获取互斥锁

执行耗时任务

访问用户空间虚拟内存

接下来通过一个类比加深理解:办公室里有一个专门的紧急任务响应人员(中断服务程序ISR),它负责处理突发事件(中断请求)。

1.紧急任务:当发生紧急事件(如火警报警、重要客户来电),响应人员立即停止当前工作,去处理突发事件(中断处理)。

2.快速响应:响应人员必须快速处理紧急任务(ISR必须尽快完成),确保紧急情况得到及时解决。

3.不允许等待和阻塞:

响应人员不能在处理紧急任务时等待其他人(不能睡眠或阻塞)。

必须尽快处理完紧急任务并返回继续监控(完成中断处理)。

4.没有具体办公桌:响应人员没有固定的办公桌(无进程上下文),他们不依赖于具体的文件和工具,只需处理紧急情况。

5.快速通知他人:如果紧急任务需要进一步处理,响应人员可以通知其他同事(调度下半部,如软中断、任务队列),这些同事可以在日常工作中完成后续处理。

如果在接收到中断时需要完成大量工作,该怎么办?这就是问题所在。如果处理时间过长,会发生以下情况:

在最高优先级ISR运行时,它不会让其他中断运行。

同类型的中断将被遗漏(中断信号不能中断ISR)。

为了解决这个问题,在Linux系统中,中断的处理被分为两部分,上半部和下半部。为了便于理解,我们可以把上半部理解成硬件中断,也就是由硬件中断信号来触发的,它必须尽快的完成,且不能被其他中断信号中断,而下半部是软件中断,它是由软件来手动触发的,它负责完成处理时间长的任务,它可以被其他中断信号中断。

上半部与下半部

在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用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 中,内容如下:


struct softirq_action
{
void (*action)(struct softirq_action *);
};
 

 

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

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 个元素。

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:要开启的软中断。

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

返回值:没有返回值。

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

void raise_softirq(unsigned int nr)

 

函数参数和返回值含义如下:

nr:要触发的软中断。

返回值:没有返回值。

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

void __init softirq_init(void)

{

int cpu;

for_each_possible_cpu(cpu) {

per_cpu(tasklet_vec, cpu).tail =

&per_cpu(tasklet_vec, cpu).head;

per_cpu(tasklet_hi_vec, cpu).tail =

&per_cpu(tasklet_hi_vec, cpu).head;

}

open_softirq(TASKLET_SOFTIRQ, tasklet_action);

open_softirq(HI_SOFTIRQ, tasklet_hi_action);

}

 

(2)tasklet

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 的参数 */

};

 

func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用 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 定义在 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。

返回值:没有返回值。

(3)工作队列

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

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

struct work_struct {

atomic_long_t data;

struct list_head entry;

work_func_t func; /* 工作队列处理函数 */

};

 

这些工作组织成工作队列,工作队列使用 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 结构体内容如下:

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;

bool desc_valid;

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;

char desc[WORKER_DESC_LEN];

struct workqueue_struct *rescue_wq;

};

 

从上文内容可以看出,每个 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 成功,其他值 失败。

设备树中断信息节点

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

intc: interrupt-controller@00a01000 {

compatible = "arm,cortex-a7-gic";

#interrupt-cells = <3>;

interrupt-controller;

reg = <0x00a01000 0x1000>,

<0x00a02000 0x100>;

};

 

第 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 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断号的范围为 0~15。

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

第 4 行, interrupt-controller 节点为空,表示当前节点是中断控制器。

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

①、 #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 对应的中断号。

程序编写

1.驱动程序

#include <linux/types.h>

#include <linux/kernel.h>

#include <linux/delay.h>

#include <linux/ide.h>

#include <linux/init.h>

#include <linux/module.h>

#include <linux/errno.h>

#include <linux/gpio.h>

#include <linux/cdev.h>

#include <linux/device.h>

#include <linux/of.h>

#include <linux/of_address.h>

#include <linux/of_gpio.h>

#include <linux/semaphore.h>

#include <linux/timer.h>

#include <linux/of_irq.h>

#include <linux/irq.h>

#include <asm/mach/map.h>

#include <asm/uaccess.h>

#include <asm/io.h>

#define IMX6UIRQ_CNT 1 /* 设备号个数 */

#define IMX6UIRQ_NAME "imx6uirq" /* 名字 */

#define KEY0VALUE 0X01 /* KEY0按键值 */

#define INVAKEY 0XFF /* 无效的按键值 */

#define KEY_NUM 1 /* 按键数量 */

/* 中断IO描述结构体 */

struct irq_keydesc {

int gpio; /* gpio */

int irqnum; /* 中断号 */

unsigned char value; /* 按键对应的键值 */

char name[10]; /* 名字 */

irqreturn_t (*handler)(int, void *); /* 中断服务函数 */

};

/* imx6uirq设备结构体 */

   struct imx6uirq_dev{

       dev_t devid; /* 设备号 */

      struct cdev cdev; /* cdev */

      struct class *class; /* 类 */

      struct device *device; /* 设备 */

      int major; /* 主设备号 */

      int minor; /* 次设备号 */

      struct device_node *nd; /* 设备节点 */

      atomic_t keyvalue; /* 有效的按键键值 */

      atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */

      struct timer_list timer;/* 定义一个定时器*/

      struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */

      unsigned char curkeynum; /* 当前的按键号 */

};

struct imx6uirq_dev imx6uirq; /* irq设备 */

/* @description : 中断服务函数,开启定时器,延时10ms,

    * 定时器用于按键消抖。

    * @param - irq : 中断号

    * @param - dev_id : 设备结构。

   * @return : 中断执行结果

   */

static irqreturn_t key0_handler(int irq, void *dev_id)

{

   struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

   dev->curkeynum = 0;

    dev->timer.data = (volatile long)dev_id;

   mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */

   return IRQ_RETVAL(IRQ_HANDLED);

}

/* @description : 定时器服务函数,用于按键消抖,定时器到了以后

* 再次读取按键值,如果按键还是处于按下状态就表示按键有效。

* @param - arg : 设备结构变量

* @return : 无

*/

void timer_function(unsigned long arg)

{

   unsigned char value;

    unsigned char num;

   struct irq_keydesc *keydesc;

    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;

    num = dev->curkeynum;

    keydesc = &dev->irqkeydesc[num];

   value = gpio_get_value(keydesc->gpio); /* 读取IO值 */

   if(value == 0){ /* 按下按键 */

      atomic_set(&dev->keyvalue, keydesc->value);

}

else{ /* 按键松开 */

      atomic_set(&dev->keyvalue, 0x80 | keydesc->value);

      atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */

   }

   }

/*

* @description : 按键IO初始化

* @param : 无

* @return : 无

*/

static int keyio_init(void)

{

   unsigned char i = 0;

   char name[10];

    int ret = 0;

   imx6uirq.nd = of_find_node_by_path("/key");

   if (imx6uirq.nd== NULL){

      printk("key node not find!\r\n");

       return -EINVAL;

   }

    /* 提取GPIO */

   for (i = 0; i < KEY_NUM; i++) {

      imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);

      if (imx6uirq.irqkeydesc[i].gpio < 0) {

         printk("can't get key%d\r\n", i);

      }

}

   /* 初始化key所使用的IO,并且设置成中断模式 */

   for (i = 0; i < KEY_NUM; i++) {

       memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(name)); /* 缓冲区清零 */

      sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */

      gpio_request(imx6uirq.irqkeydesc[i].gpio, name);

      gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);

      imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);

#if 0

      imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);

#endif

       printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio,

imx6uirq.irqkeydesc[i].irqnum);

   }

   /* 申请中断 */

   imx6uirq.irqkeydesc[0].handler = key0_handler;

   imx6uirq.irqkeydesc[0].value = KEY0VALUE;

   for (i = 0; i < KEY_NUM; i++) {

      ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler,

         IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);

    if(ret < 0){

      printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);

      return -EFAULT;

   }

}

/* 创建定时器 */

   init_timer(&imx6uirq.timer);

    imx6uirq.timer.function = timer_function;

    return 0;

}

/*

* @description : 打开设备

* @param - inode : 传递给驱动的inode

* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量

* 一般在open的时候将private_data指向设备结构体。

* @return : 0 成功;其他 失败

*/

static int imx6uirq_open(struct inode *inode, struct file *filp)

{

   filp->private_data = &imx6uirq; /* 设置私有数据 */

    return 0;

}

/*

* @description : 从设备读取数据

* @param - filp : 要打开的设备文件(文件描述符)

* @param - buf : 返回给用户空间的数据缓冲区

* @param - cnt : 要读取的数据长度

* @param - offt : 相对于文件首地址的偏移

* @return : 读取的字节数,如果为负值,表示读取失败

*/

static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)

{

   int ret = 0;

   unsigned char keyvalue = 0;

   unsigned char releasekey = 0;

    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

    keyvalue = atomic_read(&dev->keyvalue);

   releasekey = atomic_read(&dev->releasekey);

    if (releasekey) { /* 有按键按下 */

       if (keyvalue & 0x80) {

      keyvalue &= ~0x80;

      ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));

   } else {

       goto data_error;

   }

      atomic_set(&dev->releasekey, 0);/* 按下标志清零 */

   } else {

      goto data_error;

    }

    return 0;

data_error:

    return -EINVAL;

}

/* 设备操作函数 */

static struct file_operations imx6uirq_fops = {

   .owner = THIS_MODULE,

   .open = imx6uirq_open,

    .read = imx6uirq_read,

};

/*

* @description : 驱动入口函数

* @param : 无

* @return : 无

*/

static int __init imx6uirq_init(void)

{

   /* 1、构建设备号 */

   if (imx6uirq.major) {

      imx6uirq.devid = MKDEV(imx6uirq.major, 0);

       register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);

} else {

       alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);

      imx6uirq.major = MAJOR(imx6uirq.devid);

      imx6uirq.minor = MINOR(imx6uirq.devid);

}

/* 2、注册字符设备 */

cdev_init(&imx6uirq.cdev, &imx6uirq_fops);

cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

/* 3、创建类 */

imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);

if (IS_ERR(imx6uirq.class)) {

    return PTR_ERR(imx6uirq.class);

}

/* 4、创建设备 */

imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);

if (IS_ERR(imx6uirq.device)) {

    return PTR_ERR(imx6uirq.device);

}

/* 5、初始化按键 */

atomic_set(&imx6uirq.keyvalue, INVAKEY);

atomic_set(&imx6uirq.releasekey, 0);

keyio_init();

return 0;

}

/*

* @description : 驱动出口函数

* @param : 无

* @return : 无

*/

static void __exit imx6uirq_exit(void)

{

   unsigned int i = 0;

   /* 删除定时器 */

   del_timer_sync(&imx6uirq.timer); /* 删除定时器 */

    /* 释放中断 */

   for (i = 0; i < KEY_NUM; i++) {

      free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);

    }

    cdev_del(&imx6uirq.cdev);

   unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);

    device_destroy(imx6uirq.class, imx6uirq.devid);

   class_destroy(imx6uirq.class);

}

module_init(imx6uirq_init);

module_exit(imx6uirq_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("jiajia2020");

 

首先设置按键中断,在按键中断处理函数中mod_timer,进行10ms的定时,以此作为消抖处理。在定时中断处理函数中读取按键的值,检查按键状态是否有效。在设备读取函数中将按键值传递给用户空间数据缓冲区。

2.App测试程序

#include "stdio.h"

#include "unistd.h"

#include "sys/types.h"

#include "sys/stat.h"

#include "fcntl.h"

#include "stdlib.h"

#include "string.h"

#include "linux/ioctl.h"

/*

* @description : main主程序

* @param - argc : argv数组元素个数

* @param - argv : 具体参数

* @return : 0 成功;其他 失败

*/

int main(int argc, char *argv[])

{

   int fd;

   int ret = 0;

   int data = 0;

   char *filename;

    unsigned char data;

    if (argc != 2) {

      printf("Error Usage!\r\n");

       return -1;

}

filename = argv[1];

fd = open(filename, O_RDWR);

if (fd < 0) {

   printf("Can't open file %s\r\n", filename);

    return -1;

}

while (1) {

    ret = read(fd, &data, sizeof(data));

    if (ret < 0) { /* 数据读取错误或者无效 */

} else { /* 数据读取正确 */

    if (data) /* 读取到数据 */

       printf("key value = %#X\r\n", data);

}

}

    close(fd);

    return ret;

}

 

不断循环读取按键值数据,若读取到就将其打印出来。

编译测试

将驱动加载完成后,输出信息如下:

输入如下命令来检查一下对应的中断有没有被注册上:

cat /proc/interrupts

结果如下:

按开发板key0按键:

终端输出内容如下:

 

   
63 次浏览       5
 
相关文章

CMM之后对CMMI的思考
对软件研发项目管理的深入探讨
软件过程改进
软件过程改进的实现
 
相关文档

软件过程改进框架
软件过程改进的CMM-TSP-PSP模型
过程塑造(小型软件团队过程改进)
软件过程改进:经验和教训
 
相关课程

以"我"为中心的过程改进(iProcess )
iProcess过程改进实践
CMMI体系与实践
基于CMMI标准的软件质量保证

最新活动计划
SysML和EA系统设计与建模 1-16[北京]
企业架构师(业务、应用、技术) 1-23[北京]
大语言模型(LLM)Fine Tune 2-22[在线]
MBSE(基于模型的系统工程)2-27[北京]
OpenGauss数据库调优实践 3-11[北京]
UAF架构体系与实践 3-25[北京]
 
 
最新文章
iPerson的过程观:要 过程 or 结果
基于模型的需求管理方法与工具
敏捷产品管理之 Story
敏捷开发需求管理(产品backlog)
Kanban看板管理实践精要
最新课程
基于iProcess的敏捷过程
软件开发过程中的项目管理
持续集成与敏捷开发
敏捷过程实践
敏捷测试-简单而可行
更多...   
成功案例
英特尔 SCRUM-敏捷开发实战
某著名汽车 敏捷开发过程与管理实践
北京 敏捷开发过程与项目管理
东方证券 基于看板的敏捷方法实践
亚信 工作量估算
更多...