linux按键驱动示例

来源:互联网 发布:软件测试思路 编辑:程序博客网 时间:2024/04/26 05:36
键盘在所有的驱动之中最为简单的一种,但它却包含了驱动的基本框架,对以后继续深入学习其他复杂的 驱动大有裨益,以下便为你逐步剖析驱动的开发。采用的是查询方式。转载请注明出处: 一.内核模块的注册和撤销 在加载模块的时候,首先运行的是内核模块的注册函数。它的功能包括内核注册设备以及变量的初始化。 static int head,tail; int _init Keypad_init(void)
{ int result; result=register_chrdev(KEY_LED_MAJOR,KEY_LED_NAME,&Keypad_fops); Keypad_clear(); init_waitqueue_head(&queue); prink("%s %s initialized.\n",KEY_LED_NAME,KEY_LED_VERSION);//不能用 prinf return 0; } module_init(Keypad_init);//加载模块 void _exit Keypad_cleanup(void) { del_timer(&timer); unregister_chrdev(KEY_LED_MAJOR,KEY_LED_NAME); prink("Keypad driver removed \n"); } module_exit(Keypad_cleanup);//卸载该模块 二.虚拟文件系统与硬件驱动的接口 static struct file_operations Keypad_fops={ open:Keypad_open, read:Keypad_read, poll:Keypad_poll, fasync:Keypad_fasync, release:Keypad_release, }; 该接口定义完之后一些便是对这几个具体函数的实现了!现在我们一起进入下一步吧,是不是觉得其实没 什么难度的呢?别那么早开心着呢?这几个函数的实现时候,涉及到很多技术,包括内核定时器,*等待队 列的具体实现(阻塞方式),异步方式的具体实现技巧,循环队列。看到这么多技术你是否感到很兴奋呢? 以下本人将以通俗的方式为你讲解,希望你能理解。 三.设备的打开操作接口函数具体实现(Keypad_open) 设备打开一般包括两大操作,一是完成设备的初始化,二是设备引用计数器加 1 static int Keypad_open(struct inode *inode,struct file *filp) { read_xy();
try_module_get(THIS_MODULE);//此函数为 Linux 2.6 内核增加的,不同于 2.4 内核,功能是计数器的值 加1 return 0; } static void read_xy(void) { new_data();//获取键值函数 keypad_starttimer();//开启内核定时器,在固定周期时间内获取键盘新的变化 } 以下实现键盘键值获取函数 read_xy() 主要是从 KEY_CS(对应的读入地址,之前可以根据具体的硬件设备定义,比如#define kEY_CS(*(volatile unsigned short *)(0xf820000))此处应该根据具体的不同而不同! 将读入的键值存入 buf[]缓存中,环形缓冲的写指针是 head,读指针是 tail,前面已经定义过了 ////////////////////////////////键盘事件的数据结构定义///////////////////////////////// typedef struct{ ulong status;//按键的值 ulong click;//是否有按键按下,1 表示有,0 表示没有 }KEY_EVENT static KEY_EVENT cur_data,buf[BUFSIZE];//BUFSIZE 为宏定义,用于定义环形缓冲的大小 static void new_data(void) { if((KEY_CS & 0xff)!=0xff) //从 KEY_CS 地址读入数据,若有一个为 0 则表示有一个按键被按下了(此 处硬件电路为低电平有效) { switch(KEY_CS & 0xff){ case ~KEY0 & 0xff: cur_data.status=1;///////1 被按下 break; case ~KEY1 & 0xff: cur_data.status=2;//2 被按下 break; /////////其他一样添加,懂吗?? } cur_data.click=1; } else if(KEY_CS & 0xff==0xff){ cur_data.click=0; cur_data.status=0; } if(head!=tail){////////循环队列缓冲区的应用在此开始了^_^ int last=head--; if(last<0)////////若已经到了对首之前,则跳到队尾,以实现循环队列 last=BUFSIZE-1; }
//////按键信息存入循环队列缓冲区中 buf[head]=cur_data; if(++head==BUFSIZE) head=0; if(head==tail && tail++=BUFSIZE) tail=0; if(fasync) kill_fasync(&fasyc,SIGIO,POLL_IN); wake_up_interruptible(&queue); }
接下来我们介绍其他几个文件接口函数的实现 四.先介绍关闭函数 keypad_release(),为什么先介绍它呢?道理很简单,应该它比较简单,先让大家做下 热身运动,在介绍完这个之后,继续会介绍一个比较复杂的函数,看你吃得消没有哦 关闭操作主要实现的是:关闭设备异步通知,设备计数器减 1,删除定时器信号中断 static int Keypad_release(struct inode *inode,struct) { Keypad_fasync(-1,filp,0); module_put(THIS_MODULE); del_timer(&timer); return 0; } 五.设备读取操作接口函数实现 Keypad_read() 主要作用是从缓冲区读取键值,通过调用 get_data()实现,通过 copy_to_user()函数将键值复制到用户 的数据区中 static ssize_t Keypad_read(struct file *filp,char *buf,ssize_t count,loff_t *l) { DECLEARE_WAITQUEUE(wait,current);//声明等待队列,将当前进程加入到等待队列中 KEY_EVENT t; ulong out_buf[2]; if(head==tail)//当前循环队列中没有数据可以读取 { if(filp->f_flags & O_NONBLOCK)//假如用户采用的是非堵塞方式读取 return _EAGAIN; add_wait_queue(&queue,&wait);//将当前进程加入等待队列 current->state=TASK_INTERRUPTIBLE;//设置当前进程的状态 while((head==tail)&&!signal_pending(current))//假若还没有数据到循环队列并且当前进程没有受到 信号 { shedule();//进程调度 current->state=TASK_INTERRUPTIBLE; }
current->state=TASK_RUNNING; remove_wait_queue(&queue,&wait); if(head==tail) return count; t=get_data();//调用 get_data()函数,得到缓冲区中的数据,下面将给予详细的 介绍 out_buf[0]=t.status; out_buf[1]=t.click; copy_to_user(buf,&out_buf,sizeof(out_buf));//将得到的键值拷贝到用户数据区 return count; } } 很自然我们就应该要介绍 get_data()函数的实现了,该函数的功能就是从我们定义的循环队列缓冲区中读 出我们要的键值,所以其实很简单的如果理解循环队列的原理,在此不多加解释,大家应该具备一般的数 据结构相关的知识吧 static KEY_EVENT get_data(void) { int last=tail if(++tail==BUFSIZE) tail=0; return buf[last]; } 上面如果你看得懂得话,那么可以进入下面的学习了,主要介绍的是内核定时器的使用,利用等待队列实 现阻塞型 I/O,poll 系统调用,异步通知方式,介绍完之后,我将给出一个应用实例,对于有使用过文件 操作系统调用的来说,对我们所写的键盘驱动来说,他们基本上是一样的。废话少说,我们马上开始我们 精彩的驱动开发! 六.内核定时器的使用 在该驱动中,我们假设对键盘的获取是以 0.2s 为周期执行。源代码如下 static struct timer_list timer;///////我们定义的定时器,也许你会问 timer_list 是什么来的,其实 一看名称就应该就知道了,而为什么要用到 list 那么多定时器呢?其实在 Linux 中还有很多相同的定义, 比如说信号,我们定义的也是信号集,你可以定义该 list 是一个元素的,也可以是多个的。所以对于 timer_list 就可以这样描述:在未来某一个特定时刻执行某一系列特定任务的功能。下面我们还会给出内 核中 timer_list 的具体描述,^_^好像我的话又说多了 static int Keypad_starttimer(void) { init_timer(&timer);//初始化定时器结构 timer.function=Keypad_timer;//超时服务程序 timer.expires=jiffies+20;//当前时刻加 0.2s add_timer(&timer); return 0; } ///超时服务程序 static void Keypad_timer(unsigned long data) {
read_xy(); } /////////接下来说下 timer-list 这个数据结构,如果你不感兴趣的话可以跳过,该结构在 include\Linux\timer.h 中定义 struct timer_list { struct list_head entry; unsigned long expries; spinlock_t lock; unsigned long magic; void (*function)(unsigned long); unsigner long data; struct tvec_t_base_s *base; } 七.利用等待队列实现阻塞型 I\O 在用户程序执行读操作的时候有可能尚且没有数据可以读取,为此需要让 read 操作等待,直到有数据可以 读取,这就是阻塞型 i\o,阻塞型 io 可以通过使用进程休眠方法实现。在无数据可以读取的时候,采用等 待队列让进程休眠,直到有数据到达的时候才唤醒进程完成数据的读操作。 在本驱动中的 read,若循环队列缓冲区中没有数据,则进程进入休眠态,定时器函数每隔 0.2s 读取键值 一次,将按键状态放入缓冲并且适时唤醒进程读取数据。 等待队列的使用流程如下: 1.声明一个等待队列 2.把当前进程加入到等待队列中 3.把进程的状态设置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE; 4.调用 schedule,以让出 cpu 5.检测所需要的资源是否可用,若是,把当前进程从等待队列中删除,否则转 3 循环 接下来我们在对 read 中有关等待队列阻塞实现做具体的解释 static ssize_t Keypad_read(struct file *filp,char *buf,ssize_t count,loff_t *l) { DECLEARE_WAITQUEUE(wait,current);//声明等待队列,将当前进程加入到等待队列中 KEY_EVENT t; ulong out_buf[2]; if(head==tail)//当前循环队列中没有数据可以读取 { if(filp->f_flags & O_NONBLOCK)//假如用户采用的是非堵塞方式读取 return _EAGAIN; add_wait_queue(&queue,&wait);//将当前进程加入等待队列 current->state=TASK_INTERRUPTIBLE;//设置当前进程的状态 while((head==tail)&&!signal_pending(current))//假若还没有数据到循环队列并且当前进程没有受到 信号(该类信号具体来说是未决的休眠) { shedule();//进程调度 current->state=TASK_INTERRUPTIBLE;
} current->state=TASK_RUNNING;//该进程恢复执行 remove_wait_queue(&queue,&wait);//移出等待队列 if(head==tail) return count; t=get_data();//调用 get_data()函数,得到缓冲区中的数据,下面将给予详细的 介绍 out_buf[0]=t.status; out_buf[1]=t.click; copy_to_user(buf,&out_buf,sizeof(out_buf));//将得到的键值拷贝到用户数据区 return count; } } 写得有些累了,等下再继续了,原来写这些东西真的是不容易得,平时看到网络上人家写了很多东西,不 知道原来是那么累的,但是我还是会继续的 八.poll 系统调用操作接口函数 当程序需要进行对多个文件读写时,如果某个文件没有准备好,则系统就会处于读写阻塞的状态,这影响 了其他文件的读写,为了避免读写阻塞,一般可以在应用程序中使用 poll 或者 select 函数。当 poll 函数 返回时,会给出一个文件是否可读写的标志,应用程序根据不同的标志读写相应的文件,实现非阻塞的读 写,poll()函数通过 poll 系统调用,调用对应设备驱动的 poll()接口函数,poll 返回不同的标志, 告诉主进程文件是否可以读写,这些返回标志存放在 include\asm\poll.h 中 标志 含义 POLLIN 如果设备无阻塞的读,就返回该值 POLLRDNORM 通常的数据已经准备好,可以读了,就返回 该值。通常的做法是会返回(POLLLIN|POLLRDNORA) POLLRDBAND 如果可以从设备读出带外数据,就返回该值,它只可在 Linux 内核的某些网络代码中使用,通常不用在设 备驱动程序中 POLLPRI 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致 select 报告文件发生异常, 以为 select 八带外数据当作异常处理 POLLHUP 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照 select 的功能描述,调用 select 的进程被 告知进程时可读的。 POLLERR 如果设备发生错误,就返回该值。 POLLOUT 如果设备可以无阻塞地些,就返回该值 POLLWRNORM 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM) POLLWRBAND
于 POLLRDBAND 类似 在本章地驱动程序中,Keypad_poll()函数在缓冲区有新数据时(当 head!=tail),返回一个 POLLIN|POLLRDNORM,告诉主进程有新的 九.在设备驱动中实现异步通知 虽然大多数时候阻塞型和非阻塞型操作的组合及 poll 方法可以有效查询设备是否可以读写, 但是如果驱动 程序能避免主动的查询,改主动为被动的信号通知触发,则可以提高程序的效率,这也就是异步通知的目 的。异步通知向进程发送 SIGIO 信号,通知访问设备的进程,表示该设备已经准备好 IO 读写了。 之后就是如何实现异步通知的问题了,要启动异步通知,必须执行两个步骤:首先,须要制定某个作为文 件的“属主”。文件属主的进程 ID 保存在 filp->f_owner 中,这可以通过 fcntl()系统调用执行 F_SETOWN 命令设置。此外,用户程序还必须曙色之设备的 FASYNC 标志,以真正启动异步通知机制。这里的 FASYNC 标志也使用 fcntl()设置。 在完成这两个步骤之后,当新数据到达时就会产生一个 SIGNO 信号,此信号发送到存放在 filp->owner 中 的进程。 从驱动的角度看, 则主要时通过调用两个内核提供的函数来实现就是了。 他们分别是: fasync_helper() int 和 void kill_fasync();这两个函数定义在:include\Linux\fs\fcntl.h 要实现异步,驱动中只要如下编写即可 static struct fasync_struct *fasync;//首先是定义一个结构体 static int Keypad_release(struct inode *inode,struct file *filp) { Keypad_fasync(-1,filp,0);//这是一个异步通知 。。。。。。。 } static int Keypad_fasync(int fd,struct file *filp,int on) { int retval; retval=fasync_helper(fd,filp,on,&fasync); if(retval<0) return retval; return 0;} 到此为止,键盘驱动已经介绍完了,接下来就介绍下一个利用使用驱动的应用实例了。 以下程序的主体是一个条件循环,每次循环执行一次,就读取一次键值。 1。打开 Keypad 设备 #define DEV_NAME "/dev/Keypad" int fb=0; fb=open(DEV_NAME,O_RNONLY); if(!fb){ printf("Error:cannot open Keypad device.\n"); exit(1); } printf("The Keypad device was opened successfully.\n"); } 2.读取键值
unsigned long keydata[2]; int input=1; while(input!=0) { if(read(fd,(char*)keydata,sizeof(keydata))==-1){ printf("Error reading the keypad data"); close(fb); exit(2); } if(keydata[0]){ switch(keydata[1]){ case 1:printf("KEYPUSED 1");//1 键被按下 input=0;////下此循环退出 break; 。。。。。。。。。。。。。。。。。。 } } } 3。关闭 Keypad 设备 close(fb); printf("Good bye Keypad"); 键盘驱动到此介绍完毕!