
目录开篇先搞懂中断到底是什么大白话定义为什么要用中断对比轮询的核心优势一、Linux 中断核心原理小白必懂的知识点1. 中断号中断的唯一身份证2. 中断的触发方式3. 中断处理的核心机制上半部与下半部1上半部硬中断处理函数2下半部延迟处理耗时工作小白红线警告二、实战前的硬件准备硬件接线三、第一步设备树配置1. 设备树节点2. 编译烧录验证四、第二步中断驱动内核代码开发1. 创建驱动文件2. 完整驱动代码全注释详解3. 核心知识点讲解4. 编译驱动烧录验证五、第三步HAL 层适配 JNI 封装 安卓 App 开发1. HAL 层代码头文件my_key_hal.h实现文件my_key_hal.c2. JNI 封装3. 安卓 App 开发核心代码最终效果六、小白中断驱动必踩的坑提前规避结尾说两句大家好我是黒漂技术佬。上一篇我们完成了 GPIO 输入输出驱动的全链路开发用轮询的方式实现了按键状态的读取。后台有细心的兄弟问“佬轮询读取按键要一直开个线程循环读会不会很占用 CPU 资源有没有更高效的方式”问的非常专业轮询方式确实是最基础的实现但它有两个致命缺点一是占用 CPU 资源二是响应不及时有延迟。而中断驱动就是解决这个问题的最优方案也是 Linux 驱动开发必须掌握的核心技能。今天这篇我就用大白话给你讲透 Linux 中断的核心原理手把手带你完成按键中断驱动的完整开发实现按键按下 / 松开的时候内核立刻响应并且把中断事件实时上报到安卓 App彻底告别轮询让你的驱动更高效、更专业。开篇先搞懂中断到底是什么大白话定义中断就是 CPU 正在正常执行程序的时候突然收到一个来自硬件的紧急信号CPU 立刻暂停当前正在执行的工作转而去处理这个紧急事件处理完成后再回到之前暂停的地方继续执行原来的程序。举个生活中的例子你正在家里写代码CPU 正常执行程序突然快递员敲门硬件触发中断你立刻停下手里的代码去开门收快递处理中断服务函数收完快递回来继续写代码回到原来的程序继续执行。为什么要用中断对比轮询的核心优势上一篇我们读取按键状态用的是轮询方式开一个线程每隔 100ms 读一次 GPIO 的电平判断按键有没有按下。这种方式就像你每隔 1 分钟就跑到门口看看快递员来了没有不仅浪费时间占用 CPU而且快递员来了你也不能第一时间知道响应延迟。而中断方式就像快递员来了直接敲门你立刻就知道不用一直盯着门口。它的核心优势非常明显CPU 占用率极低没有中断触发的时候CPU 可以正常执行其他任务完全不用管硬件只有中断触发的时候才会去处理几乎不占用 CPU 资源响应实时性极高硬件触发中断的瞬间CPU 就会立刻响应延迟在微秒级别完全不会有轮询的延迟问题功耗更低CPU 不用一直轮询可以进入休眠状态只有中断触发的时候才唤醒对于电池供电的设备能极大降低功耗。在嵌入式开发里几乎所有的外设按键、传感器、触摸屏、串口、网络都是用中断的方式来实现事件响应的中断驱动是嵌入式工程师必须掌握的核心技能。一、Linux 中断核心原理小白必懂的知识点1. 中断号中断的唯一身份证和 GPIO 一样每个中断都有一个唯一的编号叫中断号IRQ 号内核通过中断号来区分不同的中断找到对应的中断处理函数。对于 GPIO 引脚触发的中断我们可以通过gpio_to_irq()函数把 GPIO 编号转换成对应的中断号不用自己手动查。2. 中断的触发方式中断的触发方式就是什么情况下硬件会触发中断对于 GPIO 中断有 5 种常用的触发方式表格触发方式含义按键场景用法IRQF_TRIGGER_RISING上升沿触发电平从低变高的时候触发中断对应按键松开的瞬间IRQF_TRIGGER_FALLING下降沿触发电平从高变低的时候触发中断对应按键按下的瞬间IRQF_TRIGGER_HIGH高电平触发电平保持高电平的时候触发中断IRQF_TRIGGER_LOW低电平触发电平保持低电平的时候触发中断IRQF_TRIGGER_RISINGIRQF_TRIGGER_FALLING双边沿触发按键按下和松开的瞬间都会触发中断我们这次实战就用这个3. 中断处理的核心机制上半部与下半部这是 Linux 中断最核心的设计也是小白最容易懵的地方我用大白话给你讲透。中断处理有一个核心原则中断处理函数必须尽可能快的执行完成不能长时间占用 CPU。因为中断触发的时候CPU 会暂停所有其他工作如果中断处理函数执行太久会导致系统响应变慢甚至丢失其他中断。但是很多中断事件需要做的处理工作很多耗时很长比如按键中断我们不仅要读取按键状态还要做防抖处理还要把事件上报给用户空间这些工作如果都放在中断处理函数里会违反上面的核心原则。所以 Linux 内核把中断处理分成了两个部分上半部Top Half和下半部Bottom Half。1上半部硬中断处理函数就是我们注册的中断服务函数中断触发的时候内核会立刻调用这个函数这里只能做最紧急、最快的工作比如清除中断标志、读取 GPIO 电平、记录中断触发时间然后立刻调度下半部就退出了执行时间必须极短微秒级别绝对不能在里面做耗时操作绝对不能睡眠2下半部延迟处理耗时工作用来处理上半部剩下的耗时工作比如按键防抖、数据处理、事件上报给用户空间它是在中断处理函数退出后系统空闲的时候执行的不会占用硬中断的时间不会影响系统的实时性Linux 内核提供了多种下半部的实现机制入门阶段我们只需要掌握最常用的工作队列workqueue它简单易用支持睡眠适合绝大多数场景。小白红线警告中断上下文上半部里绝对不能调用会导致睡眠的函数比如msleep()、copy_from_user()、copy_to_user()不然内核直接崩溃中断处理函数必须尽可能短耗时操作全部放到下半部处理同一个中断号不能重复注册注册中断前必须先释放不然会注册失败。二、实战前的硬件准备我们这次的实战目标用 GPIO0_A1 引脚接按键配置为双边沿触发中断按键按下和松开的时候都会触发中断驱动里实现中断上半部 下半部完成按键防抖处理准确识别按键的按下和松开事件实现中断事件上报机制把按键事件实时上报给用户空间安卓 App 能实时收到按键事件不用轮询。硬件接线和上一篇完全一样不用改接线表格RK3568 开发板引脚外接硬件接线说明GPIO0_A1按键一端按键另一端接 GNDGND按键的 GND共地按键按下的时候GPIO0_A1 引脚接 GND电平从高变低下降沿触发中断按键松开的时候引脚恢复上拉高电平电平从低变高上升沿触发中断。三、第一步设备树配置中断驱动的设备树配置和上一篇 GPIO 的配置基本一样只需要保证引脚配置为 GPIO 输入模式上拉即可不用额外配置中断相关的属性中断号我们会在驱动里通过 GPIO 编号转换得到。1. 设备树节点上一篇我们已经添加过了这里再贴出来确认一下dts/ { my_key_irq: key_irq0 { compatible my-key,irq; status okay; key-gpio gpio0 RK_PA1 GPIO_ACTIVE_LOW; pinctrl-names default; pinctrl-0 key_irq_pins; }; pinctrl { key_irq { key_irq_pins: key-irq-pins { rockchip,pins 0 RK_PA1 RK_FUNC_GPIO pcfg_pull_up; }; }; }; };核心要点引脚配置为上拉输入模式保证按键松开的时候引脚是稳定的高电平按下的时候是低电平。2. 编译烧录验证编译设备树打包 boot.img烧录到开发板重启后验证设备树节点正常生效bash运行adb shell su ls /proc/device-tree/key_irq0能看到节点属性就说明配置成功了。四、第二步中断驱动内核代码开发我们来写完整的中断驱动代码实现中断注册、上半部 下半部处理、按键防抖、事件上报给用户空间。1. 创建驱动文件bash运行cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers touch key_irq_drv.c2. 完整驱动代码全注释详解c运行#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #include linux/device.h #include linux/gpio.h #include linux/of_gpio.h #include linux/platform_device.h #include linux/interrupt.h #include linux/workqueue.h #include linux/poll.h #include linux/wait.h // 驱动信息声明 MODULE_LICENSE(GPL); MODULE_AUTHOR(黒漂技术佬); MODULE_DESCRIPTION(RK3568 Android Key Interrupt Driver); MODULE_VERSION(1.0); // 宏定义 #define DEVICE_NAME key_irq_drv #define CLASS_NAME key_irq_class #define KEY_EVENT_MAX 16 // 按键事件结构体 struct key_event { int state; // 0按下1松开 unsigned long time; // 触发时间 }; // 全局变量 static dev_t key_devno; static struct cdev key_cdev; static struct class *key_class; static struct device *key_device; static int key_gpio; // 按键GPIO编号 static int irq_num; // 中断号 static struct workqueue_struct *key_wq; // 工作队列 static struct work_struct key_work; // 工作结构体 static DECLARE_WAIT_QUEUE_HEAD(key_wait_q); // 等待队列用于事件上报 static struct key_event event_buf[KEY_EVENT_MAX]; // 事件缓冲区 static int event_rp 0; // 读指针 static int event_wp 0; // 写指针 static int event_count 0; // 事件数量 static int key_state 1; // 按键当前状态1松开0按下 static unsigned long last_irq_time 0; // 上次中断触发时间用于防抖 // 下半部工作队列处理函数 // 这里处理耗时的防抖、事件上报工作在系统空闲时执行 static void key_work_handler(struct work_struct *work) { int current_state; unsigned long now jiffies; // 1. 读取当前GPIO电平确认按键状态 current_state gpio_get_value(key_gpio); printk(【key_irq】下半部处理当前电平%d\n, current_state); // 2. 防抖处理两次中断间隔小于20ms认为是抖动忽略 if (now - last_irq_time msecs_to_jiffies(20)) { printk(【key_irq】按键抖动忽略\n); return; } // 3. 状态没有变化忽略 if (current_state key_state) { return; } // 4. 更新按键状态 key_state current_state; printk(【key_irq】按键状态变化%s\n, key_state 0 ? 按下 : 松开); // 5. 把事件写入缓冲区 if (event_count KEY_EVENT_MAX) { event_buf[event_wp].state key_state; event_buf[event_wp].time jiffies_to_msecs(now); event_wp (event_wp 1) % KEY_EVENT_MAX; event_count; } // 6. 唤醒等待队列通知用户空间有新事件 wake_up_interruptible(key_wait_q); } // 上半部中断处理函数 // 中断触发时内核立刻调用这个函数必须快不能做耗时操作 static irqreturn_t key_irq_handler(int irq, void *dev_id) { // 1. 记录中断触发时间 last_irq_time jiffies; // 2. 调度工作队列把耗时工作交给下半部处理 queue_work(key_wq, key_work); // 3. 返回中断处理完成 return IRQ_HANDLED; } // 字符设备核心函数 static int key_open(struct inode *inode, struct file *filp) { printk(【key_irq】设备被打开\n); return 0; } // read函数用户空间读取按键事件没有事件的时候会阻塞等待 static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { int ret; struct key_event event; // 如果没有事件阻塞等待直到有新事件 wait_event_interruptible(key_wait_q, event_count 0); // 从缓冲区读取一个事件 event event_buf[event_rp]; event_rp (event_rp 1) % KEY_EVENT_MAX; event_count--; // 把事件拷贝到用户空间 ret copy_to_user(buf, event, sizeof(struct key_event)); if (ret) { printk(【key_irq】事件拷贝到用户空间失败\n); return -EFAULT; } return sizeof(struct key_event); } // poll函数支持非阻塞IO给上层select/epoll用 static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait) { unsigned int mask 0; poll_wait(filp, key_wait_q, wait); // 如果有事件返回可读 if (event_count 0) { mask | POLLIN | POLLRDNORM; } return mask; } static int key_release(struct inode *inode, struct file *filp) { printk(【key_irq】设备被关闭\n); return 0; } // file_operations结构体 static const struct file_operations key_fops { .owner THIS_MODULE, .open key_open, .read key_read, .poll key_poll, .release key_release, }; // platform驱动框架 static int key_irq_probe(struct platform_device *pdev) { int ret; printk(【key_irq】驱动和设备树匹配成功\n); // 1. 从设备树获取GPIO编号 key_gpio of_get_named_gpio(pdev-dev.of_node, key-gpio, 0); if (!gpio_is_valid(key_gpio)) { dev_err(pdev-dev, 获取按键GPIO失败\n); return -EINVAL; } // 2. 申请GPIO设置为输入模式 ret gpio_request(key_gpio, key_irq); if (ret) { dev_err(pdev-dev, GPIO申请失败\n); return ret; } gpio_direction_input(key_gpio); // 3. 把GPIO编号转换成中断号 irq_num gpio_to_irq(key_gpio); if (irq_num 0) { dev_err(pdev-dev, GPIO转换中断号失败\n); ret irq_num; goto err_gpio_free; } dev_info(pdev-dev, 按键GPIO%d对应中断号%d\n, key_gpio, irq_num); // 4. 创建工作队列 key_wq create_singlethread_workqueue(key_irq_wq); if (!key_wq) { dev_err(pdev-dev, 创建工作队列失败\n); ret -ENOMEM; goto err_gpio_free; } // 初始化工作结构体绑定处理函数 INIT_WORK(key_work, key_work_handler); // 5. 注册中断双边沿触发 ret request_irq(irq_num, key_irq_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, key_irq, pdev-dev); if (ret) { dev_err(pdev-dev, 中断注册失败错误码%d\n, ret); goto err_wq_destroy; } dev_info(pdev-dev, 中断注册成功\n); // 6. 注册字符设备 ret alloc_chrdev_region(key_devno, 0, 1, DEVICE_NAME); if (ret 0) { dev_err(pdev-dev, 设备号申请失败\n); goto err_irq_free; } cdev_init(key_cdev, key_fops); key_cdev.owner THIS_MODULE; ret cdev_add(key_cdev, key_devno, 1); if (ret 0) { dev_err(pdev-dev, 字符设备注册失败\n); goto err_devno_free; } key_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(key_class)) { ret PTR_ERR(key_class); dev_err(pdev-dev, 设备类创建失败\n); goto err_cdev_del; } key_device device_create(key_class, NULL, key_devno, NULL, DEVICE_NAME); if (IS_ERR(key_device)) { ret PTR_ERR(key_device); dev_err(pdev-dev, 设备创建失败\n); goto err_class_destroy; } dev_info(pdev-dev, 按键中断驱动加载成功\n); return 0; // 错误处理 err_class_destroy: class_destroy(key_class); err_cdev_del: cdev_del(key_cdev); err_devno_free: unregister_chrdev_region(key_devno, 1); err_irq_free: free_irq(irq_num, pdev-dev); err_wq_destroy: destroy_workqueue(key_wq); err_gpio_free: gpio_free(key_gpio); return ret; } static int key_irq_remove(struct platform_device *pdev) { printk(【key_irq】驱动开始卸载\n); // 释放所有资源 device_destroy(key_class, key_devno); class_destroy(key_class); cdev_del(key_cdev); unregister_chrdev_region(key_devno, 1); free_irq(irq_num, pdev-dev); destroy_workqueue(key_wq); gpio_free(key_gpio); dev_info(pdev-dev, 按键中断驱动卸载成功\n); return 0; } // 设备树匹配表 static const struct of_device_id key_irq_of_match[] { { .compatible my-key,irq }, { /* 结束 */ } }; MODULE_DEVICE_TABLE(of, key_irq_of_match); static struct platform_driver key_irq_driver { .probe key_irq_probe, .remove key_irq_remove, .driver { .name key_irq_driver, .of_match_table key_irq_of_match, }, }; // 驱动入口和出口 static int __init key_irq_drv_init(void) { printk(【key_irq】按键中断驱动开始加载\n); return platform_driver_register(key_irq_driver); } static void __exit key_irq_drv_exit(void) { platform_driver_unregister(key_irq_driver); } module_init(key_irq_drv_init); module_exit(key_irq_drv_exit);3. 核心知识点讲解中断注册与释放gpio_to_irq()把 GPIO 编号转换成对应的中断号request_irq()向内核注册中断指定中断号、中断处理函数、触发方式、中断名称free_irq()释放中断驱动卸载的时候必须调用上半部 下半部实现上半部key_irq_handler中断触发时立刻执行只记录时间调度工作队列立刻退出执行时间极短下半部key_work_handler工作队列处理函数在系统空闲时执行完成按键防抖、状态判断、事件写入缓冲区、唤醒等待队列的工作按键防抖处理机械按键按下和松开的时候会有 20ms 左右的电平抖动会导致多次触发中断。我们通过判断两次中断的间隔小于 20ms 就认为是抖动直接忽略保证只触发一次有效事件事件上报机制等待队列我们用DECLARE_WAIT_QUEUE_HEAD定义了一个等待队列用户空间调用read()函数的时候如果没有事件就会阻塞在等待队列里进入休眠不占用 CPU当中断触发下半部写入新事件后调用wake_up_interruptible()唤醒等待队列用户空间的read()函数立刻返回拿到事件这种阻塞等待的方式完全不占用 CPU 资源响应实时性极高是 Linux 驱动事件上报的标准方式。4. 编译驱动烧录验证修改 Makefile添加中断驱动的编译makefileobj-y hello_drv.o obj-y gpio_drv.o obj-y key_irq_drv.o编译内核打包 boot.img烧录到开发板重启验证驱动加载成功bash运行adb shell su dmesg | grep key_irq能看到「按键中断驱动加载成功」的日志说明驱动正常加载查看设备文件bash运行ls -l /dev/key_irq_drv chmod 777 /dev/key_irq_drv测试中断功能执行cat /dev/key_irq_drv然后按下按键终端会立刻输出按键事件松开按键又会输出一次事件完全实时响应说明中断驱动工作正常五、第三步HAL 层适配 JNI 封装 安卓 App 开发和上一篇的流程完全一样我们只需要把 HAL 层、JNI、App 的代码改成适配中断驱动的方式实现 App 实时接收按键事件不用轮询。1. HAL 层代码头文件my_key_hal.hc运行#ifndef MY_KEY_HAL_H #define MY_KEY_HAL_H #ifdef __cplusplus extern C { #endif // 按键事件结构体和驱动里的一致 struct key_event { int state; unsigned long time; }; // 阻塞读取按键事件有事件才返回 int key_read_event(struct key_event *event); #ifdef __cplusplus } #endif #endif实现文件my_key_hal.cc运行#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include my_key_hal.h #define DEVICE_PATH /dev/key_irq_drv static int fd -1; static int key_dev_init(void) { if (fd 0) { fd open(DEVICE_PATH, O_RDWR); if (fd 0) { printf(【key_hal】打开设备文件失败\n); return -1; } } return 0; } // 阻塞读取按键事件没有事件会一直等待 int key_read_event(struct key_event *event) { int ret; if (key_dev_init() 0) return -1; ret read(fd, event, sizeof(struct key_event)); if (ret ! sizeof(struct key_event)) { printf(【key_hal】读取事件失败\n); return -1; } return 0; }2. JNI 封装java运行package com.heipiao.keyirqdemo; public class KeyJni { static { System.loadLibrary(key_jni); } public native int readKeyEvent(KeyEvent event); // 按键事件类和驱动里的对应 public static class KeyEvent { public int state; public long time; } }cpp运行#include jni.h #include my_key_hal.h extern C JNIEXPORT jint JNICALL Java_com_heipiao_keyirqdemo_KeyJni_readKeyEvent(JNIEnv *env, jobject thiz, jobject event) { struct key_event ev; int ret key_read_event(ev); if (ret 0) { // 把C结构体的值赋值给Java对象 jclass cls env-GetObjectClass(event); jfieldID state_field env-GetFieldID(cls, state, I); jfieldID time_field env-GetFieldID(cls, time, J); env-SetIntField(event, state_field, ev.state); env-SetLongField(event, time_field, ev.time); } return ret; }3. 安卓 App 开发我们写一个 App开启一个子线程阻塞读取按键事件收到事件后立刻更新 UI显示按键状态和触发时间完全不用轮询。核心代码java运行package com.heipiao.keyirqdemo; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private KeyJni keyJni; private TextView tvKeyState, tvEventTime; private Handler handler; private boolean isRunning true; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); keyJni new KeyJni(); handler new Handler(Looper.getMainLooper()); tvKeyState findViewById(R.id.tv_key_state); tvEventTime findViewById(R.id.tv_event_time); // 开启子线程阻塞读取按键事件 new Thread(() - { KeyJni.KeyEvent event new KeyJni.KeyEvent(); while (isRunning) { // 这里会阻塞直到有按键事件 int ret keyJni.readKeyEvent(event); if (ret 0) { // 收到事件更新UI handler.post(() - { if (event.state 0) { tvKeyState.setText(按键状态按下); } else { tvKeyState.setText(按键状态松开); } tvEventTime.setText(触发时间 event.time ms); }); } } }).start(); } Override protected void onDestroy() { super.onDestroy(); isRunning false; } }最终效果把 App 安装到开发板上打开 App按下按键App 立刻显示「按键状态按下」同时显示触发时间松开按键App 立刻显示「按键状态松开」实时响应完全没有延迟整个过程App 的线程大部分时间都在阻塞休眠完全不占用 CPU 资源效率极高。六、小白中断驱动必踩的坑提前规避坑 1中断注册失败返回 - EBUSY这个 GPIO 对应的中断号已经被其他驱动注册了同一个中断号不能重复注册。解决方法cat /proc/interrupts看哪个驱动占用了你的中断号在设备树里禁用对应的驱动坑 2中断能触发但是多次触发抖动严重没有做按键防抖处理机械按键的抖动会导致多次触发中断。解决方法在下半部里做防抖判断两次中断间隔小于 20ms 就忽略坑 3在中断处理函数里做耗时操作内核崩溃在中断上半部里调用了睡眠函数、耗时函数违反了中断上下文的规则。解决方法所有耗时操作全部放到下半部的工作队列里处理上半部只做最紧急的工作坑 4中断触发一次后再也不触发了中断处理函数返回了IRQ_NONE而不是IRQ_HANDLED内核认为这个中断不是你的驱动处理的会屏蔽这个中断。解决方法中断处理完成后必须返回IRQ_HANDLED坑 5用户空间 read 函数不阻塞一直返回等待队列的使用错误没有事件的时候没有阻塞。解决方法正确使用wait_event_interruptible()只有事件数量大于 0 的时候才返回。结尾说两句这篇文章我们彻底搞懂了 Linux 中断的核心原理完成了按键中断驱动的完整开发实现了从内核中断处理到安卓 App 实时接收事件的完整链路。你现在已经掌握了 Linux 驱动开发最核心的中断机制能写出更高效、更专业的驱动了。下一篇我们进入新的实战PWM 驱动开发教你怎么用 RK3568 的 PWM 控制器实现 LED 呼吸灯效果和直流电机调速并且打通安卓 App 调节 PWM 频率和占空比的全链路。我是黒漂技术佬关注我带你零基础入门 RK 安卓驱动开发不踩坑。有任何中断驱动的问题评论区留言我都会一一回复。