嵌入式Linux驱动开发——从轮询到中断

嵌入式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 可以做别的事情,或者进入睡眠。这就是中断方式的魅力所在。

相关推荐
着迷不白8 小时前
Linux 磁盘管理完全指南:分区、文件系统、挂载与 LVM 实战
linux·运维·服务器
薛定猫AI8 小时前
【深度解析】GLM-5.2 与 Z-Code:AI 编程智能体的原理拆解与 Python 调用实战
开发语言·人工智能·python
没文化的阿浩8 小时前
【C++】详解继承机制
开发语言·c++
山川而川-R8 小时前
LangChain
linux·运维·服务器
QH_ShareHub8 小时前
Linux 根目录结构详解
linux·运维·服务器
文祐8 小时前
C语言用双向链表实现单调递减(递增)队列
c语言·开发语言·链表
雪的季节8 小时前
OCR: windows 环境基于c++的 paddle ocr 编译【CPU版本】编译成dll ,在QT、VS中调用
开发语言
K成长日志8 小时前
BLE协议之设备地址
网络·物联网·网络协议·嵌入式·蓝牙·ble
蒟蒻的贤8 小时前
python搭建MiniDL
开发语言·python·深度学习
aaPIXa6228 小时前
C++模板元编程:在编译期完成计算,实现零运行时开销
java·开发语言·c++