前言
在上一篇文章中,我们首次在驱动中使用了中断,将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:
c
/*
* gpio_key_tasklet.c
* GPIO按键输入设备驱动 ------ tasklet底半部 + 软件消抖(忙等待演示)。
* 顶半部仅调度tasklet;tasklet内忙等待15ms后二次读取,确认稳定后上报。
* 注意:tasklet不可睡眠,这里使用mdelay(忙等待),仅用于演示,实际不建议。
* 作者:[你的ID]
* 适配内核:Linux 5.x
*/
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/of.h>
#include <linux/delay.h> /* mdelay */
static struct input_dev *key_input;
static struct gpio_desc *key_gpio;
static int key_irq;
/* tasklet 结构体 */
static struct tasklet_struct key_tasklet;
/* 底半部处理函数:忙等待消抖后判断按键状态并上报 */
static void key_tasklet_handler(unsigned long data)
{
int val1, val2;
/* 第一次读取电平 */
val1 = gpiod_get_value(key_gpio);
/* 延时15ms,等待抖动结束(tasklet不可睡眠,用忙等待) */
mdelay(15);
/* 第二次读取电平 */
val2 = gpiod_get_value(key_gpio);
/* 如果两次电平一致,则认为有效按键 */
if (val1 == val2) {
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 */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
tasklet_schedule(&key_tasklet);
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
key_gpio = gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(key_gpio))
return PTR_ERR(key_gpio);
key_irq = gpiod_to_irq(key_gpio);
if (key_irq < 0) {
ret = key_irq;
goto err_get_irq;
}
/* 初始化tasklet,绑定底半部函数 */
tasklet_init(&key_tasklet, key_tasklet_handler, 0);
ret = request_irq(key_irq, key_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_key", NULL);
if (ret)
goto err_req_irq;
key_input = devm_input_allocate_device(dev);
if (!key_input) {
ret = -ENOMEM;
goto err_alloc_input;
}
key_input->name = "GPIO Key (tasklet)";
key_input->phys = "gpio_key/input0";
key_input->id.bustype = BUS_HOST;
set_bit(EV_KEY, key_input->evbit);
set_bit(KEY_ENTER, key_input->keybit);
ret = input_register_device(key_input);
if (ret)
goto err_register_input;
pr_info("gpio_key: tasklet version loaded\n");
return 0;
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);
return ret;
}
static int gpio_key_remove(struct platform_device *pdev)
{
free_irq(key_irq, NULL);
tasklet_kill(&key_tasklet); /* 等待tasklet完成 */
gpiod_put(key_gpio);
return 0;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "yourname,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "gpio_key",
.owner = THIS_MODULE,
.of_match_table = gpio_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:
c
/*
* gpio_key_wq.c
* GPIO按键输入设备驱动 ------ 工作队列底半部 + 软件消抖。
* 顶半部取消旧延迟工作并重新排队;延迟后直接读取稳定电平上报。
* 作者:[你的ID]
* 适配内核:Linux 5.x
*/
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/of.h>
#include <linux/workqueue.h> /* work_struct, delayed_work */
static struct input_dev *key_input;
static struct gpio_desc *key_gpio;
static int key_irq;
/* 工作队列结构体 */
static struct workqueue_struct *key_wq;
static struct delayed_work key_dwork;
/* 工作处理函数(底半部):延迟后读取电平并上报 */
static void key_work_handler(struct work_struct *work)
{
int val;
val = gpiod_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);
}
/* 顶半部:取消前次未执行的延迟工作,再排队新的 */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
/* cancel_delayed_work 在中断上下文中安全(不会睡眠),
* 用于取消尚未执行的工作。若已开始执行则返回false,我们
* 仍然排队新工作,但已执行的工作不会受影响。
* 这样抖动期间只有最后一次中断会真正触发上报。
*/
cancel_delayed_work(&key_dwork);
queue_delayed_work(key_wq, &key_dwork, msecs_to_jiffies(15));
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
key_gpio = gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(key_gpio))
return PTR_ERR(key_gpio);
key_irq = gpiod_to_irq(key_gpio);
if (key_irq < 0) {
ret = key_irq;
goto err_get_irq;
}
/* 创建工作队列 */
key_wq = create_singlethread_workqueue("gpio_key_wq");
if (!key_wq) {
ret = -ENOMEM;
goto err_create_wq;
}
/* 初始化延迟工作 */
INIT_DELAYED_WORK(&key_dwork, key_work_handler);
ret = request_irq(key_irq, key_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_key", NULL);
if (ret)
goto err_req_irq;
key_input = devm_input_allocate_device(dev);
if (!key_input) {
ret = -ENOMEM;
goto err_alloc_input;
}
key_input->name = "GPIO Key (workqueue)";
key_input->phys = "gpio_key/input0";
key_input->id.bustype = BUS_HOST;
set_bit(EV_KEY, key_input->evbit);
set_bit(KEY_ENTER, key_input->keybit);
ret = input_register_device(key_input);
if (ret)
goto err_register_input;
pr_info("gpio_key: workqueue version loaded\n");
return 0;
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);
return ret;
}
static int gpio_key_remove(struct platform_device *pdev)
{
free_irq(key_irq, NULL);
cancel_delayed_work_sync(&key_dwork); /* 等待工作完成并取消 */
destroy_workqueue(key_wq);
gpiod_put(key_gpio);
return 0;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "yourname,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "gpio_key",
.owner = THIS_MODULE,
.of_match_table = gpio_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
可同时编译两个驱动(测试时分别加载):
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"),确保按键硬件正确。
bash
# 加载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以固定频率闪烁,或实现无需中断轮询的驱动。敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!