前言
在上一篇文章中,我们学习了I2C子系统的使用,并驱动了AT24C02 EEPROM。从简单的LED控制到总线设备,我们已经掌握了多种外设的驱动方法。但这些驱动都是"输出型"的------我们向硬件发命令。实际产品中,大量的交互来自输入设备:按键、触摸屏、鼠标、传感器等。
Linux内核为此专门设计了输入子系统(Input Subsystem) ,为所有输入设备提供统一的框架和用户空间接口。本文将以最常见的GPIO按键为例,完整展示一个输入设备驱动的开发过程:从设备树配置、中断申请,到按键事件的上报,最终用户空间可以通过标准的/dev/input/eventX节点读取按键事件。
你将掌握:
- 输入子系统的架构与核心数据结构
- 使用
input_dev注册输入设备 - 通过GPIO中断捕获按键动作
- 使用
input_report_key和input_sync上报事件 - 在用户空间通过
evtest或直接读取input节点验证驱动

一、输入子系统简介
1.1 为什么需要输入子系统?
在没有统一框架的时代,每个输入设备驱动都要自己创建设备节点、定义数据格式、处理应用层的读取逻辑。这导致代码重复、接口不一致。输入子系统则解决了这些问题:
- 统一设备节点 :所有输入设备都在
/dev/input/下,应用程序只需打开/dev/input/eventX即可读取标准化的事件数据。 - 标准事件格式 :使用
struct input_event描述每个输入事件(类型、编码、值),支持键盘、鼠标、触摸屏等多种设备。 - 自动设备发现与热插拔 :输入子系统与
udev配合,自动创建设备文件,用户无需手动mknod。 - 丰富的辅助工具 :
evtest、input-utils等可以方便地测试和调试。
1.2 核心数据结构和API
struct input_dev:代表一个输入设备,包含设备名称、支持的事件类型、事件编码等信息。
常用API:
| 函数 | 作用 |
|---|---|
devm_input_allocate_device(dev) |
分配并初始化input_dev |
set_bit(EV_KEY, input_dev->evbit) |
声明设备支持按键事件 |
set_bit(KEY_ENTER, input_dev->keybit) |
声明支持的具体按键(如KEY_ENTER) |
input_register_device(input_dev) |
向输入子系统注册设备 |
input_report_key(input_dev, keycode, value) |
报告按键状态(1按下,0释放) |
input_sync(input_dev) |
同步事件,表示一次完整上报结束 |
1.3 GPIO按键驱动的一般流程
- 在设备树中描述按键使用的GPIO引脚及有效电平。
- 在
probe中获取GPIO描述符,映射为中断号(gpiod_to_irq)。 - 申请中断,指定中断处理函数。
- 创建
input_dev,设置支持的事件类型和按键码。 - 注册输入设备。
- 中断处理函数中调用
input_report_key上报按键状态,然后调用input_sync通知核心事件完成。
二、设计思路
本文以i.MX6ULL开发板上的一个用户按键(假设为KEY0,连接在GPIO1_IO18)为例。按键一端接GPIO,另一端接地,按下时引脚电平为低。因此:
- 按下:低电平(0)
- 释放:高电平(1)
中断触发方式选择双边沿触发 (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING),这样按下和释放都能产生中断,我们可以在中断中根据当前电平判断状态并上报。
驱动的设备树节点使用自定义的compatible,以便与我们的驱动匹配,不会和内核自带的gpio-keys驱动冲突。
最终生成输入设备节点(例如/dev/input/event1),用户空间可通过evtest或直接读/dev/input/eventX获得按键事件。
三、设备树修改
在板级设备树中添加按键节点。使用GPIO1_IO18(即&gpio1 18),属性名为key-gpios,与驱动中的gpiod_get(dev, "key", ...)对应。
dts
/ {
gpio_key {
compatible = "yourname,gpio-key";
key-gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; /* 低电平有效,按下时逻辑为1 */
status = "okay";
};
};
属性说明:
compatible:自定义字符串,与驱动的of_match_table匹配。key-gpios:指定GPIO引脚,GPIO_ACTIVE_LOW指示逻辑有效电平为低。gpiodAPI会自动处理电平翻转,调用gpiod_get_value时,按下(物理低)返回1,释放(物理高)返回0。
重新编译设备树并替换,重启开发板。
四、驱动代码实现
新建文件 gpio_key_drv.c,完整代码如下。本驱动不包含软件消抖,直接使用硬件消抖配合双边沿中断。
c
/*
* gpio_key_drv.c
* GPIO按键输入设备驱动。
* 基于platform_driver,使用gpiod API和输入子系统上报按键事件。
* 加载后生成 /dev/input/eventX,可通过 evtest 或 cat 读取事件。
* 作者:[你的ID]
* 适配内核:Linux 5.x (4.x 亦可)
* 参考开发板:i.MX6ULL
*/
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h> /* gpiod API */
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/of.h>
static struct input_dev *key_input; /* 输入设备结构体 */
static struct gpio_desc *key_gpio; /* GPIO描述符 */
static int key_irq; /* 中断号 */
/* 中断处理函数(顶半部) */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
int val;
/* 读取当前GPIO逻辑电平(已由gpiod自动处理极性) */
val = gpiod_get_value(key_gpio);
/* 上报按键状态:按下(val=1)报告1,释放(val=0)报告0 */
input_report_key(key_input, KEY_ENTER, val ? 1 : 0);
input_sync(key_input);
pr_info("gpio_key: key %s, val=%d\n", val ? "pressed" : "released", val);
return IRQ_HANDLED;
}
/* ---------------- platform_driver 部分 ---------------- */
static int gpio_key_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
pr_info("gpio_key: probe called\n");
/* 1. 获取GPIO描述符,con_id为"key",对应设备树属性"key-gpios" */
key_gpio = gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(key_gpio)) {
pr_err("gpio_key: failed to get key gpio\n");
return PTR_ERR(key_gpio);
}
/* 2. 将GPIO引脚转换为中断号 */
key_irq = gpiod_to_irq(key_gpio);
if (key_irq < 0) {
pr_err("gpio_key: gpiod_to_irq failed, err=%d\n", key_irq);
ret = key_irq;
goto err_get_irq;
}
pr_info("gpio_key: irq number = %d\n", key_irq);
/* 3. 申请中断(双边沿触发) */
ret = request_irq(key_irq, key_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_key", NULL);
if (ret) {
pr_err("gpio_key: request_irq failed, err=%d\n", ret);
goto err_req_irq;
}
/* 4. 分配并初始化输入设备 */
key_input = devm_input_allocate_device(dev);
if (!key_input) {
pr_err("gpio_key: input_allocate_device failed\n");
ret = -ENOMEM;
goto err_alloc_input;
}
key_input->name = "GPIO Key";
key_input->phys = "gpio_key/input0";
key_input->id.bustype = BUS_HOST;
key_input->id.vendor = 0x0001;
key_input->id.product = 0x0001;
key_input->id.version = 0x0100;
/* 设置支持的按键类型 */
set_bit(EV_KEY, key_input->evbit);
set_bit(KEY_ENTER, key_input->keybit); /* 上报的按键码为KEY_ENTER */
/* 5. 注册输入设备 */
ret = input_register_device(key_input);
if (ret) {
pr_err("gpio_key: input_register_device failed, err=%d\n", ret);
goto err_register_input;
}
pr_info("gpio_key: input device registered as /dev/input/eventX\n");
return 0;
err_register_input:
/* input_allocate_device 分配的内存由devm管理,无需手动释放 */
err_alloc_input:
free_irq(key_irq, NULL);
err_req_irq:
err_get_irq:
gpiod_put(key_gpio);
return ret;
}
static int gpio_key_remove(struct platform_device *pdev)
{
pr_info("gpio_key: remove called\n");
free_irq(key_irq, NULL);
/* devm_input_allocate_device 会自动注销 input_dev,无需手动调用 */
gpiod_put(key_gpio);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "yourname,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "gpio_key",
.owner = THIS_MODULE,
.of_match_table = gpio_key_of_match,
},
};
module_platform_driver(gpio_key_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A GPIO key input device driver");
MODULE_VERSION("1.0");
代码关键点解析:
- gpiod_get(dev, "key", GPIOD_IN) :获取设备树中名为
"key-gpios"的GPIO资源(con_id为"key"),初始化为输入模式。 - gpiod_to_irq :从GPIO描述符获取中断号,无需在设备树中显式声明
interrupts。 - request_irq :触发标志设为
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,保证按下和释放都能产生中断。 - 输入设备设置 :使用
devm_input_allocate_device分配,设置名称和ID,并通过set_bit声明支持按键事件和KEY_ENTER码。 - 中断处理函数 :
gpiod_get_value返回逻辑电平(已自动处理ACTIVE_LOW),直接用作input_report_key的value。按下时报告1,释放时报告0。最后必须调用input_sync表示一次事件结束。 - 资源释放 :
devm_input_allocate_device分配的设备在驱动卸载时自动注销;中断和GPIO在remove中手动释放。probe错误路径使用goto逐级回滚。
关于消抖:本驱动未加入软件消抖,依赖硬件消抖和双边沿中断。如果按键抖动严重导致连续上报,可在中断中加入简易时间滤波(见后续文章)。
五、Makefile
makefile
# Makefile for gpio_key
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := gpio_key_drv.o
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
交叉编译时设置ARCH和CROSS_COMPILE。
六、测试与验证
6.1 加载驱动
bash
insmod gpio_key_drv.ko
dmesg | tail
# gpio_key: probe called
# gpio_key: irq number = xxx
# gpio_key: input device registered as /dev/input/eventX
6.2 确定输入设备节点
驱动加载后,通过以下方式找到对应的eventX:
bash
ls /dev/input/
# 对比加载前后的变化
# 或查看内核信息
dmesg | grep "input: GPIO Key"
# 例如输出:input: GPIO Key as /devices/platform/gpio_key/input/input1
也可用cat /proc/bus/input/devices,找到N: Name="GPIO Key"的行,其后的H: Handlers=...会显示eventX。
6.3 使用evtest测试
bash
evtest /dev/input/event1 # 替换为实际节点
按下和释放按键,终端会输出类似:
Event: time 123456.789012, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 123456.789012, -------------- SYN_REPORT ------------
Event: time 123456.890123, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 123456.890123, -------------- SYN_REPORT ------------
6.4 直接读取event节点
若没有evtest,可用hexdump查看原始事件数据:
bash
hexdump -C /dev/input/event1
按下按键时会输出16字节的行(struct input_event),可对照格式解析时间戳、类型、编码和值。
6.5 卸载驱动
bash
rmmod gpio_key_drv
输入设备节点会自动消失,中断和GPIO被正确释放。
七、常见问题排查
-
insmod后没有生成/dev/input/eventX- 检查
dmesg中是否有input_register_device错误。 - 确保内核配置启用了
CONFIG_INPUT=y和CONFIG_EVDEV=y(通用事件接口)。
- 检查
-
按键事件不产生,中断计数不增加
- 用
cat /proc/interrupts | grep gpio_key查看中断触发次数。 - 检查GPIO引脚是否与原理图一致,
GPIO_ACTIVE_LOW是否正确。 - 确认该GPIO未被他用(
cat /sys/kernel/debug/gpio)。
- 用
-
按键出现多次事件(抖动)
硬件消抖不足时可引入软件消抖,如在中断中记录上次触发时间,小于20ms则丢弃。进阶方法将在下一篇文章中介绍。
-
gpiod_get(dev, "key", ...)失败请确保设备树属性名为
key-gpios,且compatible字符串与驱动一致。
八、总结与下篇预告
本文成功将GPIO按键接入Linux输入子系统,通过标准/dev/input/eventX节点向用户空间上报按键事件。这也是我们首次在驱动中使用中断,中断是嵌入式驱动中最重要的异步通知机制。
下篇预告 :中断处理要求快速完成,耗时操作应推迟到底半部。下一篇我们将深入中断顶半部与底半部 的机制,使用tasklet和工作队列优化按键驱动,并加入软件消抖功能。敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!