
嵌入式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 函数里我们有个循环一直在读 GPIOwhile(1){intstategpiod_get_value(gpio);if(state!last_state){// 状态变了报告事件break;}msleep(10);// 稍微睡一下避免 CPU 占用太高}说实话这种写法有两个大问题。第一即使加了 msleepCPU 占用仍然不低。你想啊每 10ms 就要醒来一次读一次 GPIO做一次比较。如果系统里有很多这样的设备CPU 就被这些无聊的轮询任务占满了。第二响应延迟不可控。如果用户按键刚好在两次轮询之间他得等最长 10ms 才能被检测到。你说 10ms 不长但对于人机交互来说这已经能感觉到迟钝了。中断方式的核心思想中断方式的核心思想其实很简单别主动去问等硬件来通知你。GPIO 可以配置成中断源当它的电平发生变化时会触发一个中断信号。CPU 收到中断信号后暂停当前正在执行的任务跳转到中断处理函数执行。处理完之后再回到原来的任务继续执行。// 配置 GPIO 为中断源intirqgpiod_to_irq(gpio);request_irq(irq,key_irq_handler,IRQF_TRIGGER_FALLING,key,dev);// 中断处理函数staticirqreturn_tkey_irq_handler(intirq,void*dev_id){// 按键状态变了做点什么returnIRQ_HANDLED;}这就像门铃一样的原理。你不需要每隔几秒去门口看看有没有人你只需要等门铃响。门铃响了你再去开门。没响的时候你可以安心做别的事情甚至可以睡觉。::: info 上下半部机制中断处理通常被分为上半部和下半部。上半部就是中断处理函数本身必须快速执行不能睡眠。下半部可以推迟执行可以睡眠。我们的按键驱动用工作队列来实现下半部后面会详细讲。:::中断方式的优势和轮询相比中断方式的优势非常明显特性轮询方式中断方式CPU 占用高持续轮询极低事件驱动响应延迟取决于轮询周期微秒级功耗高CPU 无法深睡低CPU 可深睡消抖效果差在抖动期内可能读到错误状态好延时读取跳过抖动期代码复杂度低中但是等等你可能会说中断方式虽然响应快但按键的机械抖动怎么办按键抖动的真相这是初学者最容易踩的坑。机械按键在按下或松开的瞬间触点不是立即稳定的而是会有一段时间的抖动理想情况 按下 ────────┐ └─────────── 实际情况有抖动 按下 ────────┐┌┌┐┌┐┌─── └┘└┘└┘└ ↑ 抖动期约 5-20ms如果在中断触发时立即读取 GPIO你可能会读到错误的值。更糟糕的是抖动期间会触发多次中断你会收到一堆按下/松开事件。我们的解决方案延时读取消抖的核心思想其实很巧妙不急着读等抖动结束了再读。具体来说当中断触发时我们不立即读取 GPIO 状态而是启动一个延时机制工作队列等 20ms 后再去读。这 20ms 足够让大部分机械按键稳定下来。// 中断处理函数上半部staticirqreturn_tkey_irq_handler(intirq,void*dev_id){schedule_work(dev-work);// 调度工作队列不立即处理returnIRQ_HANDLED;}// 工作队列处理函数下半部staticvoidkey_work_handler(structwork_struct*work){msleep(20);// 等待抖动结束intstategpiod_get_value(gpio);// 读取稳定的状态// 报告事件...}这个方案的妙处在于它利用了工作队列的机制。中断处理函数快速返回只是调度一个工作真正的处理在工作队列里进行可以睡眠可以延时。20ms 后抖动早就结束了读到的就是稳定的按键状态。驱动结构预览在深入各个机制之前我们先看看整个驱动的结构structkey_debounce_dev{/* 字符设备相关 */dev_tdevid;structcdevcdev;structclass*class;structdevice*device;/* 硬件相关 */structgpio_desc*gpio;intirq;/* 工作队列 */structwork_structwork;/* 同步机制 */spinlock_tlock;wait_queue_head_twaitq;/* 状态跟踪 */intlast_gpio_state;bool event_ready;intkey_value;/* 统计信息 */atomic_tirq_count;atomic_tevent_count;atomic_tdebounce_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 可以做别的事情或者进入睡眠。这就是中断方式的魅力所在。