第 9 篇 RK 平台安卓驱动实战 2:中断驱动开发,按键中断的完整实现

目录

开篇先搞懂:中断到底是什么?

大白话定义

为什么要用中断?对比轮询的核心优势

[一、Linux 中断核心原理,小白必懂的知识点](#一、Linux 中断核心原理,小白必懂的知识点)

[1. 中断号:中断的唯一身份证](#1. 中断号:中断的唯一身份证)

[2. 中断的触发方式](#2. 中断的触发方式)

[3. 中断处理的核心机制:上半部与下半部](#3. 中断处理的核心机制:上半部与下半部)

(1)上半部:硬中断处理函数

(2)下半部:延迟处理耗时工作

小白红线警告

二、实战前的硬件准备

硬件接线

三、第一步:设备树配置

[1. 设备树节点](#1. 设备树节点)

[2. 编译烧录验证](#2. 编译烧录验证)

四、第二步:中断驱动内核代码开发

[1. 创建驱动文件](#1. 创建驱动文件)

[2. 完整驱动代码,全注释详解](#2. 完整驱动代码,全注释详解)

[3. 核心知识点讲解](#3. 核心知识点讲解)

[4. 编译驱动,烧录验证](#4. 编译驱动,烧录验证)

[五、第三步:HAL 层适配 + JNI 封装 + 安卓 App 开发](#五、第三步:HAL 层适配 + JNI 封装 + 安卓 App 开发)

[1. HAL 层代码](#1. HAL 层代码)

头文件my_key_hal.h

实现文件my_key_hal.c

[2. JNI 封装](#2. JNI 封装)

[3. 安卓 App 开发](#3. 安卓 App 开发)

核心代码

最终效果

六、小白中断驱动必踩的坑,提前规避

结尾说两句


大家好,我是黒漂技术佬。上一篇我们完成了 GPIO 输入输出驱动的全链路开发,用轮询的方式实现了按键状态的读取。后台有细心的兄弟问:

"佬,轮询读取按键,要一直开个线程循环读,会不会很占用 CPU 资源?有没有更高效的方式?"

问的非常专业!轮询方式确实是最基础的实现,但它有两个致命缺点:一是占用 CPU 资源,二是响应不及时,有延迟。而中断驱动,就是解决这个问题的最优方案,也是 Linux 驱动开发必须掌握的核心技能。

今天这篇,我就用大白话给你讲透 Linux 中断的核心原理,手把手带你完成按键中断驱动的完整开发,实现按键按下 / 松开的时候,内核立刻响应,并且把中断事件实时上报到安卓 App,彻底告别轮询,让你的驱动更高效、更专业。


开篇先搞懂:中断到底是什么?

大白话定义

中断,就是 CPU 正在正常执行程序的时候,突然收到一个来自硬件的紧急信号,CPU 立刻暂停当前正在执行的工作,转而去处理这个紧急事件,处理完成后,再回到之前暂停的地方,继续执行原来的程序。

举个生活中的例子:你正在家里写代码(CPU 正常执行程序),突然快递员敲门(硬件触发中断),你立刻停下手里的代码,去开门收快递(处理中断服务函数),收完快递,回来继续写代码(回到原来的程序继续执行)。

为什么要用中断?对比轮询的核心优势

上一篇我们读取按键状态,用的是轮询方式:开一个线程,每隔 100ms 读一次 GPIO 的电平,判断按键有没有按下。这种方式就像你每隔 1 分钟就跑到门口看看快递员来了没有,不仅浪费时间(占用 CPU),而且快递员来了你也不能第一时间知道(响应延迟)。

而中断方式,就像快递员来了直接敲门,你立刻就知道,不用一直盯着门口。它的核心优势非常明显:

  1. CPU 占用率极低:没有中断触发的时候,CPU 可以正常执行其他任务,完全不用管硬件,只有中断触发的时候,才会去处理,几乎不占用 CPU 资源;
  2. 响应实时性极高:硬件触发中断的瞬间,CPU 就会立刻响应,延迟在微秒级别,完全不会有轮询的延迟问题;
  3. 功耗更低: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_RISING IRQF_TRIGGER_FALLING 双边沿触发 按键按下和松开的瞬间,都会触发中断,我们这次实战就用这个

3. 中断处理的核心机制:上半部与下半部

这是 Linux 中断最核心的设计,也是小白最容易懵的地方,我用大白话给你讲透。

中断处理有一个核心原则:中断处理函数必须尽可能快的执行完成,不能长时间占用 CPU。因为中断触发的时候,CPU 会暂停所有其他工作,如果中断处理函数执行太久,会导致系统响应变慢,甚至丢失其他中断。

但是很多中断事件,需要做的处理工作很多,耗时很长,比如按键中断,我们不仅要读取按键状态,还要做防抖处理,还要把事件上报给用户空间,这些工作如果都放在中断处理函数里,会违反上面的核心原则。

所以 Linux 内核把中断处理分成了两个部分:上半部(Top Half)下半部(Bottom Half)

(1)上半部:硬中断处理函数
  • 就是我们注册的中断服务函数,中断触发的时候,内核会立刻调用这个函数;
  • 这里只能做最紧急、最快的工作,比如:清除中断标志、读取 GPIO 电平、记录中断触发时间,然后立刻调度下半部,就退出了;
  • 执行时间必须极短,微秒级别,绝对不能在里面做耗时操作,绝对不能睡眠!
(2)下半部:延迟处理耗时工作
  • 用来处理上半部剩下的耗时工作,比如按键防抖、数据处理、事件上报给用户空间;
  • 它是在中断处理函数退出后,系统空闲的时候执行的,不会占用硬中断的时间,不会影响系统的实时性;
  • Linux 内核提供了多种下半部的实现机制,入门阶段我们只需要掌握最常用的工作队列(workqueue),它简单易用,支持睡眠,适合绝大多数场景。

小白红线警告

  1. 中断上下文(上半部)里,绝对不能调用会导致睡眠的函数,比如msleep()copy_from_user()copy_to_user(),不然内核直接崩溃;
  2. 中断处理函数必须尽可能短,耗时操作全部放到下半部处理;
  3. 同一个中断号,不能重复注册,注册中断前必须先释放,不然会注册失败。

二、实战前的硬件准备

我们这次的实战目标:

  1. 用 GPIO0_A1 引脚接按键,配置为双边沿触发中断,按键按下和松开的时候,都会触发中断;
  2. 驱动里实现中断上半部 + 下半部,完成按键防抖处理,准确识别按键的按下和松开事件;
  3. 实现中断事件上报机制,把按键事件实时上报给用户空间,安卓 App 能实时收到按键事件,不用轮询。

硬件接线

和上一篇完全一样,不用改接线:

表格

RK3568 开发板引脚 外接硬件 接线说明
GPIO0_A1 按键一端 按键另一端接 GND
GND 按键的 GND 共地

按键按下的时候,GPIO0_A1 引脚接 GND,电平从高变低(下降沿),触发中断;按键松开的时候,引脚恢复上拉高电平,电平从低变高(上升沿),触发中断。


三、第一步:设备树配置

中断驱动的设备树配置,和上一篇 GPIO 的配置基本一样,只需要保证引脚配置为 GPIO 输入模式,上拉即可,不用额外配置中断相关的属性,中断号我们会在驱动里通过 GPIO 编号转换得到。

1. 设备树节点

上一篇我们已经添加过了,这里再贴出来,确认一下:

dts

复制代码
/ {
    my_key_irq: key_irq@0 {
        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. 编译烧录验证

  1. 编译设备树,打包 boot.img,烧录到开发板;

  2. 重启后,验证设备树节点正常生效: bash

    运行

    复制代码
    adb shell
    su
    ls /proc/device-tree/key_irq@0

    能看到节点属性,就说明配置成功了。


四、第二步:中断驱动内核代码开发

我们来写完整的中断驱动代码,实现中断注册、上半部 + 下半部处理、按键防抖、事件上报给用户空间。

1. 创建驱动文件

bash

运行

复制代码
cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers
touch key_irq_drv.c

2. 完整驱动代码,全注释详解

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. 核心知识点讲解

  1. 中断注册与释放
    • gpio_to_irq():把 GPIO 编号转换成对应的中断号;
    • request_irq():向内核注册中断,指定中断号、中断处理函数、触发方式、中断名称;
    • free_irq():释放中断,驱动卸载的时候必须调用;
  2. 上半部 + 下半部实现
    • 上半部key_irq_handler:中断触发时立刻执行,只记录时间,调度工作队列,立刻退出,执行时间极短;
    • 下半部key_work_handler:工作队列处理函数,在系统空闲时执行,完成按键防抖、状态判断、事件写入缓冲区、唤醒等待队列的工作;
  3. 按键防抖处理:机械按键按下和松开的时候,会有 20ms 左右的电平抖动,会导致多次触发中断。我们通过判断两次中断的间隔,小于 20ms 就认为是抖动,直接忽略,保证只触发一次有效事件;
  4. 事件上报机制:等待队列
    • 我们用DECLARE_WAIT_QUEUE_HEAD定义了一个等待队列,用户空间调用read()函数的时候,如果没有事件,就会阻塞在等待队列里,进入休眠,不占用 CPU;
    • 当中断触发,下半部写入新事件后,调用wake_up_interruptible()唤醒等待队列,用户空间的read()函数立刻返回,拿到事件;
    • 这种阻塞等待的方式,完全不占用 CPU 资源,响应实时性极高,是 Linux 驱动事件上报的标准方式。

4. 编译驱动,烧录验证

  1. 修改 Makefile,添加中断驱动的编译: makefile

    复制代码
    obj-y += hello_drv.o
    obj-y += gpio_drv.o
    obj-y += key_irq_drv.o
  2. 编译内核,打包 boot.img,烧录到开发板,重启;

  3. 验证驱动加载成功: bash

    运行

    复制代码
    adb shell
    su
    dmesg | grep key_irq

    能看到「按键中断驱动加载成功」的日志,说明驱动正常加载;

  4. 查看设备文件: bash

    运行

    复制代码
    ls -l /dev/key_irq_drv
    chmod 777 /dev/key_irq_drv
  5. 测试中断功能:执行cat /dev/key_irq_drv,然后按下按键,终端会立刻输出按键事件,松开按键,又会输出一次事件,完全实时响应,说明中断驱动工作正常!


五、第三步:HAL 层适配 + JNI 封装 + 安卓 App 开发

和上一篇的流程完全一样,我们只需要把 HAL 层、JNI、App 的代码,改成适配中断驱动的方式,实现 App 实时接收按键事件,不用轮询。

1. HAL 层代码

头文件my_key_hal.h

c

运行

复制代码
#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.c

c

运行

复制代码
#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. 坑 1:中断注册失败,返回 - EBUSY 这个 GPIO 对应的中断号,已经被其他驱动注册了,同一个中断号不能重复注册。解决方法:cat /proc/interrupts,看哪个驱动占用了你的中断号,在设备树里禁用对应的驱动;
  2. 坑 2:中断能触发,但是多次触发,抖动严重没有做按键防抖处理,机械按键的抖动会导致多次触发中断。解决方法:在下半部里做防抖判断,两次中断间隔小于 20ms 就忽略;
  3. 坑 3:在中断处理函数里做耗时操作,内核崩溃在中断上半部里调用了睡眠函数、耗时函数,违反了中断上下文的规则。解决方法:所有耗时操作全部放到下半部的工作队列里处理,上半部只做最紧急的工作;
  4. 坑 4:中断触发一次后,再也不触发了 中断处理函数返回了IRQ_NONE,而不是IRQ_HANDLED,内核认为这个中断不是你的驱动处理的,会屏蔽这个中断。解决方法:中断处理完成后,必须返回IRQ_HANDLED
  5. 坑 5:用户空间 read 函数不阻塞,一直返回 等待队列的使用错误,没有事件的时候没有阻塞。解决方法:正确使用wait_event_interruptible(),只有事件数量大于 0 的时候,才返回。

结尾说两句

这篇文章,我们彻底搞懂了 Linux 中断的核心原理,完成了按键中断驱动的完整开发,实现了从内核中断处理,到安卓 App 实时接收事件的完整链路。你现在已经掌握了 Linux 驱动开发最核心的中断机制,能写出更高效、更专业的驱动了。

下一篇,我们进入新的实战:PWM 驱动开发,教你怎么用 RK3568 的 PWM 控制器,实现 LED 呼吸灯效果和直流电机调速,并且打通安卓 App 调节 PWM 频率和占空比的全链路。

我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何中断驱动的问题,评论区留言,我都会一一回复。

相关推荐
_muffinman2 小时前
LED点阵8*8驱动开发笔记(Ai8051U单片机)
驱动开发·笔记·单片机
LCMICRO-133108477462 小时前
长芯微LDC64115完全P2P替代AD4115,是一款低功耗、低噪声、24位、Σ-Δ(Σ-Δ)模数转换器(ADC)
stm32·单片机·嵌入式硬件·fpga开发·硬件工程·模数转换器
徐先生 @_@|||2 小时前
AI 大模型编程的软件开发范式:SDD(Specification-Driven Development)模式驱动开发
人工智能·驱动开发
busideyang3 小时前
数据手册和参考手册区别
stm32·单片机·嵌入式硬件·嵌入式
阿拉斯攀登3 小时前
第 14 篇 显示驱动(MIPI/LVDS 屏)适配与调试,DRM 框架详解
android·驱动开发·rk3568·瑞芯微·rk安卓驱动
逐步前行3 小时前
STM32_时钟树_寄存器操作
stm32·单片机·嵌入式硬件
三佛科技-134163842123 小时前
FT8440E 与FT8440S-RT非隔离12V/18V 200MA开关电源芯片区别与联系?
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
同年紀3 小时前
C8051 U-EC6 keil无法连接下载器解决办法
单片机·嵌入式硬件
LCG元3 小时前
STM32项目开发:基于HC-SR04的超声波测距与倒车雷达系统
stm32·单片机·嵌入式硬件