在实际的嵌入式项目中,物理按键(机械开关)必然存在抖动 (Bouncing)。如果只依赖中断,按下一次开关,CPU 可能会检测到十几次电平跳变,导致功能被误触发。此时,利用内核定时器 (Kernel Timer) 来实现软件消抖,是一种常见且有效的解决方案。
消抖的核心思想可以形象地理解为 “让子弹飞一会儿”:
- 中断发生(顶半部):在按键按下的瞬间,我们并不立即处理具体的业务逻辑,而是设置或修改一个定时器的超时时间(例如,向后推迟 20ms)。
- 抖动过滤:如果在接下来的 20ms 内又产生了新的中断(由机械抖动引起),我们再次重置这个定时器,重新开始计时 20ms。
- 稳定确认:如果 20ms 内没有新的中断触发,则认为电平已经稳定,此时才在定时器的回调函数中执行真正的按键处理逻辑。
这其中的关键机制,是中断处理和定时器管理,它们是 Linux 内核并发编程的重要基础。
下面的代码展示了一个结合 GPIO 中断和内核定时器实现按键消抖的完整驱动示例:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/slab.h>
#include <linux/gpio.h> // 需要操作GPIO读取电平
#include <linux/of_gpio.h>
#include <linux/timer.h> // 核心头文件
/*
* 宏定义:消抖延时 (毫秒)
* 经验值:机械按键通常在 5ms - 15ms 之间
*/
#define DEBOUNCE_INTERVAL_MS 20
struct my_key_data {
int irq_num;
int gpio_num; // 保存 GPIO 编号,用于在定时器中读取真实电平
struct timer_list my_timer; // 定义定时器结构体
struct device *dev;
};
/*
* =====================================================
* 3. 定时器超时回调函数 (真正的底半部)
*
* 运行环境:SoftIRQ (软中断上下文)
* 权限:原子上下文,不允许休眠!(不能调用 msleep, mutex等)
* 如果需要做耗时操作,请在这里 schedule_work()
* =====================================================
*/
void my_timer_function(struct timer_list *t)
{
// 通过 from_timer 宏获取父结构体 (类似 container_of)
struct my_key_data *data = from_timer(data, t, my_timer);
int val;
// 读取当前引脚的真实电平,确保不是误触
// 注意:这里假设按键按下是低电平 (Active Low)
val = gpio_get_value(data->gpio_num); // 这类GPIO操作在[嵌入式Linux开发](/f/33-1)中很常见
if (val == 0) { // 依然是低电平,说明按键确实按下了,且稳定了
dev_info(data->dev, "【按键确认】Key Pressed! (Timer Expired)\n");
// TODO: 如果这里有复杂的 I2C/SPI 操作,请在这里 schedule_work(&data->my_work);
} else {
// 如果变成了高电平,说明刚才的抖动是干扰,或者按键已经抬起了
dev_info(data->dev, "【抖动/抬起】Ignored jitter.\n");
}
}
/*
* =====================================================
* 2. 顶半部 - 硬件中断
* 任务:仅仅是重置定时器
* =====================================================
*/
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_key_data *data = (struct my_key_data *)dev_id;
/*
* mod_timer (修改定时器):
* 如果定时器没激活,它会激活。
* 如果定时器已经在跑了(比如才过了 5ms),它会把时间重置,重新再等 20ms。
* 这就完美过滤了连续的抖动波形。
*/
mod_timer(&data->my_timer, jiffies + msecs_to_jiffies(DEBOUNCE_INTERVAL_MS));
return IRQ_HANDLED;
}
/*
* =====================================================
* 1. 驱动初始化
* =====================================================
*/
static int my_probe(struct platform_device *pdev)
{
struct my_key_data *data;
int ret;
enum of_gpio_flags flag;
data = devm_kzalloc(&pdev->dev, sizeof(struct my_key_data), GFP_KERNEL);
if (!data) return -ENOMEM;
data->dev = &pdev->dev;
platform_set_drvdata(pdev, data);
// 1. 获取 GPIO 编号 (为了在定时器里读取电平值)
// 假设设备树里是 gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
data->gpio_num = of_get_named_gpio_flags(pdev->dev.of_node, "gpios", 0, &flag);
if (!gpio_is_valid(data->gpio_num)) {
dev_err(&pdev->dev, "无效的 GPIO\n");
return -EINVAL;
}
// 申请 GPIO (为了读取值)
if (devm_gpio_request(&pdev->dev, data->gpio_num, "my_key_gpio")) {
dev_err(&pdev->dev, "无法请求 GPIO\n");
return -EINVAL;
}
// 2. 获取中断号
data->irq_num = gpio_to_irq(data->gpio_num); // 或者用 platform_get_irq
// 3. 初始化定时器 (新版内核 API: timer_setup)
// 参数:定时器指针,回调函数,标志位(通常0)
timer_setup(&data->my_timer, my_timer_function, 0);
// 4. 注册中断
// 注意:触发方式建议设为 双边沿触发 (RISING | FALLING) 或者 下降沿
// 这样按下和抬起都能被感知到,由定时器去判断稳态
ret = devm_request_irq(&pdev->dev, data->irq_num, my_irq_handler,
IRQF_TRIGGER_FALLING, "my_timer_irq", data);
if (ret) return ret;
dev_info(&pdev->dev, "带消抖的按键驱动加载成功.\n");
return 0;
}
static int my_remove(struct platform_device *pdev)
{
struct my_key_data *data = platform_get_drvdata(pdev);
dev_info(&pdev->dev, "正在卸载...\n");
/*
* 关键:删除定时器
* del_timer_sync 会等待正在其它 CPU 上运行的定时器处理完,
* 确保彻底停止,防止卸载后访问非法内存。
*/
del_timer_sync(&data->my_timer);
return 0;
}
// 适配设备树
static const struct of_device_id my_of_match[] = {
{ .compatible = "learn,my-timer-key" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_key_driver",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
进阶思考:标准做法
虽然亲手实现 GPIO + 中断 + 定时器是极佳的学习路径,但在实际产品开发中,处理按键、触摸屏等输入设备的标准方式是使用 Linux Input Subsystem (输入子系统)。该子系统已经在内核层面封装了中断申请、定时器消抖(开发者通常只需配置 .debounce_interval = 20)以及向用户空间上报标准事件(如 EV_KEY)等一系列复杂操作。本质上,Input 子系统的底层正是封装了我们上面所讨论的 mod_timer 逻辑,这体现了 Linux 内核网络与系统机制中模块化与抽象化的设计思想。