问题
在 jz2440_字符设备按键驱动程序 这篇文章中,我们描述了使用按键驱动程序的几种方法。
不过在实际操作后,我们可能会出现下面的问题:
当我们连续快速按下按键,或者因为某些原因机器的按键发生了抖动,就会导致按键只被按下一次,却产生了多次中断,在某些特定情况中会出现一些程序问题。
为了解决上述的问题。我们可以使用内核中的定时器机制。
定时器
使用定时器有两个要素:
- 定时器的超时时间设置;
- 定时器超时后,要执行的处理函数;
如何使用定时器防止抖动?
看到上图:
- 在 A 点,产生了一次中断,我们就设置定时器在10ms后再执行中断处理函数;
- 在 B 点,又产生了一次中断,由于按键抖动的时间间隔是非常小的,不可能超过10ms。因此,我们就可以重置定时器,让其重新计时10ms;
- 以此类推,在 F 点又产生了一次中断,会再重置定时器的时间;
- 最终,即使因为按键抖动产生了多次中断,也只会执行一次中断处理函数;
驱动程序
使用内核的 timer 定时器,其 API 说明可以参考 Linux内核API 定时机制|极客笔记 (deepinout.com)
-
定义一个 timer 定时器
static struct timer_list s3c2440_button_timer;
-
进行初始化,在 init 函数中进行初始化:
static int __init s3c2440_buttons_init(void) { major = register_chrdev(0, DEVICE_NAME, &s3c2440_buttons_fops); if (major < 0) { printk(DEVICE_NAME " can't register major number\n"); return major; } s3c2440_buttons_class = class_create(THIS_MODULE, "s3c2440_buttons_poll"); if (IS_ERR(s3c2440_buttons_class)) return PTR_ERR(s3c2440_buttons_class); s3c2440_buttons_dev = class_device_create(s3c2440_buttons_class, NULL, MKDEV(major, 0), NULL, "button_shake"); if (unlikely(IS_ERR(s3c2440_buttons_dev))) return PTR_ERR(s3c2440_buttons_dev); init_timer(s3c2440_button_timer); /* 对定时器进行初始化 */ s3c2440_button_timer.data = NULL; /* 传给处理函数的参数 unsigned long类型 */ s3c2440_button_timer.function = s3c2440_buttons_timer; /* 定时器超时的处理函数 */ add_timer(&s3c2440_button_timer); /* 将定时器注册进内核, * 当定时器超时处理函数就会被调用 */ printk(DEVICE_NAME " initialized!\n"); return 0; }
上面我们设置了 处理函数 还没有设置超时时间。
-
在中断处理函数中设置超时时间:
static irqreturn_t s3c2440_buttons_handler(int irq, void *dev_id) { /* 10ms后启动定时器 */ mod_timer(&s3c2440_button_timer, jiffies + (HZ/100)); return IRQ_HANDLED; }
我们将原来的中断处理的代码移动到了定时器超时的处理函数中,而中断处理函数只进行定时器超时的设置。
jiffies:是一个内核中的全局变量。每种计算机底层体系结构都提供了一些执行周期性操作的手段,通常的形式是定时器中断。jiffies 递增的频率同体系结构有关,取决于内核中一个主要的常数 HZ。该常数的值通常介于100和1 000中间。换言之,jiffies 的值每秒递增的次数在100至1 000次之间。
对于 jz2440 开发板来说,这值为 FCLK 的值,也就是100HZ。也就是说,一秒时间内会产生100次定时器中断,每次中断都会使 jiffies 的值加一。
基于 jiffies 的计时相对粒度较粗,因为目前1 000 Hz已经算不上很高的频率了。在底层硬件能力允许的前提下,内核可使用高分辨率的定时器提供额外的计时手段,能够以纳秒级的精确度和分辨率来计量时间。
计时的周期是可以动态改变的。在没有或无需频繁的周期性操作的情况下,周期性地产生定时器中断是没有意义的,这会阻止处理器降低耗电进入睡眠状态。动态改变计时周期对于供电受限的系统是很有用的,例如笔记本电脑和嵌入式系统。
j i f f i e s + ( H Z / 100 ) jiffies + (HZ/100) jiffies+(HZ/100) 让定时器在 jiffies 的基础上,加上百分之一的 HZ 时间,就可以使得定时器在10ms后触发。
在 A 点时,设置为了 jiffies + 10 后触发中断执行;而在 B 点时,又设置为 jiffies + 10 后触发中断执行。这样无论在触发了多少次中断,最终结果都是在最后一次中断的 jiffies + 10 后执行处理函数。
-
定时器超时处理函数:
static void s3c2440_buttons_timer(unsigned long data) { struct pin_desc *pindesc = (struct pin_desc *)s3c2440_timer_data.dev_id; if (pindesc == NULL) return; unsigned char pinval; pinval = s3c2410_gpio_getpin(pindesc->pin); /* 读出引脚值 */ if (pinval) { key_val = 0x80 | pindesc->key_val; /* 按键松开 */ } else { key_val = pindesc->key_val; /* 按键按下 */ } ev_press = 1; wake_up_interruptible(&button_waitq); kill_fasync(&s3c2440_buttons_async_queue, SIGIO, POLL_IN); }
if (pindesc == NULL)
:是为了防止定时器,刚刚在 init 创建通过add_timer(&s3c2440_button_timer);
添加到内核就立即被执行一次。 -
在 exit 函数中,从内核移除定时器:
static void __exit s3c2440_buttons_exit(void) { /* 卸载驱动程序 */ class_device_destroy(s3c2440_buttons_class, MKDEV(major, 0)); class_destroy(s3c2440_buttons_class); unregister_chrdev(major, DEVICE_NAME); del_timer(&s3c2440_button_timer); /* 移除定时器 */ printk(DEVICE_NAME " unregister\n"); }