输入设备介绍
鼠标、键盘、按键、触摸屏等提供输入支持的设备都属于输入设备,在Linux也提供了一套驱动框架“input 子系统”与之对应,用于抽象输入设备,并提供管理输入设备驱动和输入事件处理程序的功能
input 子系统
input 子系统用于管理各种输入设备的驱动程序和各种输入事件的事件处理程序,input 子系统分为3层:
- 输入设备驱动层:包含各种各样的输入设备驱动程序,用于驱动输入设备。
- input 核心层:承上起下,用于管理输入设备驱动程序和输入事件处理程序,并提供驱动层和事件处理层相互匹配、相互通信的功能。
- 输入事件处理层:包含各种各样的输入事件处理程序,用于对输入设备的硬件进行了抽象,以便应用层能更方便的和输入设备进行交互,其中 evdev 是一个通用输入事件处理程序,它能与所有输入设备进行匹配,此外对于鼠标、键盘、触摸屏等常用设备内核中也有相应的输入事件驱动程序。
注意:输入设备和输入事件驱动是多对多的关系,即一个输入设备可能有多个输入事件处理程序,一个输入事件处理程序也可能有多个输入设备
struct input_dev 对象
input_dev 对象表示一个 input 设备,它的核心成员如下:
//输入设备的名字,通过命令 cat /proc/bus/input/devices 可以查看const char *name;//在系统层次结构中设备的物理路径,通过命令 cat /proc/bus/input/devices 可以查看const char *phys;//设备的唯一识别码,通过命令 cat /proc/bus/input/devices 可以查看const char *uniq;//设备ID,包含总线类型、作者、产品和版本相关消息,它用于输入设备和事件处理程序间的匹配,此外它通过命令 cat /proc/bus/input/devices 可以查看struct input_id id;//设备属性位图,记录输入设备的一些属性unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];//输入设备支持的事件类型位图,表示输入设备可以支持哪些事件unsigned long evbit[BITS_TO_LONGS(EV_CNT)];//表示相应事件可以上报的事件码位图//按键事件unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//相对坐标事件unsigned long relbit[BITS_TO_LONGS(REL_CNT)];//绝对坐标事件unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];//其他事件unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];//led事件unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];//声音事件unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];//压力反馈事件unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];//压力状态事件unsigned long swbit[BITS_TO_LONGS(SW_CNT)];//触摸屏相关,是一个动态分配的数组,用于存储触摸点的状态struct input_mt *mt;//操作函数,一般由事件处理程序调用,用于控制输入设备,如 evdev 事件处理程序则是通过字符设备接口开放到应用层,供应用层进行调用int (*open)(struct input_dev *dev);void (*close)(struct input_dev *dev);int (*flush)(struct input_dev *dev, struct file *file);int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);//使用 input_handle 中的 h_node 成员构成的链表, Linux 通过 input_handle 对象关联输入设备和输入事件处理程序struct list_head h_list;
事件码、事件类型、设备属性定义参考 input_event_codes.h 文件,可以通过函数 void __set_bit (int nr, volatile void *addr) 直接设置位图中的某一位,也可以通过函数 void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code) 函数完成事件码位图和事件类型位图的设置。
当设备支持重复事件后内核会为其开启一个定时器生成重复事件,无需设备驱动层重复上报。
注册和注销输入设备
注册输入设备:
- 分配输入设备;使用函数 struct input_dev *input_allocate_device(void)
- 初始化 input_dev 对象,主要是设置设备属性、支持的事件类型、对应的事件码
- 注册输入设备;使用函数 int input_register_device(struct input_dev *dev)
注销输入设备: - 注销输入设备;使用函数 void input_unregister_device(struct input_dev *dev)
- 释放输入设备;使用函数 void input_free_device(struct input_dev *dev)
struct input_handler 对象
input_handler 对象表示一个输入事件处理程序,其核心成员如下:
//事件处理函数,用于处理输入设备上报的事件void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);//事件处理函数,用于处理输入设备上报的事件,可处理多个事件,未定义时系统默认使用event实现void (*events)(struct input_handle *handle, const struct input_value *vals, unsigned int count);//事件过滤函数,用于处理输入设备上报的事件bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);//自定义匹配函数,当系统匹配函数执行成功后才会调用此函数bool (*match)(struct input_handler *handler, struct input_dev *dev);//用于建立事件处理程序和输入设备的联系,通过调用 input_register_handle 实现int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);//用于断开事件处理程序和输入设备的联系,通过调用 input_unregister_handle 实现void (*disconnect)(struct input_handle *handle);//事件处理程序和输入设备建立联系后执行void (*start)(struct input_handle *handle);//名字const char *name;//设备匹配表,用于与输入设备匹配const struct input_device_id *id_table;//使用 input_handle 中的 d_node 成员构成的链表, Linux 通过 input_handle 对象关联输入设备和输入事件处理程序struct list_head h_list;
注册和注销输入事件处理程序
注册输入事件处理程序
- 初始化 input_handler 对象,主要包括各种事件处理函数、输入设备驱动关联函数、设备匹配表等
- 使用 int input_register_handler(struct input_handler *handler) 函数注册输入事件处理程序
注销输入事件处理程序 - 使用 void input_unregister_handler(struct input_handler *handler) 注销输入事件处理程序
struct input_handle 对象
input_handle 用于关联输入设备和事件处理程序,其核心成员如下:
//关联的输入设备struct input_dev *dev;//关联的事件处理程序struct input_handler *handler;//用于构建输入设备驱动程序中的 h_list 链表struct list_head d_node;//用于构建输入事件处理程序中的 h_list 链表struct list_head h_node;
建立或断开输入设备和输入事件处理程序的联系
建立输入设备驱动程序和输入事件处理程序的联系:
- 在注册输入设备驱动过的程中会遍历 input_handler_list 并调用 input_attach_handler 进行输入设备驱动程序和输入事件处理程序的匹配,同样在注册输入事件处理程序的过程中也会遍历 input_dev_list 并调用 input_attach_handler 进行输入设备驱动程序和输入事件处理程序的匹配(利用输入设备驱动程序的 id 和输入事件处理程序的 id_table 进行匹配)。
- 默认匹配函数执行成功后会调用输入事件处理程序提供的 match 函数
- 匹配完成后会调用事件处理程序提供 connect 函数
- 在 connect 函数中分配并初始化 input_handle 对象(这里需要使用 input_get_device 进行获取输入设备)
- 使用 input_register_handle 建立输入设备和输入事件处理程序的联系
断开输入设备和输入事件处理程序的联系 - 当注销输入设备驱动程序或输入事件处理程序时会遍历其 h_list (管理的 input_handle 对象)链表,执行输入事件处理程序的 disconnect 函数
- 在 disconnect 函数中使用 input_unregister_handle 断开输入设备和输入事件处理程序的联系(这里还需要对输入设备进行 put_device 操作)
输入设备上报事件
通过函数 input_event 可以上报输入设备产生的输入事件:
/*** dev 需要上报的 input_dev。* type 上报的事件类型,比如 EV_KEY。* code 事件码,如 KEY_0、 KEY_1 等等。* value 事件值,如按键事件 1 表示按键按下, 0 表示按键松开。*/void input_event(struct input_dev *dev,unsigned int type,unsigned int code,int value)
另外针对常见类型的事件还有专门的事件上报函数,这些函数都是通过 input_event 函数进行了二次封装实现,如 input_report_key 可用于上报按键事件
事件同步
输入设备事件上报完成后需要调用 input_sync 告诉内核此次上报结束:
/*** 需要上报同步事件的 input_dev。*/void input_sync(struct input_dev *dev)
输入事件处理程序下发事件
输入事件处理程序可以通过 input_inject_event 下发事件给输入设备,它最终会调用到输入设备驱动的 event 函数,如 evdev 驱动在 connect 中注册了一个字符设备,并在字符设备的 write中调用了此函数,使得应用层可以向输入设备驱动程序发送事件。
/*** handle 需要下发事件的 input_handle (在 input_handle 关联了事件处理程序和输入设备)* type 下发的事件类型* code 事件码* value 事件值*/void input_inject_event(struct input_handle *handle, unsigned int type, unsigned int code, int value)
在应用层通过evdev访问输入设备
evdev 是一个通用的输入事件处理程序,它能与所有输入设备进行匹配,在匹配成功后会创建一个字符设备,自然应用层也可以通过它创建的字符设备访问到对应的输入设备,其步骤如下:
- 包含头文件 #include <linux/input.h>。
- 打开设备(evdev 支持阻塞、非阻塞、 IO 多路复用、异步通知等机制, IO 多路复用、异步通知建议采用非阻塞方式打开,避免 IO 错误或连续读写时导致程序意外阻塞)。
- 通过 ioctl 获取设备信息,控制命令即参数参考内核中的 \drivers\input\evdev.c 中的 evdev_do_ioctl 函数。
- 通过 read 读取输入设备上报的事件,通过 write 发送事件给输入设备(如控制LED等)。
- 使用完成关闭输入设备。
通过read函数读取到的是一些列的输入事件,其数据类型如下:
struct timeval {__kernel_time_t tv_sec;__kernel_suseconds_ tv_usec;};struct input_event {//事件上报时间struct timeval time;//表示哪类事件,比如 EV_KEY 表示按键类、EV_REL 表示相对位移,EV_ABS 表示绝对位置__u16 type;//表示该类事件下的事件码,比如对于 EV_KEY 类事件它表示按键值,对于触摸屏它表示是 X 还是Y ,或是压力值__u16 code;//表示事件值,对于按键它表示按键状态,对于触摸屏,它表示坐标值 x 值或 y 值或压力值__s32 value;};
在读取据时,可以得到一个或多个数据(比如一个触摸屏的一个触点会上报 X 、Y 位置信息,还可能会上报压力值), 应用层可通过同步事件确定此次上报是否结束,对于同步事件其 type 、 code 、 value 三项都是 0
确定设备信息
通过命令 cat /proc/bus/input/devices 可以查看输入设备的详细信息,其内容格式如下:
//设备 IDI: Bus=0019 Vendor=0000 Product=0001 Version=0000//设备名称N: Name="Power Button"//系统层次结构中设备的物理路径P: Phys=LNXPWRBN/button/input0//位于 sys 文件系统的路径S: Sysfs=/devices/LNXSYSTM:00/LNXPWRBN:00/input/input0//设备的唯一标识码U: Uniq=//与输入设备关联的输入事件处理程序H: Handlers=kbd event0//设备属性B: PROP=0//设备支持的事件类型B: EV=3//可上报的按键位图B: KEY=10000000000000 0I: Bus=0011 Vendor=0001 Product=0001 Version=ab41N: Name="AT Translated Set 2 keyboard"P: Phys=isa0060/serio0/input0S: Sysfs=/devices/platform/i8042/serio0/input/input1U: Uniq=H: Handlers=sysrq kbd event1 ledsB: PROP=0B: EV=120013B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe//设备支持的其他事件B: MSC=10//设备上的指示灯B: LED=7......
使用命令读取数据
调试输入设备时,可使用 hexdump 读取输入设备上报的数据,其数据格式如下:
|序号 | |秒 | |微秒 | |type| |code| |value |0000340 0000 0000 0000 0000 508b 62dd 0000 00000000350 a54d 0009 0000 0000 0003 0000 9d3f 00000000360 508b 62dd 0000 0000 a54d 0009 0000 0000
GPIO按键输入驱动
原理图
硬件原理图如下所示,在引脚PG3上接了一个按键KEY0,按下按键时PG3为低电平,松开按键时PG3为高电平
设备树编写
设备树如下所示:
intr_key {compatible = "intr_key";status = "okay";key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;interrupt-parent = <&gpiog>;interrupts = <3 IRQ_TYPE_EDGE_BOTH>;};
驱动代码编写
驱动代码主要包括以下部分:
- 分配、初始化、注册输入设备
//分配输入设备key->input_dev = devm_input_allocate_device(&pkey_dev->dev);if(!key->input_dev){printk("alloc input device failed");return -ENOMEM;}//设置输入设备名称key->input_dev->name = "virtual_input_Device";//设置事件类型,按键事件、重复事件__set_bit(EV_KEY, key->input_dev->evbit);__set_bit(EV_REP, key->input_dev->evbit);//设置事件码__set_bit(KEY_0, key->input_dev->keybit);//注册输入设备result = input_register_device(key->input_dev);if(result){printk("register input device failed");return result;}
- 延时消抖定时器注册,在按下按键时可能会产生抖动,导致误触发,我们可以等其稳定后在来读取按键值
//初始化消抖定時器timer_setup(&key->timer, timer_func, 0);
- 设置GPIO为输入,获取并注册GPIO的中断处理函数
//获取中断号key->irq = of_irq_get(pkey_dev->dev.of_node, 0);if(key->irq <= 0){input_unregister_device(key->input_dev);printk("irq get failed");return key->irq;}//获取中断触发方式irq_flags = irq_get_trigger_type(key->irq);if(irq_flags == IRQF_TRIGGER_NONE)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;irq_flags |= IRQF_SHARED;//注册中断result = devm_request_irq(&pkey_dev->dev, key->irq, key_handler, irq_flags, "key", (void*)key);if(result != 0){input_unregister_device(key->input_dev);printk("request irq failed\r\n");return result;}
- 在中断函数中设置消抖定时器触发时间,然后再消抖定时器函数中上报按键状态
static irqreturn_t key_handler(int irq, void *dev)
{struct key_handle *key = (struct key_handle *)dev;//设置定时器到期时间mod_timer(&key->timer, jiffies + msecs_to_jiffies(10));//返回IRQ_HANDLED,表示中断被成功处理return IRQ_HANDLED;
}static void timer_func(struct timer_list *tm)
{struct key_handle *key = container_of(tm, struct key_handle, timer);//上报GPIO状态input_report_key(key->input_dev, KEY_0, gpiod_get_value(key->gpio));//上报完成input_sync(key->input_dev);
}
完整的按键输入驱动程序如下所示:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/input.h>struct key_handle {//GPIO描述符struct gpio_desc *gpio;//GPIO中断号unsigned int irq;//软件定时器,用于消抖struct timer_list timer;//输入设备struct input_dev *input_dev;
};static irqreturn_t key_handler(int irq, void *dev)
{struct key_handle *key = (struct key_handle *)dev;//设置定时器到期时间mod_timer(&key->timer, jiffies + msecs_to_jiffies(10));//返回IRQ_HANDLED,表示中断被成功处理return IRQ_HANDLED;
}static void timer_func(struct timer_list *tm)
{struct key_handle *key = container_of(tm, struct key_handle, timer);//上报GPIO状态input_report_key(key->input_dev, KEY_0, gpiod_get_value(key->gpio));//上报完成input_sync(key->input_dev);
}static int key_probe(struct platform_device *pkey_dev)
{int result;uint32_t irq_flags;struct key_handle *key;printk("%s\r\n", __FUNCTION__);//分配设备句柄,devm表示模块卸载时自动释放key = devm_kzalloc(&pkey_dev->dev, sizeof(struct key_handle), GFP_KERNEL);if(!key){printk("alloc memory failed\r\n");return -ENOMEM;}//复位key设备句柄memset(key, 0, sizeof(struct key_handle));//分配输入设备key->input_dev = devm_input_allocate_device(&pkey_dev->dev);if(!key->input_dev){printk("alloc input device failed");return -ENOMEM;}//设置输入设备名称key->input_dev->name = "virtual_input_Device";//设置事件类型,按键事件、重复事件__set_bit(EV_KEY, key->input_dev->evbit);__set_bit(EV_REP, key->input_dev->evbit);//设置事件码__set_bit(KEY_0, key->input_dev->keybit);//注册输入设备result = input_register_device(key->input_dev);if(result){printk("register input device failed");return result;}//初始化消抖定時器timer_setup(&key->timer, timer_func, 0);//获取GPIO,并设置为输入(devm表示模块卸载时自动释放)key->gpio = devm_gpiod_get_index(&pkey_dev->dev, "key", 0, GPIOD_IN);if(IS_ERR(key->gpio)){input_unregister_device(key->input_dev);printk("get gpio failed\r\n");return PTR_ERR(key->gpio);}//获取中断号key->irq = of_irq_get(pkey_dev->dev.of_node, 0);if(key->irq <= 0){input_unregister_device(key->input_dev);printk("irq get failed");return key->irq;}//获取中断触发方式irq_flags = irq_get_trigger_type(key->irq);if(irq_flags == IRQF_TRIGGER_NONE)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;irq_flags |= IRQF_SHARED;//注册中断result = devm_request_irq(&pkey_dev->dev, key->irq, key_handler, irq_flags, "key", (void*)key);if(result != 0){input_unregister_device(key->input_dev);printk("request irq failed\r\n");return result;}//设置平台设备的驱动私有数据pkey_dev->dev.driver_data = (void*)key;return 0;
}static int key_remove(struct platform_device *pkey_dev)
{struct key_handle *key;printk("%s\r\n", __FUNCTION__);//提取平台设备的驱动私有数据key = (struct key_handle*)pkey_dev->dev.driver_data;//注销输入设备input_unregister_device(key->input_dev);return 0;
}/* 匹配列表,用于设备树和驱动进行匹配 */
static struct of_device_id key_of_match[] = {{.compatible = "intr_key"},{},
};static struct platform_driver key_drv = { .driver = {.name = "intr_key",.of_match_table = key_of_match,},.probe = key_probe,.remove = key_remove,
};static int __init mykey_init(void)
{return platform_driver_register(&key_drv);
}static void __exit mykey_exit(void)
{platform_driver_unregister(&key_drv);
}module_init(mykey_init);
module_exit(mykey_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("csdn");
MODULE_DESCRIPTION("key test");
MODULE_ALIAS("key_input");
驱动测试程序
按键驱动加载成功后会与event事件处理程序匹配,event事件处理程序会在/dev/input/目录中创建一个event*的设备文件,通过此设备文件可以读取到按键驱动上报的数据,其测试代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>int main(int argc, char *argv[])
{int fd, ret;struct input_event ev;if(argc < 2){printf("Usage:\r\n\t./keyinputApp /dev/input/eventX @ Open Key\r\n");return -1;}//打开输入设备fd = open(argv[1], O_RDWR);if(0 > fd){printf("Error: file %s open failed!\r\n", argv[1]);return -1;}while(1){//读取输入设备ret = read(fd, &ev, sizeof(struct input_event));if (ret){//处理输入事件switch (ev.type){/* 按键事件 */case EV_KEY:if (KEY_0 == ev.code){/* 判断是不是 KEY_0 按键 */if (ev.value){/* 按键按下 */ printf("Key0 Press\n");}else{/* 按键松开 */ printf("Key0 Release\n");}}break;/* 重复事件 */case EV_REL:if (KEY_0 == ev.code){/* 按键一直按下 */printf("Key0 repetition\n");}break;/* 其他类型的事件,自行处理 */case EV_ABS:break;case EV_MSC:break;case EV_SW:break;};}else{printf("Error: file %s read failed!\r\n", argv[1]);break;}}/* 关闭设备 */close(fd);return 0;
}
上机测试
- 根据硬件原理图对设备树进行修改,然后用命令make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs -j8编译设备树,并用新的设备树启动目标板。
- 从这里下载代码并进行编译,然后将编译出来的.ko文件和.out文件拷贝到目标板根文件系统的root目录中。
- 执行命令insmod key.ko加载按键驱动,此时通过命令cat /proc/bus/input/devices可以看到按键的基本信息
- 执行命令./app.out /dev/input/event0运行测试程序,测试程序会读取按键状态并输出,在按下或松开时终端都会输出相应字符串,长按时还会重复输出按下字符串。