字符设备驱动之按键处理二(中断处理的按键驱动程序)

仍然是处理按键驱动问题,前面《字符设备驱动之按键处理一(查询方式的按键驱动程序)》已经通过查询方式完成了简单功能的探究,但这种方式存在一个比较严重的问题:占用太多CPU资源(97%或更高)。在基于操作系统的程序中,这种做法显然是不可取的。本文探究以中断处理的方式实现按键驱动程序。

1. 首先依然是搭建框架

a. file_operations


相对于之前所写的LED和查询按键驱动程序,多了一项:.release,顾名思义,它表示释放。更多内容后续跟进...

b. read、open等函数




c. initexit函数(完成设备节点创建)


2. 硬件操作

a. 配置管脚

之前通过查询获取按键状态的分析中,有一个重要步骤是配置管脚,是在驱动的open函数里完成的, 对于中断处理,request_irq函数会完成管脚配置,在eint_open函数中调用request_irq完成中断处理程序的注册,添加代码如下:

request_irq(IRQ_EINT0, eint_irq, IRQ_TYPE_EDGE_BOTH, "S1", 1);
request_irq(IRQ_EINT1, eint_irq, IRQ_TYPE_EDGE_BOTH, "S2", 1);
request_irq(IRQ_EINT2, eint_irq, IRQ_TYPE_EDGE_BOTH, "S3", 1);
request_irq(IRQ_EINT4, eint_irq, IRQ_TYPE_EDGE_BOTH, "S4", 1);
ps 4个按键对应4个中断,写四句;IRQ_TYPE_EDGE_BOTHinclude/linux/irq.h中定义;IRQ_EINT0arch/arm/mach-s3c2410/include/mach/irqs.h中定义。request_irq定义如下:

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
第一个参数是中断号,第二个参数是中断处理函数,第三个参数是标志参数,可以设置很多值,本文设置中断触发方式(将中断触发设置为IRQ_TYPE_EDGE_BOTH,即上升沿和下降沿都触发),第四个参数是设备名称,第五个参数是设备id(譬如对于共享中断,可以区分中断源,此处都设为1)。第一个参数和第五个参数dev传递给中断处理函数,分别作为它的第一、第二个形参,后文会有详述。

同样,要在eint_release函数里释放注册的函数,添加代码如下:

free_irq(IRQ_EINT0, 1);

ps:同样,本文涉及4个按键,对应四个中断,所以有4句。这里为什么有dev_id的传入呢?对于某些共享中断,它可能有多个中断处理程序,若不传递dev_id或传递有误,就无法弄清注销哪一个中断处理程序。这个值不一定要为1,但request_irq和free_irq得保持一致。

然后是中断处理函数eint_irq,仿照kernel源码中相关文件,初步完成简单的打印工作,如下:

static irqreturn_t eint_irq(int irq, void *dev_id)
{
printk(“hello, world! irq = %d\n”,irq);
	return IRQ_HANDLED;
}

3. 测试

将驱动程序编译烧写到根文件系统中, 执行“cat /proc/devices”命令可以看到设备节点信息如下:


同样,执行“cat /proc/interrupts”可以看到中断信息如下:


没有发现注册的中断信息,然后执行“exec5</dev/eints”打开中断服务,可以在终端上看到信息如下:


执行“ps”指令读到可以知道当前执行进程的进程号为903,如下:


然后执行“ls /proc/903/fd:


执行“exec 5<&-”指令关闭中断服务:



再次打开中断,测试按键能否触发中断,如下:


上图“Hello, eint!...”为点击按键后打印的信息,参照上文eint_irq可以得知按键按下和释放触发了中断。

这是比较简单的中断驱动测试,下文接着丰富中断处理函数功能:能够即时并高效地将按键按下状态在终端显示出来。

常规思路,至少有这么两种方案:

a. 在eint_irq程序使用一个switch程序段,当中断发生时,通过判断gpio值来打印不同的信息。4个IO口,每个IO口两种可能值,至少需要8条case语句完成此功能,太过麻烦

b. 另一种方法,稍微复杂,如下:
定义一个结构体并创建四个该结构体变量,如下:


如何运用它们,首先把它们通过eint_open函数里的request_irq传入中断处理函数,最终eint_open函数如下:


ps:正如前文所述,对于非共享中断,最后一个参数可以为任意值,但得与free_irq里保持一致。

同样,要在eint_release函数里处理一下,最终eint_release函数如下:


中断处理函数eint_irq作如下处理:

struct pin_desc * pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
pinval = s3c2410_gpio_getpin(pindesc->pin);
if(pinval)
{
	key_val = 0x80 | pindesc->key_val;
}
else
{
	key_val = pindesc->key_val;
}
当然,在此之前的创建一个用以返回给应用程序的全局变量key_val,如下:


然后需要在read函数里把这个全局变量返回给用户空间,在read函数里添加一行代码如下:

copy_to_user(buf, &key_vals,1);

然而,若驱动程序至此结束,依然没能解决之前通过查询获取按键状态的占用大量CPU资源的问题。所以还得继续处理,休眠处理!

在中断处理函数eint_irq里添加如下代码:

wait_event_interruptible(button_waitq,ev_press);	//按键没有按下(没有触发中断)时让应用程序休眠
ev_press = 0;
最终eint_read函数如下:


为什么要这么处理呢?首先看一下宏wait_event_interruptible的定义,如下:


可知ev_press为0时,wait_event_interruptible将本进程(顾名思义,可中断进程)设置为挂起状态。后文用于测试的应用程序里的while(1)循环一直在调用read库函数,若没有这样的设置,copy_to_user一直会向用户空间返回数据,当然会一直占用CPU,继而没能从根本上解决占用过多CPU的问题。这样设置后,当按键触动,也即中断处理程序被eint_irq被调用,ev_press置1,wait_event_interruptible就不会将当前程序挂起,后续copy_to_user就会被调用向用户空间传入数据,之后ev_press再被清零。

ps:更多更详细过程有待分析补充...

得在此之前定义两行代码,如下:


ps:“staticDECLARE_WAIT_QUEUE_HEAD(button_waitq);”定义button_waitq,用它来挂载休眠进程。

有了休眠,当然要唤醒,在中断服务函数里唤醒,加上两句代码:

ev_press = 1;
wake_up_interruptible(&eint_irq); //唤醒休眠中的进程
最终中断处理函数eint_irq如下:


接下来编写测试程序:


将编译的驱动模块和测试程序烧写到根文件系统中,执行应用程序结果如下:


ps:四个按键各按一次得到上述结果。

让程序在后台执行,执行“top”指令查看CPU使用情况!显然已大为改观,如下:



总结

本文在查询方式获取按键状态的基础上进行探究,首先引用了外部中断机制,将原先放在read的读取按键状态(通过读取相应寄存器的值)的程序给搁到外部中断服务程序里,而read只是将与按键状态相关的全局变量key_val给送到用户空间。

然而,虽然显得相较之前高端了一点点,但仍然没有解决实际问题,也即是“按键状态查询占用了太多CPU资源”。

此时,需要引入两个重要的函数:wait_event_interruptible和wake_up_interruptible,前者可以设置当前进程处于睡眠状态,后者相反,可以唤醒睡眠状态的进程。如何安排呢?因为在应用程序里,read函数会被循环调用,也即驱动程序里的read函数会被循环调用,所以在read函数里添加wait_event_interruptible函数,当没有按键被按下时,就让此进程处于唤醒状态,这样的话,此进程就几乎不会占用CPU资源,因为它处于唤醒(挂起)状态。

但如何唤醒进程呢?也即有按键按下时如何能够让应用程序知道呢?中断这个时候就体现出了极大的意义了。把wake_up_interruptible放在中断服务程序中,当按键按下时,中断服务程序被触发,然后可以将读取的按键状态值送到用户空间。至于按键读取,可以放在中断服务程序里完成,当然也可以放在驱动的read函数里完成,这个关系不大。

对于中断,由本文可以知道,多个不同的中断可以对应一个中断服务函数(这没什么大不了的,但与单片机有些不同)。对于处理单任务的单片机,其实也可以这么处理:把多个按键连接到外部中断对应的IO口,按键按下时,触发外部中断,然后通过分析IO口状态来判断是哪个按键被按下了,然后调用相应的处理函数…

所以,由本文可见,在按键状态获取中,中断最主要的作用是唤醒进程

本文这种思想同样可以应用于单片机程序编写中,可以使逻辑简洁,扩展性更强!

中断的作用搞清楚了,也能够依葫芦画瓢自行处理休眠和唤醒的这些操作,只是还没太理解这些操作是怎么一回事儿:

为什么通过“static DECLARE_WAIT_QUEUE_HEAD(button_waitq);”和“wait_event_interruptible(button_waitq, ev_press);”两行代码就能够让本进程处于休眠状态?

为什么通过“wake_up_interruptible(&button_waitq);”这么一行代码就能够唤醒休眠的进程?button_waitq到底是干啥的?

再摘抄一段《Linux内核源代码情景分析》上的文字,如下:

状态TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE均表示进程处于睡眠状态。但是,TASK_UNINTERRUPTIBLE表示进程处于“深度睡眠”而不受“信号”(signal,也称“软中断”)的打扰,而TASK_INTERRUPTIBLE则可以因“信号”的到来而被唤醒。内核提供了不同的函数,让一个进程进入不同深度的睡眠或将进程从睡眠中唤醒。具体地说,函数sleep_on()和wake_up()用于深度睡眠,而interruptible_sleep_on()和wake_up_interruptible()则用于浅度睡眠。深度睡眠一般只用于临界区和关键性的部位,而“可中断”的睡眠那就是通用的了。特别,当进程在“阻塞性”(blocking)的系统调用中等待某一事件发生时,应该进入“可中断”睡眠而不应深度睡眠。例如,当进程等待操作人员按某个键的时候,就不应该进入深度睡眠,否则就不能对别的事件作出反应,别的进程就不能通过发一个信号来“杀”掉这个进程了。还应该注意,这里的INTERRUPTIBLE或UNINTERRUTPIBLE跟“中断”毫无关系,而只是说睡眠能否因其他事件而中断,即唤醒。不过,所谓其他事件主要是“信号”,而信号的概念实际上与中断的概念是相同的,所以这里所谓INTERRUPTIBLE也是指这种“软中断”而言。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值