当前位置: 首页 > 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.rkmt.cn/news/1413805.html

相关文章:

  • Gemini东南亚多语种落地指南:从印尼语方言识别到越南语声调建模的5大关键技术突破
  • 为什么你的Gemini始终卡在5%转化率?3个未公开的上下文衰减陷阱正在 silently kill 你的ROI
  • GetQzonehistory终极指南:3分钟学会QQ空间数据安全备份
  • ChanlunX:通达信缠论分析插件终极指南 - 三分钟实现智能缠论可视化
  • 2026年品牌AI搜索可见度监测平台深度测评:搜极星凭什么成为国产AI平台监测首选?
  • 3分钟搞定Zotero SciHub插件:终极文献PDF自动下载方案
  • G-Helper技术深度解析:华硕笔记本性能控制的全新范式
  • 终极指南:用MetPy快速处理气象数据的完整解决方案
  • AI应用的隐私保护:从设计开始的隐私
  • NMPC如何实现自动驾驶漂移控制:模型、算法与工程实践
  • 跟着 MDN 学CSS day_27:(处理不同方向的文本)
  • 别再乱接线了!Arduino Nano + HC-05蓝牙 + DHT11传感器,保姆级避坑指南(附完整代码)
  • 别再死记公式了!用三维动画和几何直觉理解MUSIC/ESPRIT算法的子空间核心
  • 基于ElevenLabs API的AI助手语音合成集成实践
  • 核电厂外来人员无感定位技术方案解析
  • 扬州元点智创GEO联系方式 合作电话 官方网站 官网地址 - 元点智创
  • 上海大模型应用开发公司怎么选:技术路线、费用结构与能力评估全解析
  • 3PEAK思瑞浦 TPA6062-SO1R SOP8 运算放大器
  • Smithbox终极指南:从零掌握魂系列游戏参数与地图编辑
  • Java RPG Maker MV/MZ 解密工具:高效专业的一站式解决方案
  • 长期项目使用Taotoken聚合调用在模型更新与切换上的便利性
  • RPG Maker Decrypter:解锁加密游戏资源的终极免费工具
  • 从忘记压缩包密码到护网演练:一个网络安全爱好者的三年工具进化史
  • Unity 2022 LTS 实战:从零手搓一个带缩放、瞬移和副本地图的完整小地图系统
  • 如何用PrusaSlicer提升3D打印质量:7个实用技巧
  • 深耕垂直赛道工程化落地 集之互动开创AI短剧出海运营新模式
  • 避坑指南:EXT151(QRC)安装后OA库路径报错?看这篇就够了
  • Unity 2D游戏地图效率翻倍:Tilemap高阶技巧与常见坑点全解析(2024版)
  • Epcoritamab 艾可瑞妥单抗治疗复发难治性弥漫大 B 细胞淋巴瘤:皮下给药的疗效数据
  • Taotoken Token Plan套餐如何为高频用户带来显著成本节省