[经验] 【记录】ARM-linux开发之按键控制

ywlzh   2016-7-30 10:53 楼主
驱动框架: 采用混杂驱动程序的框架,具体细节在下面这个帖子二楼做过描述 https://bbs.eeworld.com.cn/thread-495558-1-1.html 硬件电路图: QQ图片20160730083821.jpg QQ图片20160730083854.jpg 分析: 对于按键程序的编写,在我用stm32单片机的时候,就有多种控制方式,ARM-linux 下也有多种方式去做,查询,中断,poll机制,信号,用内核定时器........ 查询方式: 这种方式的实现,内核只需要提供read,open接口即可,在open()里面将对应引脚初始化成输入配置,在应用程序中,C语言写的话,while(1){}无限循环,读按键,发现读出来的值有变化,就证明按键按下去了,就打印到终端,用qt,C++写的话,需要自己创建一个线程,在这个线程里不断的执行读,打印,不管是用什么应用程序写,用top命令可以发现自己的应用程序占据cpu 77.7%甚至更多。这种方式显然不适合。 中断方式: 对于中断更多的解释可以查看《宋宝华-精通LINUX设备驱动开发》第四章第2节 QQ图片20160730091509.jpg QQ图片20160730091626.jpg QQ图片20160730091652.jpg 从中提出我需要写的部分,中断配置添加进我写好的混杂驱动程序框架中,在函数外定义一个结构体:
  1. static unsigned char key_value; //保存键值,作为全局变量使用
/* 硬件引脚 定义 */ struct pin_des { int pin; unsigned char key_val; }; static struct pin_des pins_desc[3] = { {MXS_PIN_TO_GPIO(PINID_SSP0_DATA4), 0x01}, {MXS_PIN_TO_GPIO(PINID_SSP0_DATA5), 0x02}, {MXS_PIN_TO_GPIO(PINID_SSP0_DATA6),0x03}, }; 注意发现,电路是5个按键,而我只定义了3个按键,是因为我用查询方式的时候,发现有两个按键不管按没按下,读到的值都是0,是坏了还是怎么的,没有多想,按键用3个也是可以实现的,学习不去计较这些。 在open()函数主要是初始化硬件,配置中断,这个尽量不要在程序入口函数里,而官方的按键驱动偏偏写在了程序入口函数,不知道他们是怎么想的。
  1. static int button_open(struct inode *inode, struct file *filp)
  2. {
  3. unsigned char i ;
  4. int irq_no;
  5. int iRet;
  6. for(i=0;i<3;i++){
  7. gpio_free(pins_desc[i].pin);
  8. iRet = gpio_request(pins_desc[i].pin, DEVICE_NAME);
  9. if(iRet != 0){
  10. printk("request button failed \n");
  11. return -EBUSY;
  12. }
  13. gpio_direction_input(pins_desc[i].pin);
  14. printk("direction input OK \n");
  15. irq_no = gpio_to_irq(pins_desc[i].pin);
  16. set_irq_type(irq_no, IRQF_TRIGGER_FALLING); //下降沿中断
  17. printk("IRQF_TRIGGER_FALLING \n");
  18. //申请中断并设置中断处理函数
  19. iRet = request_irq(irq_no, button_irq, IRQF_DISABLED, "button_exit", &pins_desc[i]);
  20. if (iRet != 0){
  21. printk("request irq failed!! ret: %d irq:%d gpio:%d \n", iRet, irq_no, pins_desc[i].pin);
  22. return -EBUSY;
  23. }
  24. }
  25. return 0;
  26. }
中断函数是需要自己写,名字也是需要自己想,至于中断为什么可以找到你写的中断函数,这个中断的框架linux内核早就做好了,做驱动开发的人只需要拿来用,而且中断也是等着你用就行。
  1. static irqreturn_t button_irq(int irq, void *dev_id)
  2. {
  3. printk("irq test \n");
  4. struct pin_des * pindesc = (struct pin_desc *)dev_id;
  5. unsigned int val;
  6. val = (unsigned char)gpio_get_value(pins_desc->pin);
  7. if(val) key_value = 0x10 |pindesc->key_val;
  8. else key_value = pindesc->key_val;
  9. ev_press = 1; /* 表示中断发生了 */
  10. wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
  11. return IRQ_RETVAL(IRQ_HANDLED);
  12. }
会发现,中断需要唤醒休眠,为什么用休眠?如果是单纯的查询按键,知道CPU的占用率太高了,所以就引进休眠的手段,为CPU考虑一下,什么时候休眠呢?这里你可以在open里面主动休眠,中断唤醒,你也可以使用poll机制,让poll去休眠。 配合休眠的手段,那read()函数也要配合着改进
  1. ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
  2. {
  3. unsigned char i;
  4. unsigned char key_val[5];
  5. /* 如果没有按键动作, 休眠 */
  6. wait_event_interruptible(button_waitq, ev_press);
  7. copy_to_user(buf, &key_value, 1);
  8. ev_press = 0;
  9. return 1;
  10. }
应用程序和查询方式的应用程序一样。 poll机制: 在中断方式里提到的休眠手段,可以用poll去休眠,这样对应用层好,对内核也好,这种方式改进了只要你按键按下,终端持续的打印出键值,不是打印一下,为什么?因为按键按下,这个动作对于内核来说是一段持续的时间,会持续响应,导致应用程序也会持续响应,终端也就持续打印。所以加入poll机制,就是内核响应了,应用层响应,下次响应就需要等一段时间,几百毫秒或者几十个毫秒不等,看你应用层怎么设置了。 poll实现; 驱动程序中poll函数在file_operations结构里面,所以需要再增加一个函数就行
  1. .poll = button_poll,
  1. static unsigned button_poll(struct file *file, poll_table *wait)
  2. {
  3. unsigned int mask = 0;
  4. poll_wait(file, &button_waitq, wait); // 不会立即休眠
  5. if (ev_press)
  6. mask |= POLLIN | POLLRDNORM;
  7. return mask;
  8. }
内核配置完了,那么应用层的配置:
  1. struct pollfd fds[1];
fds[0].fd = fd; fds[0].events = POLLIN; while (1) { ret = poll(fds, 1, 5000); if (ret == 0) { printf("time out\n"); } else { read(fd, &key_val, 1); printf("key_val = 0x%x\n", key_val); } } 信号的方式: 如果熟悉RTOS操作系统,对信号量肯定不陌生,信号量是对多个任务之间通信使用的。而Linux里面内核与应用层除了read,writer,ioctl,来进行数据交互,还有信号这个东西,可以让内核用信号来告知应用层,只是没有数据罢了。这样应用层就有了中断了,对,可以理解为应用层的中断,有信号了,应用层就会执行相应的函数。这个信号的分析,需要先从应用层分析 应用层的实现: 内核提供了signal()函数接口,有两个参数,第一个参数是SIGIO,其值大小为29,第二参数就像内核申请中断函数一样,就是自己的中断函数,需要自己写,自己想,而信号来了会自动的跳转到这个函数中去。 signal(SIGIO, button_signal); void button_signal(int signum) { unsigned char key_val; read(fd, &key_val, 1); printf("key_val: 0x%x\n", key_val); } 应用层要是写好了,就可以gcc 编译一下,测试一下,运行应用程序,再打开一个终端,ps找到这个应用程序的进程号,比如 2376 kill -29 2376 应该就会有反应,没有反应就好好检查程序。 关于信号的种类及处理,可以看下面这个网页 http://blog.chinaunix.net/uid-25002135-id-3300821.html 这样的情况是手动的操作,后续机器是自动操作的,不可能处处需要人为,那就需要一个方法让应用程序可以告诉对应的驱动程序我的进程号并且信号的大小。
  1. fcntl(fd, F_SETOWN, getpid());
  2. Oflags = fcntl(fd, F_GETFL);
  3. fcntl(fd, F_SETFL, Oflags | FASYNC);
这三句话都牵扯到了一个函数fcntl(),这个是应用层给内核命令的,太多的解释,自己也会含糊不清,第一句,告诉内核我的进程号,第二句选择好信号大小,第三句让驱动程序在fasync()函数里做好更改。 驱动程序的实现: 这个在应用层提到的fasync()函数,就在驱动程序file_operations结构里,所以添加即可
  1. .fasync = button_fasync,
  1. static int button_fasync (int fd, struct file *filp, int on)
  2. {
  3. printk("driver: sixth_drv_fasync\n");
  4. return fasync_helper (fd, filp, on, &button_async);
  5. }
配置好了,内核只需要通过 static struct fasync_struct *button_async; //作为全局使用 kill_fasync (&button_async, SIGIO, POLL_IN); 什么时候用,那就是自己的事了,这样应用层与驱动程序就做好了联系 应用程序的改进: 这种信号的方式,应用程序就不需要采用while(1){}死循环的方式了,如果用C写应用程序,需要做的就是不能程序return,所以还是需要while(1){sleep(100);} 只是不做什么事而已。 如果要用QT写一个应用程序,怎么办?而我的QT是用C++写的,而signal()函数是对应C的函数,如果对应到了类的成员函数,要是你用类button,不断的去创造其他子类,就有button ss,button ss1.......那么就会有ss1.signal().ss.signel(),那内核发过来的信号到底要跳到哪个signal()函数中去,这是一个问题!假如抛开类的操作,就在QT里面使用纯C函数,那么要是在这个函数里面使用到GUI也是不行的,这个函数只能处理C函数,C++函数是处理不了的,而GUI是用C++写的,怎么解决? 这个时候只能感概C++与C的不容,仔细琢磨,不应该呀,肯定是自己C++能力不够,于是查资料,果然皇天不负有心人! 静态成员函数 在《C++程序设计教程-钱能》第八章第6节 QQ图片20160730104053.jpg 就是共有的 这个词打破了这个局面,我可以在类中建一个静态成员函数,这样不管你用这个类继续创了多少个子类,而这个成员函数有且只有一个,而且还是这些个子类共有的,那么 signal(SIGIO, button_signal); 这个函数就可以在QT中使用了,只要button_signal函数是作为静态成员函数的存在就行。 接下来又有一个问题了,要是现在这个函数里使用其他成员函数从而使用GUI控件,怎么办?C++默认静态成员是不能访问别的成员函数的,只有被访问。一找资料,网上就是一大批的涌现,网络是个好东西,都有详细的解决方法,解决静态成员函数访问其他成员函数的方法有一些,这里不细诉。 看看效果 这里QT采用弹窗处理,按下按键,就弹一个窗口,在这个窗口并且显示是那个按键按下的 QQ图片20160730082754.jpg QQ图片20160730082759.jpg QQ图片20160730082803.jpg 本帖最后由 ywlzh 于 2016-7-30 11:19 编辑
天地庄周马;江湖范蠡船。 个性签名还是放QQ号吧,2060347305,添加说明EEworld好友

回复评论 (4)

内核定时器 消抖处理: 按键按下,没有RS触发器硬件消抖,就只能采用软件消抖的办法,在单片机编程的时候,要么是延时一段时间,要么是用定时器定时查询按键,而Linux内核,如果在驱动程序用延时函数,这回损耗LinuxCPU,对于多任务多进程的操作系统,纯延时是不可取的,而在RTOS系统里,延时也就是调度,这倒是可取的。 内核定时器,是系统已经做好的,对于开发者来说,就是玩初始化一个定时器,为其添加时间,定时器函数,就OK了,触发了定时器,过段时间就会跳到定时器函数里去。
  1. static struct timer_list button_timer; //定义一个全局且静态的timer_list 结构体
  1. init_timer(&button_timer);
  2. button_timer.function = button_timer_function;
  3. add_timer(&button_timer);
值得注意的是,这三句话在入口函数里写的话,会立即跳到button_timer_function函数中去,所以要在button_timer_function里添加一句,判断按键是否有按下,没有直接返回。这一步很重要,要不然,整个Linux系统会崩溃不动,终端没有任何反应。
  1. mod_timer(&button_timer,jiffies+HZ/100); //10ms以后启动定时
这句话就是触发定时器并且刷新定时器定时时间的函数,这句话添加进中断函数里面去,就是按键哪怕是有抖动,那也是在每个抖动会触发这个函数,而这个函数就不停的刷新定时器的时间,也就是说,这个函数被最后一个抖动给触发了,也就会重新的定上10ms,而button_timer_function只触发了一次,定时器的时间被按键抖动给不停的刷新,而时间不断的复位,最后就是最后的10ms才能进button_timer_function函数, 而定时器的时间是怎么确定的呢? QQ图片20160804095507.png 至此,内核定时的基础操作已完成 本帖最后由 ywlzh 于 2016-8-4 10:05 编辑
天地庄周马;江湖范蠡船。 个性签名还是放QQ号吧,2060347305,添加说明EEworld好友
点赞  2016-8-4 09:55
厉害,值得一看
点赞  2017-9-10 22:16
楼主怎么没把Qt实现弹出窗口的代码贴出来啊?
点赞  2018-1-9 08:29
引用: tianjinpw 发表于 2018-1-9 08:29
楼主怎么没把Qt实现弹出窗口的代码贴出来啊?

静态成员函数里写个messagebox就行了 没必要贴出来,知道怎么传this就OK 了
天地庄周马;江湖范蠡船。 个性签名还是放QQ号吧,2060347305,添加说明EEworld好友
点赞  2018-1-12 21:07
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复