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以固定频率闪烁,或实现无需中断轮询的驱动。敬请期待!


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

相关推荐
cui_ruicheng11 小时前
Linux网络编程(十):自定义协议与网络计算器
linux·服务器·网络·tcp/ip
开开心心就好11 小时前
180套模板的图片艺术拼接实用工具
linux·服务器·网络·spring·智能手机·maven·excel
Strugglingler11 小时前
Linux Device Drivers-第十章 中断处理
linux·irq·工作队列·tasklet·中断共享
程序leo源11 小时前
Qt界面优化详解
linux·c语言·开发语言·c++·qt·c#
tang74516396212 小时前
Ubuntu 24.04 安装 Nginx 1.29.6 完整版教程20260320
linux·nginx·ubuntu
Tingjct12 小时前
【linux】part1-进程详解
linux·运维·服务器
小糖学代码12 小时前
LLM系列:环境搭建:4.Nginx使用教程
运维·python·神经网络·nginx
L16247612 小时前
OpenSSL + OpenSSH 两套安装方案(覆盖系统目录 / 独立目录)
linux·ssh
汪汪大队u12 小时前
XX校园网规划与搭建实验
运维·网络