当前位置: 首页 > news >正文

Linux字符设备驱动开发(七):输入子系统——驱动GPIO按键并上报事件

前言

在上一篇文章中,我们学习了I2C子系统的使用,并驱动了AT24C02 EEPROM。从简单的LED控制到总线设备,我们已经掌握了多种外设的驱动方法。但这些驱动都是“输出型”的——我们向硬件发命令。实际产品中,大量的交互来自输入设备:按键、触摸屏、鼠标、传感器等。

Linux内核为此专门设计了输入子系统(Input Subsystem),为所有输入设备提供统一的框架和用户空间接口。本文将以最常见的GPIO按键为例,完整展示一个输入设备驱动的开发过程:从设备树配置、中断申请,到按键事件的上报,最终用户空间可以通过标准的/dev/input/eventX节点读取按键事件。

你将掌握:

  • 输入子系统的架构与核心数据结构
  • 使用input_dev注册输入设备
  • 通过GPIO中断捕获按键动作
  • 使用input_report_keyinput_sync上报事件
  • 在用户空间通过evtest或直接读取input节点验证驱动

一、输入子系统简介

1.1 为什么需要输入子系统?

在没有统一框架的时代,每个输入设备驱动都要自己创建设备节点、定义数据格式、处理应用层的读取逻辑。这导致代码重复、接口不一致。输入子系统则解决了这些问题:

  • 统一设备节点:所有输入设备都在/dev/input/下,应用程序只需打开/dev/input/eventX即可读取标准化的事件数据。
  • 标准事件格式:使用struct input_event描述每个输入事件(类型、编码、值),支持键盘、鼠标、触摸屏等多种设备。
  • 自动设备发现与热插拔:输入子系统与udev配合,自动创建设备文件,用户无需手动mknod
  • 丰富的辅助工具evtestinput-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", ...)对应。

/ { 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,完整代码如下。本驱动不包含软件消抖,直接使用硬件消抖配合双边沿中断。

/* * 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>staticstructinput_dev*key_input;/* 输入设备结构体 */staticstructgpio_desc*key_gpio;/* GPIO描述符 */staticintkey_irq;/* 中断号 *//* 中断处理函数(顶半部) */staticirqreturn_tkey_irq_handler(intirq,void*dev_id){intval;/* 读取当前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);returnIRQ_HANDLED;}/* ---------------- platform_driver 部分 ---------------- */staticintgpio_key_probe(structplatform_device*pdev){intret;structdevice*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");returnPTR_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;gotoerr_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);gotoerr_req_irq;}/* 4. 分配并初始化输入设备 */key_input=devm_input_allocate_device(dev);if(!key_input){pr_err("gpio_key: input_allocate_device failed\n");ret=-ENOMEM;gotoerr_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);gotoerr_register_input;}pr_info("gpio_key: input device registered as /dev/input/eventX\n");return0;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);returnret;}staticintgpio_key_remove(structplatform_device*pdev){pr_info("gpio_key: remove called\n");free_irq(key_irq,NULL);/* devm_input_allocate_device 会自动注销 input_dev,无需手动调用 */gpiod_put(key_gpio);return0;}/* 设备树匹配表 */staticconststructof_device_idgpio_key_of_match[]={{.compatible="yourname,gpio-key"},{}};MODULE_DEVICE_TABLE(of,gpio_key_of_match);staticstructplatform_drivergpio_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 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

交叉编译时设置ARCHCROSS_COMPILE


六、测试与验证

6.1 加载驱动

insmod gpio_key_drv.kodmesg|tail# gpio_key: probe called# gpio_key: irq number = xxx# gpio_key: input device registered as /dev/input/eventX

6.2 确定输入设备节点

驱动加载后,通过以下方式找到对应的eventX

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测试

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查看原始事件数据:

hexdump-C/dev/input/event1

按下按键时会输出16字节的行(struct input_event),可对照格式解析时间戳、类型、编码和值。

6.5 卸载驱动

rmmod gpio_key_drv

输入设备节点会自动消失,中断和GPIO被正确释放。


七、常见问题排查

  1. insmod后没有生成/dev/input/eventX

    • 检查dmesg中是否有input_register_device错误。
    • 确保内核配置启用了CONFIG_INPUT=yCONFIG_EVDEV=y(通用事件接口)。
  2. 按键事件不产生,中断计数不增加

    • cat /proc/interrupts | grep gpio_key查看中断触发次数。
    • 检查GPIO引脚是否与原理图一致,GPIO_ACTIVE_LOW是否正确。
    • 确认该GPIO未被他用(cat /sys/kernel/debug/gpio)。
  3. 按键出现多次事件(抖动)
    硬件消抖不足时可引入软件消抖,如在中断中记录上次触发时间,小于20ms则丢弃。进阶方法将在下一篇文章中介绍。

  4. gpiod_get(dev, "key", ...)失败
    请确保设备树属性名为key-gpios,且compatible字符串与驱动一致。


八、总结与下篇预告

本文成功将GPIO按键接入Linux输入子系统,通过标准/dev/input/eventX节点向用户空间上报按键事件。这也是我们首次在驱动中使用中断,中断是嵌入式驱动中最重要的异步通知机制。

下篇预告:中断处理要求快速完成,耗时操作应推迟到底半部。下一篇我们将深入中断顶半部与底半部的机制,使用tasklet和工作队列优化按键驱动,并加入软件消抖功能。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

http://www.jsqmd.com/news/903225/

相关文章:

  • 风道整流器:5分钟物理改造,实现电脑风冷系统降噪60-90%
  • 深入Power PMAC EtherCAT PDO映射:从自动生成代码到手动精准控制电机
  • 别再死记公式了!用三维动画和几何直觉理解MUSIC/ESPRIT算法的子空间核心
  • Gemini东南亚多语种落地指南:从印尼语方言识别到越南语声调建模的5大关键技术突破
  • 2026郑师傅线下门店全面布局!非遗香品全覆盖,家门口就能体验东方香韵 - 企业推荐官【官方】
  • 别再手动找图了!用ResNet50+LSH快速搭建一个本地图片搜索引擎(附完整代码)
  • 【限时解密】Gemini企业版2024 Q3新增的「合规水印追踪」功能:可溯源每条AI输出至具体租户、时间、操作人,审计留痕达7年
  • Windows内存优化终极指南:Mem Reduct 免费轻量级内存管理神器
  • 实战指南:高效配置通达信缠论分析插件 ChanlunX
  • 3分钟搞定Zotero SciHub插件:终极文献PDF自动下载方案
  • are you close to your cousins
  • 为内部知识库问答机器人接入 Taotoken 以灵活选用性价比模型
  • 如何高效探索Parquet文件:革命性的WebAssembly驱动在线分析工具
  • 90%剪辑师都在用:15个正版版权音乐平台整理
  • Mi-Create:如何用开源工具打造个性化小米手表表盘?
  • 构建自主数字资产智能体:从架构设计到实战优化
  • TinyBERT实战:如何用4层‘小模型’在GLUE的QNLI任务上逼近12层BERT-base?
  • 破解汽配仓储痛点,科捷智能智能工厂一站式赋能方案
  • 极限的和就是和的极限,这个理论如何应用到生活中?股票投资中
  • 别再只盯着SQLmap了!手把手教你用Django的QuerySet方法复现CVE-2022-28346
  • REFramework:如何轻松为RE引擎游戏添加VR支持和脚本功能?实用指南带你高效入门
  • 保姆级教程:用Obi Fluid插件在Unity 2020.2中实现逼真水流效果(附Demo工程)
  • 2026年国内主流醋酸钠厂家实测评测:推荐天津市碧波源科技发展有限公司 - 奔跑123
  • ChanlunX:让缠论分析从复杂理论变为可视化实战工具
  • League Akari:英雄联盟客户端自动化工具完整使用指南
  • 告别环境冲突!用Miniconda在Windows上为PyCharm创建专属Python虚拟环境(保姆级图文)
  • REFramework游戏兼容性深度解析:解决RE引擎游戏崩溃问题的完整方案
  • GetQzonehistory终极指南:5分钟搞定QQ空间数据永久备份
  • 别再死记硬背了!用这4种DDS+PLL组合方案,轻松搞定高精度频率源设计
  • 扬州元点智创GEO联系方式 合作电话 官方网站 官网地址 - 元点智创