前言在上一篇文章中我们首次在驱动中使用了中断将GPIO按键接入输入子系统。但在那个驱动里所有工作都在**中断处理函数顶半部**中完成读取GPIO电平、上报按键事件。这在按键场景下尚可接受因为操作足够短。然而内核要求中断处理尽可能快中断期间当前CPU被独占同优先级及更低优先级的中断被屏蔽系统响应能力下降。如果要在中断中做耗时操作如大量数据处理、慢速I/O就需要将工作分成两半顶半部Top Half在中断上下文中执行仅完成最紧急的任务如记录状态、确认中断并调度底半部。底半部Bottom Half在更宽松的上下文中执行处理耗时或可延迟的工作。Linux提供了多种底半部机制tasklet、工作队列workqueue、软中断softirq等。本文将以GPIO按键驱动为例展示如何使用tasklet和工作队列实现底半部处理并加入经典的软件消抖功能。你将掌握中断顶半部与底半部的设计原则tasklet的初始化、调度与执行含注意事项工作队列的创建、延迟调度与取消如何在底半部中实现软件消抖消除按键抖动一、中断处理的分工1.1 为什么需要底半部Linux的中断处理分为两类上下文中断上下文运行在硬件中断或软中断中不可阻塞不能调用可能导致睡眠的函数必须快速完成。进程上下文运行在内核线程或用户进程中可以睡眠可以访问用户空间可以持有信号量。如果在中断上下文中调用耗时的函数如msleep、copy_to_user、i2c_transfer会导致内核崩溃或严重延迟。因此中断处理逻辑应尽可能短的顶半部然后通过底半部执行剩余工作。1.2 常见底半部机制对比机制执行上下文特点适用场景tasklet软中断上下文不可睡眠同一tasklet不会同时在多CPU上运行实现简单轻量级、高频率的后续处理工作队列内核线程上下文可以睡眠可以执行耗时操作有延迟需要睡眠或较大延迟的处理软中断软中断上下文可以在多CPU上并发执行需要更小心地处理并发网络子系统等高性能场景本文重点演示tasklet不可睡眠轻量和工作队列可以睡眠灵活并实现消抖。二、软件消抖原理机械按键在按下和释放瞬间由于金属弹片接触反弹电平会在几十毫秒内多次抖动。如果直接上报这些电平跳变用户空间会看到一次按键产生多次按下/释放事件。软件消抖的核心思想在首次检测到电平变化后延迟一段时间通常10ms ~ 20ms再次读取电平若电平稳定则认为是一次有效按键再进行上报。这个“延迟判断”的工作极不适合在顶半部中断中完成因为需要延迟。因此我们把它放在底半部。三、方案一使用tasklet实现3.1 tasklet的特点与限制tasklet运行在软中断上下文不能调用可能导致睡眠的函数如msleep、mutex_lock等。因此消抖所需的延时只能通过忙等待如mdelay实现。忙等待会占用CPU通常不推荐这里仅作演示。实际产品中如需要睡眠延迟应使用工作队列。3.2 驱动代码tasklet版本新建文件gpio_key_tasklet.c/* * gpio_key_tasklet.c * GPIO按键输入设备驱动 —— tasklet底半部 软件消抖忙等待演示。 * 顶半部仅调度tasklettasklet内忙等待15ms后二次读取确认稳定后上报。 * 注意tasklet不可睡眠这里使用mdelay忙等待仅用于演示实际不建议。 * 作者[你的ID] * 适配内核Linux 5.x */#includelinux/module.h#includelinux/device.h#includelinux/platform_device.h#includelinux/gpio/consumer.h#includelinux/interrupt.h#includelinux/input.h#includelinux/of.h#includelinux/delay.h/* mdelay */staticstructinput_dev*key_input;staticstructgpio_desc*key_gpio;staticintkey_irq;/* tasklet 结构体 */staticstructtasklet_structkey_tasklet;/* 底半部处理函数忙等待消抖后判断按键状态并上报 */staticvoidkey_tasklet_handler(unsignedlongdata){intval1,val2;/* 第一次读取电平 */val1gpiod_get_value(key_gpio);/* 延时15ms等待抖动结束tasklet不可睡眠用忙等待 */mdelay(15);/* 第二次读取电平 */val2gpiod_get_value(key_gpio);/* 如果两次电平一致则认为有效按键 */if(val1val2){input_report_key(key_input,KEY_ENTER,val1?1:0);input_sync(key_input);pr_info(gpio_key: stable key %s (val%d)\n,val1?pressed:released,val1);}else{pr_info(gpio_key: bounce ignored (val1%d, val2%d)\n,val1,val2);}}/* 顶半部仅调度tasklet */staticirqreturn_tkey_irq_handler(intirq,void*dev_id){tasklet_schedule(key_tasklet);returnIRQ_HANDLED;}staticintgpio_key_probe(structplatform_device*pdev){intret;structdevice*devpdev-dev;key_gpiogpiod_get(dev,key,GPIOD_IN);if(IS_ERR(key_gpio))returnPTR_ERR(key_gpio);key_irqgpiod_to_irq(key_gpio);if(key_irq0){retkey_irq;gotoerr_get_irq;}/* 初始化tasklet绑定底半部函数 */tasklet_init(key_tasklet,key_tasklet_handler,0);retrequest_irq(key_irq,key_irq_handler,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,gpio_key,NULL);if(ret)gotoerr_req_irq;key_inputdevm_input_allocate_device(dev);if(!key_input){ret-ENOMEM;gotoerr_alloc_input;}key_input-nameGPIO Key (tasklet);key_input-physgpio_key/input0;key_input-id.bustypeBUS_HOST;set_bit(EV_KEY,key_input-evbit);set_bit(KEY_ENTER,key_input-keybit);retinput_register_device(key_input);if(ret)gotoerr_register_input;pr_info(gpio_key: tasklet version loaded\n);return0;err_register_input:err_alloc_input:free_irq(key_irq,NULL);err_req_irq:tasklet_kill(key_tasklet);err_get_irq:gpiod_put(key_gpio);returnret;}staticintgpio_key_remove(structplatform_device*pdev){free_irq(key_irq,NULL);tasklet_kill(key_tasklet);/* 等待tasklet完成 */gpiod_put(key_gpio);return0;}staticconststructof_device_idgpio_key_of_match[]{{.compatibleyourname,gpio-key},{}};MODULE_DEVICE_TABLE(of,gpio_key_of_match);staticstructplatform_drivergpio_key_driver{.probegpio_key_probe,.removegpio_key_remove,.driver{.namegpio_key,.ownerTHIS_MODULE,.of_match_tablegpio_key_of_match,},};module_platform_driver(gpio_key_driver);MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(GPIO key with tasklet debounce (mdelay demo));MODULE_VERSION(1.0);代码说明tasklet_init绑定处理函数调度后由内核在适当时候执行。mdelay(15)忙等待15ms因tasklet不可睡眠不能使用msleep。二次读取比对一致才上报实现消抖。模块卸载时tasklet_kill确保tasklet不再运行。四、方案二使用工作队列实现推荐4.1 工作队列的优势工作队列在内核线程中执行可以睡眠因此可使用msleep或直接利用延迟工作队列delayed_work来推迟执行无需手动延时。且可在中断中取消前次未执行的工作避免抖动期间累积多个事件。4.2 驱动代码工作队列版本新建文件gpio_key_wq.c/* * gpio_key_wq.c * GPIO按键输入设备驱动 —— 工作队列底半部 软件消抖。 * 顶半部取消旧延迟工作并重新排队延迟后直接读取稳定电平上报。 * 作者[你的ID] * 适配内核Linux 5.x */#includelinux/module.h#includelinux/device.h#includelinux/platform_device.h#includelinux/gpio/consumer.h#includelinux/interrupt.h#includelinux/input.h#includelinux/of.h#includelinux/workqueue.h/* work_struct, delayed_work */staticstructinput_dev*key_input;staticstructgpio_desc*key_gpio;staticintkey_irq;/* 工作队列结构体 */staticstructworkqueue_struct*key_wq;staticstructdelayed_workkey_dwork;/* 工作处理函数底半部延迟后读取电平并上报 */staticvoidkey_work_handler(structwork_struct*work){intval;valgpiod_get_value(key_gpio);input_report_key(key_input,KEY_ENTER,val?1:0);input_sync(key_input);pr_info(gpio_key: work reported key %s (val%d)\n,val?pressed:released,val);}/* 顶半部取消前次未执行的延迟工作再排队新的 */staticirqreturn_tkey_irq_handler(intirq,void*dev_id){/* cancel_delayed_work 在中断上下文中安全不会睡眠 * 用于取消尚未执行的工作。若已开始执行则返回false我们 * 仍然排队新工作但已执行的工作不会受影响。 * 这样抖动期间只有最后一次中断会真正触发上报。 */cancel_delayed_work(key_dwork);queue_delayed_work(key_wq,key_dwork,msecs_to_jiffies(15));returnIRQ_HANDLED;}staticintgpio_key_probe(structplatform_device*pdev){intret;structdevice*devpdev-dev;key_gpiogpiod_get(dev,key,GPIOD_IN);if(IS_ERR(key_gpio))returnPTR_ERR(key_gpio);key_irqgpiod_to_irq(key_gpio);if(key_irq0){retkey_irq;gotoerr_get_irq;}/* 创建工作队列 */key_wqcreate_singlethread_workqueue(gpio_key_wq);if(!key_wq){ret-ENOMEM;gotoerr_create_wq;}/* 初始化延迟工作 */INIT_DELAYED_WORK(key_dwork,key_work_handler);retrequest_irq(key_irq,key_irq_handler,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,gpio_key,NULL);if(ret)gotoerr_req_irq;key_inputdevm_input_allocate_device(dev);if(!key_input){ret-ENOMEM;gotoerr_alloc_input;}key_input-nameGPIO Key (workqueue);key_input-physgpio_key/input0;key_input-id.bustypeBUS_HOST;set_bit(EV_KEY,key_input-evbit);set_bit(KEY_ENTER,key_input-keybit);retinput_register_device(key_input);if(ret)gotoerr_register_input;pr_info(gpio_key: workqueue version loaded\n);return0;err_register_input:err_alloc_input:free_irq(key_irq,NULL);err_req_irq:destroy_workqueue(key_wq);err_create_wq:err_get_irq:gpiod_put(key_gpio);returnret;}staticintgpio_key_remove(structplatform_device*pdev){free_irq(key_irq,NULL);cancel_delayed_work_sync(key_dwork);/* 等待工作完成并取消 */destroy_workqueue(key_wq);gpiod_put(key_gpio);return0;}staticconststructof_device_idgpio_key_of_match[]{{.compatibleyourname,gpio-key},{}};MODULE_DEVICE_TABLE(of,gpio_key_of_match);staticstructplatform_drivergpio_key_driver{.probegpio_key_probe,.removegpio_key_remove,.driver{.namegpio_key,.ownerTHIS_MODULE,.of_match_tablegpio_key_of_match,},};module_platform_driver(gpio_key_driver);MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(GPIO key with workqueue debounce);MODULE_VERSION(1.0);代码说明cancel_delayed_work在中断上下文安全使用用于取消尚未执行的延迟工作。这样每次中断都会重置等待计时确保只有在最后一次中断的15ms后才会执行一次上报完美消抖。处理函数中直接读取电平并上报无需再延时。queue_delayed_work将工作延迟msecs_to_jiffies(15)后投入工作队列。卸载时使用cancel_delayed_work_sync防止工作还在运行。五、Makefile可同时编译两个驱动测试时分别加载KERNEL_DIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) obj-m : gpio_key_tasklet.o gpio_key_wq.o all: make -C $(KERNEL_DIR) M$(PWD) modules clean: make -C $(KERNEL_DIR) M$(PWD) clean六、测试与验证设备树节点与前一篇完全相同compatible yourname,gpio-key确保按键硬件正确。# 加载tasklet版本insmod gpio_key_tasklet.ko# 用evtest观察按键效果快速按下应只有一对press/release事件evtest /dev/input/eventX rmmod gpio_key_tasklet# 加载workqueue版本insmod gpio_key_wq.ko evtest /dev/input/eventX rmmod gpio_key_wq分别测试观察消抖效果抖动不再产生虚假事件。工作队列版本由于可以睡眠且不会长时间占用CPU为实际工程推荐方案。七、总结与下篇预告本文通过tasklet和工作队列重构了按键驱动并实现了软件消抖。关键原则中断顶半部只做最少的事情耗时或可延迟的工作交给底半部。tasklet 轻量但不可睡眠需谨慎使用延时。工作队列更灵活推荐用于需要延时或可能睡眠的场景。下篇预告我们将利用内核定时器实现更精确的周期性任务例如让LED以固定频率闪烁或实现无需中断轮询的驱动。敬请期待如果本文对你有帮助欢迎点赞、收藏、关注。有任何技术疑问欢迎在评论区留言交流