Linux字符设备驱动开发(八):中断底半部——tasklet与工作队列实现按键消抖

前言

上一篇文章中,我们首次在驱动中使用了中断,将GPIO按键接入输入子系统。但在那个驱动里,所有工作都在**中断处理函数(顶半部)**中完成:读取GPIO电平、上报按键事件。这在按键场景下尚可接受,因为操作足够短。

然而,内核要求中断处理尽可能快:中断期间,当前CPU被独占,同优先级及更低优先级的中断被屏蔽,系统响应能力下降。如果要在中断中做耗时操作(如大量数据处理、慢速I/O),就需要将工作分成两半:

  • 顶半部(Top Half):在中断上下文中执行,仅完成最紧急的任务(如记录状态、确认中断),并调度底半部。
  • 底半部(Bottom Half):在更宽松的上下文中执行,处理耗时或可延迟的工作。

Linux提供了多种底半部机制:tasklet工作队列(workqueue)软中断(softirq)等。本文将以GPIO按键驱动为例,展示如何使用tasklet工作队列 实现底半部处理,并加入经典的软件消抖功能。你将掌握:

  • 中断顶半部与底半部的设计原则
  • tasklet的初始化、调度与执行(含注意事项)
  • 工作队列的创建、延迟调度与取消
  • 如何在底半部中实现软件消抖,消除按键抖动

一、中断处理的分工

1.1 为什么需要底半部?

Linux的中断处理分为两类上下文:

  • 中断上下文 :运行在硬件中断或软中断中,不可阻塞(不能调用可能导致睡眠的函数),必须快速完成。
  • 进程上下文:运行在内核线程或用户进程中,可以睡眠,可以访问用户空间,可以持有信号量。

如果在中断上下文中调用耗时的函数(如msleepcopy_to_useri2c_transfer),会导致内核崩溃或严重延迟。因此,中断处理逻辑应尽可能短的顶半部,然后通过底半部执行剩余工作。

1.2 常见底半部机制对比

机制 执行上下文 特点 适用场景
tasklet 软中断上下文 不可睡眠,同一tasklet不会同时在多CPU上运行,实现简单 轻量级、高频率的后续处理
工作队列 内核线程上下文 可以睡眠,可以执行耗时操作,有延迟 需要睡眠或较大延迟的处理
软中断 软中断上下文 可以在多CPU上并发执行,需要更小心地处理并发 网络子系统等高性能场景

本文重点演示tasklet (不可睡眠,轻量)和工作队列(可以睡眠,灵活),并实现消抖。


二、软件消抖原理

机械按键在按下和释放瞬间,由于金属弹片接触反弹,电平会在几十毫秒内多次抖动。如果直接上报这些电平跳变,用户空间会看到一次按键产生多次按下/释放事件。

软件消抖 的核心思想:在首次检测到电平变化后,延迟一段时间(通常10ms ~ 20ms)再次读取电平,若电平稳定,则认为是一次有效按键,再进行上报。

这个"延迟判断"的工作极不适合在顶半部(中断)中完成,因为需要延迟。因此,我们把它放在底半部。


三、方案一:使用tasklet实现

3.1 tasklet的特点与限制

tasklet运行在软中断上下文,不能调用可能导致睡眠的函数 (如msleepmutex_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以固定频率闪烁,或实现无需中断轮询的驱动。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

相关推荐
AlfredZhao7 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
Inhand陈工2 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信