2. 按键输入与中断处理

任务目标

在嵌入式Linux环境中,为GPIO按键实现中断方式的输入驱动。按键按下时产生中断,驱动通过中断处理函数上报按键事件,用户态程序通过读取设备节点获取按键状态。本案例使用树莓派3B+(BCM2837)与GPIO17外接按键,按键另一端接3.3V,按下时GPIO检测到上升沿。

环境与依赖

  • 内核源码树:linux-4.19.y(树莓派官方分支)
  • 交叉编译工具链:arm-linux-gnueabihf-gcc
  • 硬件连接:GPIO17(物理引脚11)接按键一端,按键另一端接3.3V,GPIO17内部下拉使能
  • 设备树:在arch/arm/boot/dts/bcm2710-rpi-3-b.dts中追加按键节点

实现方案

采用Linux字符设备驱动框架,在probe中注册中断(上升沿触发),中断处理函数中使用tasklet或workqueue完成实际事件上报(本案例使用kthread_work以避免在中断上下文直接调用copy_to_user)。驱动维护一个按键事件环形缓冲区,用户态通过read()读取按键事件(按下/释放及时间戳)。

关键完整代码

// key_irq_drv.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/kfifo.h>
#include <linux/timer.h>
#include <linux/of_gpio.h>
#include <linux/kthread.h>

#define KEY_EVENT_MAX 16

struct key_event {
    int code;       // 1=按下, 0=释放
    struct timeval ts;
};

struct key_drv_dev {
    int gpio;
    int irq;
    struct kfifo fifo;
    struct task_struct *task;
    struct kthread_work work;
    struct kthread_worker worker;
    struct key_event last_ev;
};

static struct key_drv_dev *g_dev;

static void key_work_handler(struct kthread_work *work)
{
    struct key_drv_dev *dev = container_of(work, struct key_drv_dev, work);
    int val = gpio_get_value(dev->gpio);
    struct key_event ev;
    ev.code = val ? 1 : 0;
    do_gettimeofday(&ev.ts);
    if (ev.code != dev->last_ev.code) {
        kfifo_in(&dev->fifo, &ev, sizeof(ev));
        dev->last_ev = ev;
    }
}

static irqreturn_t key_irq_handler(int irq, void *data)
{
    struct key_drv_dev *dev = data;
    kthread_queue_work(&dev->worker, &dev->work);
    return IRQ_HANDLED;
}

static ssize_t key_read(struct file *file, char __user *buf,
                        size_t count, loff_t *ppos)
{
    struct key_event ev;
    int ret;
    if (count < sizeof(ev))
        return -EINVAL;
    ret = kfifo_out(&g_dev->fifo, &ev, sizeof(ev));
    if (!ret)
        return -EAGAIN;
    if (copy_to_user(buf, &ev, sizeof(ev)))
        return -EFAULT;
    return sizeof(ev);
}

static const struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .read = key_read,
};

static struct miscdevice key_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "key_irq",
    .fops = &key_fops,
};

static int key_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    int ret;

    g_dev = kzalloc(sizeof(*g_dev), GFP_KERNEL);
    if (!g_dev)
        return -ENOMEM;

    g_dev->gpio = of_get_named_gpio(np, "key-gpio", 0);
    if (!gpio_is_valid(g_dev->gpio)) {
        dev_err(dev, "invalid gpio\n");
        ret = -EINVAL;
        goto err_free;
    }

    ret = devm_gpio_request_one(dev, g_dev->gpio,
                                GPIOF_IN | GPIOF_EXPORT_DIR_FIXED |
                                GPIOF_INPUT_LOW, "key-gpio");
    if (ret) {
        dev_err(dev, "gpio request failed\n");
        goto err_free;
    }

    g_dev->irq = gpio_to_irq(g_dev->gpio);
    if (g_dev->irq < 0) {
        ret = g_dev->irq;
        goto err_free;
    }

    ret = request_irq(g_dev->irq, key_irq_handler,
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                      "key_irq", g_dev);
    if (ret) {
        dev_err(dev, "request_irq failed\n");
        goto err_free;
    }

    kfifo_alloc(&g_dev->fifo, KEY_EVENT_MAX * sizeof(struct key_event),
                GFP_KERNEL);
    kthread_init_worker(&g_dev->worker);
    g_dev->task = kthread_run(kthread_worker_fn, &g_dev->worker,
                              "key_worker");
    if (IS_ERR(g_dev->task)) {
        ret = PTR_ERR(g_dev->task);
        goto err_irq;
    }
    kthread_init_work(&g_dev->work, key_work_handler);
    g_dev->last_ev.code = -1; // 初始无效值

    ret = misc_register(&key_misc);
    if (ret)
        goto err_task;

    platform_set_drvdata(pdev, g_dev);
    dev_info(dev, "key irq driver probed, gpio=%d irq=%d\n",
             g_dev->gpio, g_dev->irq);
    return 0;

err_task:
    kthread_stop(g_dev->task);
err_irq:
    free_irq(g_dev->irq, g_dev);
err_free:
    kfree(g_dev);
    return ret;
}

static int key_remove(struct platform_device *pdev)
{
    struct key_drv_dev *dev = platform_get_drvdata(pdev);
    misc_deregister(&key_misc);
    kthread_flush_worker(&dev->worker);
    kthread_stop(dev->task);
    free_irq(dev->irq, dev);
    kfifo_free(&dev->fifo);
    kfree(dev);
    return 0;
}

static const struct of_device_id key_of_match[] = {
    { .compatible = "example,key-irq" },
    { }
};
MODULE_DEVICE_TABLE(of, key_of_match);

static struct platform_driver key_driver = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "key_irq",
        .of_match_table = key_of_match,
    },
};
module_platform_driver(key_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO Key IRQ Driver");

设备树配置

// 在 bcm2710-rpi-3-b.dts 的根节点下添加
key_irq {
    compatible = "example,key-irq";
    key-gpio = <&gpio 17 GPIO_ACTIVE_HIGH>;
    status = "okay";
};

编译与加载

# 内核模块编译(在内核源码树外)
make -C /path/to/kernel M=$(pwd) modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

# 设备树编译
dtc -I dts -O dtb -o bcm2710-rpi-3-b.dtb bcm2710-rpi-3-b.dts

# 加载模块
insmod key_irq_drv.ko
# 确认设备节点
ls -l /dev/key_irq

用户态测试程序

// test_key.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>

struct key_event {
    int code;
    struct timeval ts;
};

int main(void)
{
    int fd = open("/dev/key_irq", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    struct key_event ev;
    while (1) {
        if (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
            printf("key %s at %ld.%06ld\n",
                   ev.code ? "PRESS" : "RELEASE",
                   ev.ts.tv_sec, ev.ts.tv_usec);
            fflush(stdout);
        }
    }
    close(fd);
    return 0;
}

验证方法

  1. 编译并加载驱动后,确认dmesg输出包含key irq driver probed,且/dev/key_irq节点存在。
  2. 编译并运行test_key程序,按下并释放按键,观察终端输出。
  3. 通过条件:每次按下输出一行key PRESS at <时间戳>,释放输出一行key RELEASE at <时间戳>,且时间戳单调递增。连续快速按动按键不应丢失事件(环形缓冲区容量16,超出时新事件被丢弃属正常)。

常见问题

  • 中断未触发:检查GPIO内部下拉是否生效(GPIOF_INPUT_LOW),以及设备树中key-gpio属性是否与硬件接线一致。
  • 事件重复上报:本驱动通过last_ev.code比较去抖,若硬件抖动严重需在中断处理中增加延时消抖(如使用timer_list延迟采样)。
  • read返回EAGAIN:表示当前无按键事件,属正常非阻塞行为,测试程序应循环重试。