第十章Linux中使用延时函数、定时器和中断的讲解
本章节将重点讲解linux驱动编程中的延时、定时器以及中断的使用。内核定时器的实现原理是基于中断来实现的,而中断服务处理程序是运行在进程的上下文之中,因此要求中断响应处理的时间要尽可能的短,因此linux使用了中断处理分离机制,将中断处理过程分为上下两部分分别处理,提高的中断响应的效率。
10.1延时函数讲解
/*函数功能:微秒级延时函数 *函数参数: *@n:为需要延时n微秒,最大不超过MAX_UDELAY_MS微秒 *函数返回值:无 */ #define MAX_UDELAY_MS 2 #define udelay(n) \ (__builtin_constant_p(n) ? \ ((n) > (MAX_UDELAY_MS * 1000) ? __bad_udelay() : \ __const_udelay((n) * ((2199023U*HZ)>>11))) : \ __udelay(n)) #endif /* defined(_ARM_DELAY_H) */
/*函数功能:毫秒级延时函数 *函数参数: *@n:为需要延时n毫秒,最大不超过MAX_UDELAY_MS毫秒 *函数返回值:无 */ #ifndef MAX_UDELAY_MS #define MAX_UDELAY_MS 5 #endif #ifndef mdelay #define mdelay(n) (\ (__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \ ({unsigned long __ms=(n); while (__ms--) udelay(1000);})) #endif
/*函数功能:纳秒级延时函数 *函数参数: *@n:为需要延时n纳秒,最大不超过20000毫秒 *函数返回值:无 */ #define ndelay(n)\ ({ \ if (__builtin_constant_p(n)) { \ if ((n) / 20000 >= 1) \ __bad_ndelay(); \ else \ __const_udelay((n) * 5ul); \ } else { \ __ndelay(n); \ } \ }) #endif /* __ASM_GENERIC_DELAY_H */
从上面的定义可以知道,对于ndelay()和mdelay()函数而言,他们是由udelay()衍生而来的,同时udelay()一般适用于比较小的延时,当传入的需要延时数n>2000时,系统会报出错误。同理mdelay()和ndelay()传入的延时数n分别不应大于5000和20000。
由于udelay()、mdelay()和ndelay()它们实现的原理是忙等待,即类似for循环来进行等待延时,因此如果需要延时长时间的话会占用CPU资源比较严重,因此如果延时长时间例如是需要延时毫秒级别及以上的话,可以选用内核提供的睡眠延时函数msleep()和ssleep(),其中msleep()和ssleep()分别为毫秒级和秒级的睡眠延时函数。
对于不需要精确的延时的情况,可以选用上述的延时函数进行延时,但是对于需要精确延时的情况时,则需要选用定时器进行延时。
10.2内核时钟
(1)内核时钟
操作系统的内核一般需要一个系统时钟才能够正常工作,同时这个系统时钟是由硬件提供的,操作系统则使用系统时钟进行计时,例如sleep()和时间片轮转都需要使用系统时钟来计时的。对于操作系统内核使用的时钟则称为内核时钟,亦称滴答时钟。如下为两个系统内核时钟例子:
STM32F407 + uC/OS-III :操作系统内核时钟频率:OS_TICKS_PER_SEC = 200 S5P6818 + linux:操作系统内核时钟频率:HZ = 1000
对于linux系统而言,它的内核时钟是由处理器的定时器来提供的。
(2) 内核时钟的频率(HZ)
在HZ是linux内核代码中的一个全局变量,它表示linux内核时钟的频率大小,同时HZ是一个常数,如果内核配置需要修改HZ的值时,需要重新配置和编译linux内核才能够生效。
如下为HZ在内核中的定义:
# define HZ CONFIG_HZ /* Internal kernel timer frequency */ #define CONFIG_HZ 1000
(3) jiffies变量
jiffies同样也是linux核中的一个全局变量,它的值大小表示linux内核从启动到现在经过了多少次内核时钟周期。即1秒中内,jiffies增加了HZ次。
如下为jiffies和HZ的关系:
jiffies/HZ=linux系统启动到现在用了的秒数。
如下为jiffies的定义,其中可以看到它其实是一个volatile修饰的变量,是一个实时从寄存器获取到的值:
jiffies = jiffies_64; u64 __jiffy_data jiffies_64; unsigned long volatile __jiffy_data jiffies;
10.3动态定时器使用
(1) 动态定时器结构体定义
如下为动态定时器在内核中的结构体定义struct timer_list:
struct timer_list { struct list_head entry; unsigned long expires; //定时器的超时时间 struct tvec_base *base; void (*function)(unsigned long); //超时处理函数 unsigned long data; //向超时处理函数传递的参数 int slack; };
结构体成员含义说明如下:
(a) 双向链表entry:主要用于将多个定时器连接为一条双向循环的队列
(b) expires:设置定时器的超时时间,当定时器的exipires的值小于或者等于jiffies时,就可以认为该定时器已经超时或者到期了。
(c) function:为一个函数指针,用于当定时器超时或者到期时执行该函数。
(d) data:为像超时处理函数function传递的参数。
(1) 使用动态定时器的步骤
如下为使用动态定时器的步骤:
(a) 定义一个动态定时器及初始化相关成员
例如:
static struct timer_list gec6818_timer; gec6818_timer.function = gec6818_timer_fun; gec6818_timer.expires = jiffies + 100; //当前时间开始,100个jiffy后,会产生超时(100ms) gec6818_timer.data = 10;
(b) 初始化动态定时器
初始化动态定时器需要使用如下函数:
/*函数功能:初始化动态定时器 *函数参数: *@struct timer_list *timer:为定时器指针,传入需要进行初始化的定时器 *函数返回值:无 */ void init_timer(struct timer_list *timer)
(c) 设置动态定时器的超时处理函数
如下为设置超时处理函数gec6818_timer_fun()事例:
//设置动态定时器的超时时间和超时处理函数 void gec6818_timer_fun(unsigned long data) //data=10 { printk("jiffies=%lu\n",jiffies); printk("data = %d\n",data); }
(d) 将定时器加入内核
在定义和初始化动态定时器完成后,需要使用如下函数将动态定时器加入内核及启动动态定时器,并且该动态定时器仅触发一次超时:
/*函数功能:将定时器加入内核 *函数参数: *@struct timer_list *timer:为定时器指针,传入需要进行初始化的定时器 *函数返回值:无 */ void add_timer(struct timer_list *timer)
(e) 修改定时器超时时间
如果需要设置定时器的超时时间需要使用如下函数:
/*函数功能:修改超时时间数值 *函数参数: *@struct timer_list *timer:为定时器指针,传入需要进行初始化的定时器 *@unsigned long expires:调整后的超时时间数值 *函数返回值: *当修改了非活动定时器返回0,当修改了活动定时器返回1 */ int mod_timer(struct timer_list *timer, unsigned long expires)
(f) 删除动态定时器
在驱动退出或者使用完定时器时需要使用删除函数来释放动态定时器的相应资源,如下为动态定时器删除函数的定义:
/*函数功能:将定时器加入内核 *函数参数: *@struct timer_list *timer:为定时器指针,传入需要进行初始化的定时器 *函数返回值: *当删除了非活动定时器返回0,当删除了活动定时器返回1 */ int del_timer(struct timer_list *timer)
如下给出完整的使用动态定时器的驱动以供参考学习:
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/time.h> //定义一个动态定时器 static struct timer_list gec6818_timer; void gec6818_timer_handler(unsigned long data) { printk("gec6818_timer_handler init\n"); //重新修改超时时间,进行周期性的超时 mod_timer(&gec6818_timer,jiffies + 500); } //入口函数 static int __init gec6818_led_init(void) { //初始化动态定时器 init_timer(&gec6818_timer); //配置动态定时器 gec6818_timer.function = gec6818_timer_handler;//处理函数 gec6818_timer.expires = jiffies + HZ; //当前时间开始,HZ个jiffies后,会产生超时 gec6818_timer.data = 10;//传递参数 //动态定时器加到内核 add_timer(&gec6818_timer); printk("gec6818 led init\n"); return 0; } //出口函数 static void __exit gec6818_led_exit(void) { //删除动态定时器 del_timer(&gec6818_timer); printk("gec6818 led exit\n"); } module_init(gec6818_led_init); module_exit(gec6818_led_exit) //模块描述 MODULE_AUTHOR("whl@163.com"); //作者信息 MODULE_DESCRIPTION("gec6818 led driver"); //模块功能说明 MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
10.3中断的使用
在linux系统中中断是一种比较稀缺重要的处理器资源,在系统中通过中断能够实现高效的响应外部事件,提升系统的响应效率,增加系统吞吐量。同时由于中断服务程序的执行并不在进程上下文中完成,因此就务必要求中断服务程序的处理时间要尽可能的短,因此linux在中断处理中引入了上半部和下半部分离的机制,将耗时不同的中断服务分开处理,从而提升中断响应效率。
10.3.1linux中断的概念及处理流程
在linux系统中在CPU运行程序时,如果突然触发了某件急需处理的事件,此时CPU必须暂停当前程序的运行,然后马上去处理此突发事件,待处理完成后再次返回到原来程序被中断的位置继续执行,这个过程即是中断的触发和中断处理的大致过程。
通常根据中断的来源可以分为内部中断和外部中断,内部中断源是指来自CPU内部的例如软件中断指令、溢出和除法错误等;而外部中断源则来自外部的中断请求,由外设提出请求。
根据中断是否可屏蔽又可以分为可屏蔽中断和不可屏蔽中断,可屏蔽中断可以通过设置中断控制寄存器等方法对指定的中断进行屏蔽,即屏蔽后即使有该指定的中断请求也不会响应。而对于不可屏蔽中断,则只要中断请求到来,就必须对该中断请求进行响应处理。
10.3.2linux中断处理架构
在理论上设备的中断将会打断内核进程的正常运行和调度的,考虑到系统需要更高的吞吐量的要求,因此就要求中断服务程序尽可能的短小高效。但是这个要求往往在实际的开发中是很难达到的,在实际系统中,往往中断到来时,需要响应完成的任务一般是比较耗时的。
如下图所示为linux内核的中断处理机制,为了在中断执行时间尽可能短和中断处理工作量大之间寻求一个平衡点,linux则将中断服务程序分为两个部分:上半部和下半部。
中断服务程序上半部主要用于完成尽可能短且比较紧急的任务,例如它往往仅仅简单地读取寄存器的中断状态,和清楚中断标志后立即进行“记录中断”的工作。“记录中断”即是将下半部处理程序挂到设备的下半部执行队列中执行,因此上半部执行的效率就大大提升了,以此来实现服务响应更多的中断请求。
由于上述的中断分离机制的作用,中断处理响应工作的重心现在落在了下半部处理程序中了,需要它来完成中断事件的大部分的任务,同时下半部处理程序还是可以被新来的中断打断的,而上半部处理程序是被设置为不可中断的,这也是这两部处理程序的最大的不同。
虽然中断处理分离机制能够提升系统的响应效率,但是如果理解为linux驱动中的中断处理一定是分为两个部分是不恰当的。例如如果当需要处理的中断响应工作量本身比较少的话,这部分则直接在上半部份完成即可。
10.3.3 linux中断编程
(1)申请和释放中断
在linux驱动中需要使用中断的时候,需要先对中断资源进行申请才能够进行使用,以及在使用完后需要释放中断相关资源,如下为申请和释放中断资源的函数定义:
/*函数功能:申请中断 *函数参数: *@unsigned int irq:中断号,每个中断源有一个唯一的中断号 *@irq_handler_t handler:中断服务程序,中断响应的时候,执行的函数 *@ unsigned long flags:中断的标志,外部中断:触发方式 *@const char *name:中断的名称,自定义 *@ void *dev:向中断服务程序发送的参数 *函数返回值: *返回0表示申请成功,返回-EINVAL表示中断号无效或者中断处理函数为NULL,返回-EBUSY表示该中断号已被占用 */ int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
handler参数是向系统记录的中断处理函数(上半部),是一个回调函数指针,当系统响应中断时会调用这个函数,dev参数则作为参数传递给handler中。
irqflags参数是用来设置这个中断的属性的,即可以作用于设定中断的触发及响应方式。咋触发方式具有IRQF_TRIGGER_RISING(上升沿触发)、IRQF_TRIGGER_FALLING(下降沿触发)、IRQF_TRIGGER_HIGH(高电平触发)、IRQF_TRIGGER_LOW(低电平触发)等参数。在处理响应方式的设置方面,如果设置了IRQF_SHARED,则将该中断设置为被多个设备共享。
b)中断服务函数定义
/*函数功能:中断服务程序 *函数参数: *@int irq:中断号,每个中断源有一个唯一的中断号 *@ void *dev:申请中断的时候,传递的参数 *函数返回值: *返回enum类型的值: enum irqreturn { IRQ_NONE = (0 << 0), //没有该中断号 IRQ_HANDLED = (1 << 0), //中断处理结束,正常返回 IRQ_WAKE_THREAD = (1 << 1),//表示处理程序请求线程唤醒,唤醒下半部处理程序 };
c)释放中断
/*函数功能:释放中断 *函数参数: *@int irq:中断号,每个中断源有一个唯一的中断号 *@ void *dev:申请中断的时候,传递的参数 *函数返回值:无 void free_irq(unsigned int irq, void *dev);
(2)屏蔽中断
如下三个与屏蔽中断和使能中断的函数定义:
/*函数功能:屏蔽中断 *函数参数: *@int irq:中断号,每个中断源有一个唯一的中断号 *函数返回值:无 void disable_irq(int irq); /*函数功能:屏蔽中断 *函数参数: *@int irq:中断号,每个中断源有一个唯一的中断号 *函数返回值:无 void disable_irq_nosync(int irq); /*函数功能:使能中断 *函数参数: *@int irq:中断号,每个中断源有一个唯一的中断号 *函数返回值:无 void enable_irq(int irq);
disable_irq()和disable_irq_nosync()都用于对某个中断源进行屏蔽,它们的区别在于disable_irq_nosync()会立即返回,而disable_irq()需要等待当前的中断处理完成后才会返回。正因为disable_irq()会阻塞等待当前中断处理完成,所以在n号中断的上半部不能调用disable_irq(n),否则会引起死锁造成系统瘫痪,这种情况只能使用disable_irq_nosync(n)来对n号中断进行屏蔽。
10.3.4中断处理下半部机制
linux实现中断处理下半部的机制主要由如下方式:tasklet、工作队列和软件中断。
1、tasklet方式
tasklet方式的使用方法是比较简便的,在使用时只须提前定义好指定的tasklet和处理函数,同时将两者绑定起来即可使用,如下为定义tasklet及其处理函数的例子:
/*定义一个处理函数*/ void my_tasklet_func(unsigned long); /*定义一个 tasklet 结 构 m y_tasklet, 与 m y_tasklet_func(data)函数相关联 */ DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
如上述代码中使用函数DECLARE_TASKLET()即可将名为my_tasklet的tasklet和处理函数my_tasklet_func进行绑定,其中data为传入处理函数的参数。
在上面的步骤中完成了绑定工作后,仅需要调用如下函数tasklet_schedule()即可让系统在合适的时候调度tasklet,继而进一步调用处理函数my_tasklet_func()。为方便读者学习使用,下面给出常用的tasklet模版:
/*定义 tasklet 和下半部函数,同时进行绑定操作*/ void xxx_do_tasklet(unsigned long); DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0); /*中断处理下半部*/ void xxx_do_tasklet(unsigned long) { ... } /*中断处理上半部*/ irqreturn_t xxx_interrupt(int irq, void *dev_id) { ... tasklet_schedule(&xxx_tasklet); ... } /*设备驱动模块加载入口函数*/ int _ _init xxx_init(void) { ... /*申请中断*/ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... return IRQ_HANDLED; } /*设备驱动模块卸载函数*/ void _ _exit xxx_exit(void) { ... /*释放中断*/ free_irq(xxx_irq, xxx_interrupt); ... }
在上述代码中的驱动入口函数xxx_init()申请中断,同时在驱动模块卸载函数xxx_exit()中进行释放中断资源。在申请中断函数中可以看出,中断处理函数设置为xxx_interrupt(),在这个函数中将使用tasklet_schedule(&xxx_tasklet)调度名为xxx_do_tasklet()的tasklet函数,以此完成中断响应处理。
2、工作队列
工作队列的使用方式和上述tasklet使用方式比较类似,可以通过如下代码实现定义一个工作队列和一个中断下半部处理执行函数,然后再通过INIT_WORK()函数即可将上面的工作队列和中断处理函数进行绑定起来:
/*定义一个工作队列*/ struct work_struct my_wq; /*定义一个中断下半部处理函数*/ void my_wq_func(unsigned long); /*初始化工作队列并将其与处理函数绑定*/ INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL);
跟调度tasklet的tasklet_schedule()类似的,调度工作队列的执行函数时只需调用如下函数即可:
/*调度工作队列执行*/ schedule_work(&my_wq);
为方便读者学习使用工作队列,如下给出工作队列使用的模板:
/*定义工作队列和关联函数*/ struct work_struct xxx_wq; void xxx_do_work(unsigned long); /*中断处理底半部*/ void xxx_do_work(unsigned long) { ... } /*中断处理顶半部*/ irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) { ... schedule_work(&xxx_wq); ... return IRQ_HANDLED; } /*设备驱动模块加载函数*/ int xxx_init(void) { ... /*申请中断*/ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... /*初始化工作队列*/ INIT_WORK(&xxx_wq, (void (*)(void *)) xxx_do_work, NULL); ... } /*设备驱动模块卸载函数*/ void xxx_exit(void) { ... /*释放中断*/ free_irq(xxx_irq, xxx_interrupt); ... }
上述代码的实现流程基本与tasklet方式的实现流程大同小异的,但是不同的是工作队列使用时需要再驱动加载函数中首先增加了初始化工作队列的操作,才能在后续使用工作队列来响应中断。
3、软件中断
软件中断是一种常用的中断下半部处理机制,它一般运行的时机是在中断上半部处理程序返回的时候,由于tasklet是基于软件中断的方式来实现的,所以它也是在软件中断的上下文中执行,软件中断上下文是属于原子上下文的一种,而对于工作队列方式则是运行在进程上下文中,所以软件中断和tasklet处理函数是不能进行睡眠的,而工作队列的处理函数则可以进行睡眠操作。
10.3.5实例
在日常的设备驱动开发中,按键驱动是比较常见的驱动之一,同时按键驱动通常会对外部中断进行响应,来完成一些控制任务。如下为一个按键中断检测驱动,主要实现的功能是检测按键GPIOA28、GPIOB9、GPIOB30、GPIOB31中有那几个按键被按下,并将按键的状态值保存下来,在应用层可以查询得到该状态值。
在按键驱动代码key_drv.c加载函数gec6818_key_init()主要完成实现了对这几个按键GPIO使用 request_irq()函数进行申请中断资源、绑定中断服务函数且设置中断触发方式(本例程为下降沿触发方式IRQF_TRIGGER_FALLING),以及使用init_waitqueue_head()函数完成初始化工作队列的操作,如下为加载函数中断部分的关键代码,其中结构体key_irq_info_tbl保存设置了GPIO需要的中断号和中断的名称信息:
...... static const struct key_irq_info key_irq_info_tbl[4]={ { .num = IRQ_GPIO_A_START+28, .name = "gpioa28", }, { .num = IRQ_GPIO_B_START+9, .name = "gpiob9", }, { .num = IRQ_GPIO_B_START+30, .name = "gpiob30", }, { .num = IRQ_GPIO_B_START+31, .name = "gpiob31", }, }; ... //申请GPIOA28、GPIOB9、GPIOB30、GPIOB31中断 for(i=0; i<4; i++) { rt= request_irq(key_irq_info_tbl[i].num, keys_irq_handler, IRQF_TRIGGER_FALLING, key_irq_info_tbl[i].name, NULL); if(rt < 0) { printk("request_irq fail %s",key_irq_info_tbl[i].name); goto request_irq_fail; } } //初始化等待队列 init_waitqueue_head(&gec6818_key_wq); ......
对于申请中断时绑定的中断处理函数为 keys_irq_handler(int irq, void *dev),主要实现当系统检测到中断时,及时将key_val的状态值更新的功能,在中断处理函数的最后还需要使用wake_up()函数来唤醒正在等待中断处理完成的进程,使其及时返回发生中断处继续执行后续程序。
在本例程可以看出,中断处理程序并没有下半部(亦或是没有严格意义上的tasklet、工作队列或软件中断下半部),它只是将一个等待队列唤醒,而这个等待队列的唤醒也将导致一个阻塞的进程被执行(这个阻塞的进程可看作软中断下半部)。现在我们看到,等待队列可以作为中断服务程序上半部和进程同步的一种良好机制。可是在任何情况下,都不应该在上半部去等待一个正在等待的工作队列,而只能唤醒等待队列。
//按键的中断服务程序 irqreturn_t keys_irq_handler(int irq, void *dev) { if(irq == (IRQ_GPIO_A_START + 28)) key_val |=1<<0; if(irq == (IRQ_GPIO_B_START + 9)) key_val |=1<<1; if(irq == (IRQ_GPIO_B_START + 30)) key_val |=1<<2; if(irq == (IRQ_GPIO_B_START + 31)) key_val |=1<<3; //设置等待条件为真,真就是1 key_press_flag=1; //唤醒等待队列中的进程 wake_up(&gec6818_key_wq); return IRQ_HANDLED; }
如下给出完整的按键驱动程序key_drv.c代码
key_drv.c:
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/device.h> #include <linux/io.h> #include <linux/ioctl.h> #include <cfg_type.h> #include <linux/gpio.h> #include <linux/miscdevice.h> #include <linux/interrupt.h> #include <linux/sched.h> #define GEC6818_KALL_STA _IOR('K', 5, unsigned long) static unsigned int key_val=0; //定义一个等待的条件 static int key_press_flag = 0; //定义一个等待队列 static wait_queue_head_t gec6818_key_wq; struct key_irq_info{ unsigned int num; char name[20]; }; static const struct key_irq_info key_irq_info_tbl[4]={ { .num = IRQ_GPIO_A_START+28, .name = "gpioa28", }, { .num = IRQ_GPIO_B_START+9, .name = "gpiob9", }, { .num = IRQ_GPIO_B_START+30, .name = "gpiob30", }, { .num = IRQ_GPIO_B_START+31, .name = "gpiob31", }, }; irqreturn_t keys_irq_handler(int irq, void *dev) { if(irq == (IRQ_GPIO_A_START + 28)) key_val |=1<<0; if(irq == (IRQ_GPIO_B_START + 9)) key_val |=1<<1; if(irq == (IRQ_GPIO_B_START + 30)) key_val |=1<<2; if(irq == (IRQ_GPIO_B_START + 31)) key_val |=1<<3; //设置等待条件为真,真就是1 key_press_flag=1; //唤醒等待队列中的进程 wake_up(&gec6818_key_wq); return IRQ_HANDLED; } static int gec6818_key_open (struct inode * inode, struct file *file) { printk("gec6818_key_open \n"); return 0; } static int gec6818_key_release (struct inode * inode, struct file *file) { printk("gec6818_key_release \n"); return 0; } static long gec6818_key_ioctl (struct file *filp, unsigned int cmd, unsigned long args) { int rt=0; switch(cmd) { case GEC6818_KALL_STA: { //访问等待队列,判断key_press_flag条件是否真 wait_event_interruptible(gec6818_key_wq,key_press_flag); key_press_flag=0; }break; default: printk("key ENOIOCTLCMD\n"); return -ENOIOCTLCMD; } rt = copy_to_user((void *)args,&key_val,4); key_val =0; if(rt != 0) return -EFAULT; return 0; } static const struct file_operations gec6818_key_fops = { .owner = THIS_MODULE, .open = gec6818_key_open, .release = gec6818_key_release, .unlocked_ioctl = gec6818_key_ioctl, }; static struct miscdevice gec6818_key_miscdev = { .minor = MISC_DYNAMIC_MINOR, //MISC_DYNAMIC_MINOR,动态分配次设备号 .name = "gec6818_keys", //设备名称,/dev/gec6818_keys .fops = &gec6818_key_fops, //文件操作集 }; //入口函数 static int __init gec6818_key_init(void) { int rt=0; int i=0; //混杂设备的注册 rt = misc_register(&gec6818_key_miscdev); if (rt) { printk("misc_register fail\n"); return rt; } //申请GPIOA28、GPIOB9、GPIOB30、GPIOB31中断 for(i=0; i<4; i++) { rt= request_irq(key_irq_info_tbl[i].num, keys_irq_handler, IRQF_TRIGGER_FALLING, key_irq_info_tbl[i].name, NULL); if(rt < 0) { printk("request_irq fail %s",key_irq_info_tbl[i].name); goto request_irq_fail; } } //初始化等待队列 init_waitqueue_head(&gec6818_key_wq); printk("gec6818 key init\n"); return 0; request_irq_fail: for(i=0; i<4; i++) free_irq(key_irq_info_tbl[i].num,NULL); misc_deregister(&gec6818_key_miscdev); return rt; } //出口函数 static void __exit gec6818_key_exit(void) { int i; for(i=0; i<4; i++) free_irq(key_irq_info_tbl[i].num,NULL); misc_deregister(&gec6818_key_miscdev); printk("gec6818 key exit\n"); } //驱动程序的入口:insmod led_drv.ko调用module_init,module_init又会去调用gec6818_key_init。 module_init(gec6818_key_init); //驱动程序的出口:rmsmod led_drv调用module_exit,module_exit又会去调用gec6818_key_exit。 module_exit(gec6818_key_exit) //模块描述 MODULE_AUTHOR("whl@163.com"); //作者信息 MODULE_DESCRIPTION("gec6818 led driver"); //模块功能说明 MODULE_LICENSE("GPL"); //许可证:驱动遵循GPL协议
如下给出应用层测试按键驱动代码key_test.c:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <sys/ioctl.h> #define GEC6818_KALL_STA _IOR('K', 5, unsigned long) int main(int argc, char **argv) { int led_fd=-1; int key_fd=-1; int key_val=0; int rt=0; //打开gec6818_keys设备 key_fd = open("/dev/gec6818_keys",O_RDWR); if(key_fd < 0) { perror("open /dev/gec6818_keys:"); return key_fd; } while(1) { //读取按键的状态 rt = ioctl(key_fd,GEC6818_KALL_STA,&key_val); if(rt == 0) { printf("key val=%02X\n",key_val); } printf("now run to next read key proceess\n"); } close(led_fd); return 0; }
使用key_test.c生成的测试软件,主要实现循环的读取当前按键状态值,当有按键按下的时候,对应key_val对应的位将被置一,如当只有GPIOA28和GPIOB9被按下时,此时key_val对应的值为0b0011(二进制),对应十进制的值为3;对于其他按键组合以此类推会得到不同的key_val状态值,读者也可以在获取得到的状态值后增加项目所需要的特定操作,如控制蜂鸣器响等,来实现不同的任务。
10.4本章总结
本章节主要讲解了linux定时器和延时函数的使用,能够利用定时器和延时函数实现一些延时操作。但系统需要更加高效的完成一些任务时,就需要通过中断来高效完成一些紧急的任务,提高系统CPU的利用率,从而实现更加为复杂的功能,本章节详细的对中断的实现机制、使用中断的函数等进行了讲解,同时在最后给出了实例让读者对linux驱动的中断有更为深入直观的认识,为后续章节的学习做好准备。