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

相关推荐
无限进步_1 小时前
【Linux】系统级文件I/O与文件描述符深度剖析
linux·运维·服务器
放弃 治疗1 小时前
宝塔面板安装 JDK 完整教程|Java 环境配置详解
java·开发语言
ShineWinsu1 小时前
对于Linux:线程局部存储(TLS)和线程封装的解析
linux·c++·面试·线程·tls·线程封装·线程局部存储
工头阿乐1 小时前
使用Conan构建现代C++项目:完整指南
开发语言·c++
2023自学中1 小时前
imx6ull开发板,sd卡启动运行linux,手动给开发板的 emmc 做分区、烧系统
linux·嵌入式·开发板
暮云星影1 小时前
瑞芯微rk3566开发FIT Secure Boot
linux·arm开发·驱动开发·安全
biter down2 小时前
2:Ubuntu 22.04 LTS 的完整下载教程
linux·运维·ubuntu
零陵上将军_xdr2 小时前
为什么DCL单例要加volatile?——CPU乱序执行与内存屏障
java·linux
杨云龙UP2 小时前
Oracle/ODA RAC /u01 空间告警处理指南:grid 用户监听日志清理_2026-06-15
linux·数据库·oracle·oracle linux·oda·监听日志·在线清理