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;
}
验证方法
- 编译并加载驱动后,确认
dmesg输出包含key irq driver probed,且/dev/key_irq节点存在。 - 编译并运行
test_key程序,按下并释放按键,观察终端输出。 - 通过条件:每次按下输出一行
key PRESS at <时间戳>,释放输出一行key RELEASE at <时间戳>,且时间戳单调递增。连续快速按动按键不应丢失事件(环形缓冲区容量16,超出时新事件被丢弃属正常)。
常见问题
- 中断未触发:检查GPIO内部下拉是否生效(
GPIOF_INPUT_LOW),以及设备树中key-gpio属性是否与硬件接线一致。 - 事件重复上报:本驱动通过
last_ev.code比较去抖,若硬件抖动严重需在中断处理中增加延时消抖(如使用timer_list延迟采样)。 - read返回EAGAIN:表示当前无按键事件,属正常非阻塞行为,测试程序应循环重试。