嵌入式Linux驱动开发------从轮询到中断
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/
今天我们要来点更专业的:中断方式。
轮询的问题在哪里
先让我们回忆一下轮询方式的实现。在 read 函数里,我们有个循环一直在读 GPIO:
c
while (1) {
int state = gpiod_get_value(gpio);
if (state != last_state) {
// 状态变了,报告事件
break;
}
msleep(10); // 稍微睡一下,避免 CPU 占用太高
}
说实话,这种写法有两个大问题。第一,即使加了 msleep,CPU 占用仍然不低。你想啊,每 10ms 就要醒来一次,读一次 GPIO,做一次比较。如果系统里有很多这样的设备,CPU 就被这些无聊的轮询任务占满了。第二,响应延迟不可控。如果用户按键刚好在两次轮询之间,他得等最长 10ms 才能被检测到。你说 10ms 不长?但对于人机交互来说,这已经能感觉到迟钝了。
中断方式的核心思想
中断方式的核心思想其实很简单:别主动去问,等硬件来通知你。
GPIO 可以配置成中断源,当它的电平发生变化时,会触发一个中断信号。CPU 收到中断信号后,暂停当前正在执行的任务,跳转到中断处理函数执行。处理完之后,再回到原来的任务继续执行。
c
// 配置 GPIO 为中断源
int irq = gpiod_to_irq(gpio);
request_irq(irq, key_irq_handler, IRQF_TRIGGER_FALLING, "key", dev);
// 中断处理函数
static irqreturn_t key_irq_handler(int irq, void *dev_id) {
// 按键状态变了,做点什么
return IRQ_HANDLED;
}
这就像门铃一样的原理。你不需要每隔几秒去门口看看有没有人,你只需要等门铃响。门铃响了,你再去开门。没响的时候,你可以安心做别的事情,甚至可以睡觉。
::: info 上下半部机制
中断处理通常被分为"上半部"和"下半部"。上半部就是中断处理函数本身,必须快速执行,不能睡眠。下半部可以推迟执行,可以睡眠。我们的按键驱动用工作队列来实现下半部,后面会详细讲。
:::
中断方式的优势
和轮询相比,中断方式的优势非常明显:
| 特性 | 轮询方式 | 中断方式 |
|---|---|---|
| CPU 占用 | 高(持续轮询) | 极低(事件驱动) |
| 响应延迟 | 取决于轮询周期 | 微秒级 |
| 功耗 | 高(CPU 无法深睡) | 低(CPU 可深睡) |
| 消抖效果 | 差(在抖动期内可能读到错误状态) | 好(延时读取跳过抖动期) |
| 代码复杂度 | 低 | 中 |
但是等等,你可能会说,中断方式虽然响应快,但按键的机械抖动怎么办?
按键抖动的真相
这是初学者最容易踩的坑。机械按键在按下或松开的瞬间,触点不是立即稳定的,而是会有一段时间的抖动:
理想情况:
按下 ────────┐
└───────────
实际情况(有抖动):
按下 ────────┐┌┌┐┌┐┌───
└┘└┘└┘└
↑ 抖动期,约 5-20ms
如果在中断触发时立即读取 GPIO,你可能会读到错误的值。更糟糕的是,抖动期间会触发多次中断,你会收到一堆按下/松开事件。
我们的解决方案:延时读取
消抖的核心思想其实很巧妙:不急着读,等抖动结束了再读。
具体来说,当中断触发时,我们不立即读取 GPIO 状态,而是启动一个延时机制(工作队列),等 20ms 后再去读。这 20ms 足够让大部分机械按键稳定下来。
c
// 中断处理函数(上半部)
static irqreturn_t key_irq_handler(int irq, void *dev_id) {
schedule_work(&dev->work); // 调度工作队列,不立即处理
return IRQ_HANDLED;
}
// 工作队列处理函数(下半部)
static void key_work_handler(struct work_struct *work) {
msleep(20); // 等待抖动结束
int state = gpiod_get_value(gpio); // 读取稳定的状态
// 报告事件...
}
这个方案的妙处在于,它利用了工作队列的机制。中断处理函数快速返回(只是调度一个工作),真正的处理在工作队列里进行,可以睡眠,可以延时。20ms 后,抖动早就结束了,读到的就是稳定的按键状态。
驱动结构预览
在深入各个机制之前,我们先看看整个驱动的结构:
c
struct key_debounce_dev {
/* 字符设备相关 */
dev_t devid;
struct cdev cdev;
struct class* class;
struct device* device;
/* 硬件相关 */
struct gpio_desc* gpio;
int irq;
/* 工作队列 */
struct work_struct work;
/* 同步机制 */
spinlock_t lock;
wait_queue_head_t waitq;
/* 状态跟踪 */
int last_gpio_state;
bool event_ready;
int key_value;
/* 统计信息 */
atomic_t irq_count;
atomic_t event_count;
atomic_t debounce_skipped;
};
这个结构体包含了驱动需要的所有信息。字符设备相关的内容我们在之前的教程里已经讲过了。硬件相关的是 GPIO 描述符和中断号。工作队列用于实现延时处理。同步机制包括自旋锁和等待队列。状态跟踪用于记录按键状态。统计信息用于验证消抖效果。
完整的工作流程
让我们走一遍完整的工作流程,从用户空间到硬件再回到用户空间:
1. 用户空间调用 read()
↓
2. read() 发现没有新事件,调用 wait_event_interruptible() 睡眠
↓
3. 用户按下按键
↓
4. GPIO 电平变化,触发中断
↓
5. 中断处理函数执行(上半部)
- 递增 irq_count 计数器
- 调度工作队列
- 快速返回
↓
6. 20ms 后,工作队列处理函数执行(下半部)
- 读取 GPIO 状态
- 和上一次状态比较
- 如果状态变化,更新 event_ready
- 调用 wake_up_interruptible() 唤醒 read()
↓
7. read() 被唤醒,返回数据给用户空间
整个流程里,CPU 只在两个地方真正干活:中断处理函数(几微秒)和工作队列处理函数(几毫秒)。其他时间,CPU 可以做别的事情,或者进入睡眠。这就是中断方式的魅力所在。