一、什么是input子系统
什么是输入设备?
如键盘,鼠标,触摸屏,游戏手柄
假设我们写一个键盘驱动,创建文件,硬件初始化,实现文件操作集函数,中断等等,非常
麻烦。这些输入设备驱动当中是有共同点的,共同点就是获取数据,上报给用户。所以linux就将通用的代码编写好,将差异化的代码留给驱动驱动工程师,从而出现了inpu子系统
所以:
input系统是 Linux专门为输入类设备编写的一个子系统(框架),从而规范驱动开发,降低
开发难度。并且驱动的通用性兼容性会更好
使用input 子系统前提下:
比如所使用的键盘有不同的厂家,但是这些厂家的键盘连接到电脑上都可以正常
使用,因为这些厂家的驱动都是按照input这个子系统框架来编写的。
总结input 子系统的作用:
1.兼容所有的输入设备
2.统一的驱动编程方式
3.统一的应用操作接口:/dev/input
二、输入设备与节点的对应关系
input子系统管理设备的节点文件在/dev/input/目录下.
根据驱动方式的不同分为3类:
(1)摇杆(joystick)设备 :设备文件名为jsX(X取值范围0~31)。
(2)鼠标(mouse和mice)设备:设备文件名为mouseX(X取值0~30,次设备号32~62)或mice(次设备号为63)
(3)通用的事件类设备(event):键盘、触摸屏等,设备文件名为eventX(X取值:0~31,次设备号为64~95)。
判断哪个节点是对应哪个设备呢?
1.试探性方法:
①使用cat命令打开文件,然后操作物理设备,观察终端是否有输出。
具体命令:sudo cat /dev/input/设备文件名
现象:
②使用sudo hexdump 命令打开/dev/input/下设备文件,然后分别操作各个输入设备,然后分别操作输入设备,如果输出有16进制数,说明此时打开的设备文件就对应于当前操作的输入设备
具体命令:sudo hexdump /dev/input/设备文件名
bexdump 命令简介:
hexdump,.是 Lnux 下的一个二进制文件査看工具,它可以将二进制文件转换为 ASCI1、八进制、
进制、十六进制格式进行查看。
语法:
hexdump [选项][文件]...
常用选项:
-C输出规范的十六进制和 ASCII 码。
-b单字节八进制显示。
-c单字节字符显示。
-d 双字节十进制显示。
-o双字节八进制显示。
-x双字节十六进制显示。
-s从偏移量开始输出。
现象:
2.查看输入设备信息方法
查看开发板的根目录--/proc/bus/input/devices文件,该文件记录了当前系统的所有输入设备的信息。通过文件内容的Name和Handlers来查看设备文件与设备的对应关系。
具体命令:cat/pros/bus/input/devices
例如:
鼠标属性含义:
1: Bus=0003 Vendor=0e0fProduct=0003 Version=0110:总线类型为 0003,供应商ID 为0e0f,
产品ID为0003,版本号为0110。
N: Name="yMware VMware Virtual USB Mouse":设备名称为"VMware yMware Virtual UsB
Mouse".
P:Phys=usb-0000:02:00.0-1/input0:物理路径为 usb-0000:02:00.0-1/input0。
S:
Sysfs=/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-1/2-1:1.0/0003:0E0F:0003.0001/
nput/input5:设备在 sysfs,文件系统中的路径。
U: Unig=:唯一标识符为空。
H:Handlers=mouse2event4:设备处理程序(mouse2和 event4)用于处理鼠标事件。
B: PROP=0:属性值为 0.
B:EV=17:支持的事件类型为 17.
B:KEY=ff00000000:支持的按键类型为ff0000,其他都为0。
B:REL=903:支持的相对位置信息。
B:MSC=10:支持的杂项事件类型为 10。
三、input 子系统的框架
Input子系统包括三个层次,分别是设备驱动层,核心层,事件处理层
为什么要分层呢?
比如我们开发了一个驱动程序 a.c,其中里面有 100 行代码是通用的,然后我又开发了一个驱动程序 b.c,那这 100 行代码就不用在重新写了,所以分层的好处就是不用重复劳动,降低开发难度。所以编写一个输入子系统驱动程序,只需要写设备驱动层,核心层和事件处理层的代码不需要编写。
1、事件处理层
事件通用层代码 evdev.c
事件处理层代码一般不需要我们去编写,内核实现了一个通用的事件处理层代码,对应的设
备节点就是 event0,event1等。但是如有特殊需求,则需要自己编写事件通用层代码,代
码所在路径:drivers/input/evdev.c
核心数据结构->input_handler
内核中基本的形式:
struct input_handler {
void *private;
void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
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);
int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
void (*disconnect)(struct input_handle *handle);
void (*start)(struct input_handle *handle);
bool legacy_minors;
int minor;
const char *name;
const struct input_device_id *id_table;
struct list_head h_list;
struct list_head node;
};
struct input handler 结构体定义在 include/linux/input.h 文件。input handler 结构体中成员:
private:指向私有数据的指针,用于存储特定输入设备驱动程序需要的任何额外信息。
event:处理单个输入事件的函数指针。当输入设备产生一个事件时,该函数将被调用。
events:处理一组输入事件的函数指针。当输入设备产生一组连续的事件时,该函数将被调
用。
filter:过滤输入事件的函数指针。该函数用于决定是否接受或忽略特定类型、代码和值的输
入事件。
match:匹配输入设备的函数指针。用于确定某个输入设备是否适用于当前输入处理程序。
connect:连接输入设备的函数指针。当一个输入设备与输入处理程序建立连接时,该函数将
被调用。
disconnect:断开输入设备连接的函数指针。当一个输入设备与输入处理程序断开连接时,
该函数将被调用。
start:启动输入处理程序的函数指针。当输入处理程序启动时,该函数将被调用。
legacyminors:一个布尔值,表示是否使用传统的次设备号分配方式。
minor: 输入处理程序的次设备号。
name:输入处理程序的名称。
id table:指向输入设备识别标识表的指针。用于指定哪些输入设备适用于该输入处理程序。
hlist: 链表节点,链接该 handler绑定的input handle
node:链表节点,将 handler注册到系统的input handler list 链表
2、设备驱动层
核心数据结构input_dev,主要作用:对输入设备硬件(寄存器)的读写访问,中断设置;把硬件产生的输入信号或数据按照核心层定义规范上报给核心层。好处:向上屏蔽硬件设备的差异。
3、核心层
核心数据结构input_handler,承上启下,向上提供事件处理的接口,向下提供设备驱动层规范接口。作用:将设备驱动层上报的事件传递给事件处理层进行处理。
核心数据结构->input_handle
内核中基本的形式:
struct input_handler {
void *private;
void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
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);
int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
void (*disconnect)(struct input_handle *handle);
void (*start)(struct input_handle *handle);
bool legacy_minors;
int minor;
const char *name;
const struct input_device_id *id_table;
struct list_head h_list;
struct list_head node;
};
关系图:
什么是事件类型,什么是事件子类型?
解决方案:事件类型是大体划分一下,事件子类型是划分区域内找到对应的子事件类型。
四 、设备驱动层
1、核心数据结构->input_dev
内核中基本的形式:
struct input_dev {
struct input_dev{ //描述input设备驱动层的对象
const char *name; //设备名,可以不用实现
const char *phys; //设备在系统层次结构中的路径(位置)
const char *uniq; //设备的唯一识别码
struct input_id 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(REL_CNT)]; //以位图方式记录该设备支持的绝对事件的子类
};
2、设备ID
#include <uapi/linux/input.h>
struct input_id{
__u16 bustype; //设备总线的类型
__u16 vendor; //设备厂商号
__u16 product; //设备产品号
__u16 version; //设备版本号
};
设备总线的类型
#include <uapi/linux/input.h>
#define BUS_PCI 0x01
#define BUS_ISAPNP 0x02
#define BUS_HOST 0x19
#define BUS_I2C 0x18
#define BUS_SPI 0x1C
3、事件类型
evbit[BITS_TO_LONGS(EV_CNT)]包含的事件类型如下,一般来说对应的序号从0开始进行编号—event0。
内核源码里面,有对应的事件类型:
#include <uapi/linux/input-event-codes.h>
#define EV_SYN 0x00 //同步事件,所有的设备都必须支持的事件
#define EV_KEY 0x01 //按键事件,按键类设备
#define EV_REL 0x02 //相对事件,鼠标类设备
#define EV_ABS 0x03 //绝对事件,触摸等
4、按键事件的子类---按键
keybit[BITS_TO_LONGS(KEY_CNT)]包含的按键类型如下,一般内核从0开始进行编号。
#include <uapi/linux/input-event-codes.h>
(1) 标准键盘按键
#define KEY_ESC 1
#define KEY_1 2
....
(2) 鼠标按键
#define BTN_LEFT 0x110
#define BTN_RIGHT 0x111
....
(3) 游戏手柄按键;
#define BTN_JOYSTICK 0x120
#define BTN_TRIGGER 0x120
.....
内核源码的基本形式:
#define BTN_MOUSE 0x110
#define BTN_LEFT 0x110
#define BTN_RIGHT 0x111
#define BTN_MIDDLE 0x112
#define BTN_SIDE 0x113
#define BTN_EXTRA 0x114
#define BTN_FORWARD 0x115
#define BTN_BACK 0x116
#define BTN_TASK 0x117
5、相对事件的子类---相对运动轴
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; //是对应相对事件的子类
一般从0开始进行编号。
#include <uapi/linux/input-event-codes.h>
#define REL_X 0x00
#define REL_Y 0x01
...
#define REL_WHEEL 0x08 //鼠标中间的滚轮
6、绝对事件的子类---绝对运动轴
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; //绝对坐标事件的子类型
一般从0开始进行编号。
#include <uapi/linux/input-event-codes.h>
(1) 单点触控
#define ABS_X 0x00
#define ABS_Y 0x01
#define ABS_Z 0x02
....
#define ABS_PRESSURE 0x18 //压力轴
...
(2) 多点触控(多点触控的电容屏)
.....
#define ABS_MT_POSITION_X 0x35
#define ABS_MT_POSITION_Y 0x36
...
#define ABS_MT_PRESSURE 0x3a
五、相关函数和宏
1、input_dev对象的注册和注销
input_register_device
函数功能:
注册指定的input_dev对象。并且会设置同步事件的支持
将设备添加到内核输入子系统链表,成功后会在 /dev/input/ 生成设备节点(如 event0)。
头文件:
#include <linux/input.h>
函数原型:
int input_register_device(struct input_dev *dev)
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
返回值:
成功:0 失败: 负数错误码
input_unregister_device
函数功能:
注销指定的input_dev对象。
头文件:
#include <linux/input.h>
函数原型:
int input_unregister_device(struct input_dev *dev)
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
返回值:
成功:0 失败: 负数错误码
2、input_dev对象的分配和释放
input_allocate_device
函数功能:
在堆空间动态分配并初始化一个input_dev对象
头文件:
#include <linux/input.h>
函数原型:
struct input_dev* input_allocate_device(void);
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
返回值:
成功:分配得到input_dev对象的首地址
注意:必须以动态分配的方式创建一个input_dev对象,不能以静态定义的方式进行创建。
input_free_device
函数功能:
在堆空间释放指定的input_dev对象
头文件:
#include <linux/input.h>
函数原型:
void input_free_device(struct input_dev *dev);
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
返回值:无
3、事件支持
作用:告诉内核将来会发生对应的事件类型,事件子类型
input_set_capability
函数功能:
设置指定的input_dev对象支持指定的事件类型或子类
头文件:
#include <linux/input.h>
函数原型:
void input_set_capability(struct input_dev *dev,unsigned int type,unsigned int code);
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
unsigned int type:支持的事件类型,Linux内核定义了一系列的事件类型,如EV_KEY(键事件)、EV_REL(相对坐标事件)、EV_ABS(绝对坐标事件)等。每种类型代表了一种输入事件的类别。
unsigned int code:所支持的事件类型对应的子类,如果type是EV_KEY,那么code可以是KEY_A(代表'A'键)、KEY_ENTER(代表回车键)等。事件代码用于唯一标识一个输入事件。
返回值:无
4、事件上报
作用:事件发生了,利用上报函数将对应引脚的状态信息上报给设备节点/dev/input/设备文件名
input_report_key
函数功能:
设置指定的input_dev对象支持指定的事件类型或子类
头文件:
#include <linux/input.h>
函数原型:
void input_report_key(struct input_dev *dev,unsigned int code,int value);
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
unsigned int code:所支持的事件类型对应的子类
int value:按键状态 1和0
返回值:无
input_report_rel
函数功能:
Input_dev对象上报指定的相对运动轴的坐标值
头文件:
#include <linux/input.h>
函数原型:
void input_report_rel(struct input_dev *dev,unsigned int code,int value)
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
unsigned int code:所支持的事件类型对应的子类
int value:相对上次位置变化的坐标值
返回值:无
input_report_abs
函数功能:
Input_dev对象上报指定绝对运动轴的坐标值
头文件:
#include <linux/input.h>
函数原型:
void input_report_rel(struct input_dev *dev,unsigned int code,int value)
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
unsigned int code:所支持的事件类型对应的子类
int value:当前位置的绝对坐标值
当前位置的绝对坐标值返回值:无
input_sync
函数功能:
Input_dev对象上报一个同步事件,表示该完成事件上报结束
头文件:
#include <linux/input.h>
函数原型:
void input_sync(struct input_dev *dev)
函数参数 :
struct input_dev *dev:指定input_dev对象的地址
返回值:无
六、驱动的具体实现
struct input_event 结构体是在Linux系统中用于表示输入事件的一个数据结构,它通常用于处理来自键盘、鼠标、触摸屏等输入设备的事件。
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
这个结构体定义在<linux/input.h>头文件中。下面是对这个结构体各个成员的详细解释:
struct timeval time:
这是一个timeval结构体,表示事件发生的时间。timeval结构体通常包含两个成员:time_t tv_sec(秒)和suseconds_t tv_usec(微秒)。这允许非常精确地记录事件发生的时间点。
__u16 type:
这是一个无符号的16位整数,表示事件的类型。事件的类型决定了后续code和value成员的含义。常见的事件类型包括:
EV_KEY:按键事件
EV_REL:相对坐标事件(如鼠标移动)
EV_ABS:绝对坐标事件(如触摸屏上的触摸点)
EV_SYN:同步事件,表示一系列事件的结束
等等
__u16 code:
这也是一个无符号的16位整数,表示事件的代码。code的值依赖于type成员的值。例如,如果type是EV_KEY,那么code可能表示具体的按键(如KEY_A表示字母A键)。如果type是EV_REL,那么code可能表示鼠标移动的相对方向(如REL_X表示X轴上的移动)。
__s32 value:
这是一个有符号的32位整数,表示事件的值。value的具体含义依赖于type和code成员的值。例如,对于按键事件(EV_KEY),value可能是1(表示按键被按下)或0(表示按键被释放)。对于相对坐标事件(EV_REL),value可能表示移动的距离或方向。
思路:
input子系统执行流程:
一、设备驱动的注册
1、申请input_dev:
使用input_allocate_device函数申请一个input_dev结构体实例。这个结构体用于描述一个输入设备。
2、初始化input_dev:
使用input_set_capability函数设置input_dev的事件类型和事件值,即初始化input_dev结构体中的相关字段,如evbit、keybit等,以指明设备支持哪些类型的事件和具体的事件码。
3、注册input_dev:
使用input_register_device函数将初始化好的input_dev注册到内核中。这一步会将input_dev添加到input核心层维护的全局链表input_dev_list中,并遍历input_handler_list链表,尝试为input_dev匹配并连接合适的input_handler。
二、事件的上报与处理
1、上报输入事件:
当输入设备发生事件时(如按键按下),设备驱动会使用如input_report_key函数上报事件。这个函数需要指定input_dev、事件码(如KEY_A)和事件值(如1表示按下,0表示释放)。
2、事件的处理:
①上报的事件会被input核心层接收,并根据事件的类型和事件码,找到匹配的input_handler进行处理。input_handler是负责具体事件处理的函数或模块,如键盘事件处理模块、鼠标事件处理模块等。
②处理过程可能包括将事件解码、转换格式、传递给用户空间应用程序等步骤。最终,用户空间应用程序可以通过读取设备文件(如/dev/input/eventX)来获取输入事件数据,并作出相应的处理。
应用层的执行流程:
在Linux的input子系统中,当设备驱动上报事件后,用户空间应用程序可以通过以下步骤来获取这些数据:
一、确定设备文件
查看设备节点:
基于input子系统注册成功的设备都会在/dev/input目录下生成对应的设备节点,通常为/dev/input/eventX(其中X是一个数字,按顺序自动生成)。可以通过命令ls /dev/input/来查看当前系统中所有的输入设备节点。
确定目标设备:
根据需要获取数据的输入设备类型(如键盘、鼠标、触摸屏等),找到对应的设备节点。也可以通过查看/proc/bus/input/devices文件来获取系统中注册的所有输入设备的详细信息,并确定目标设备的节点。
二、打开设备文件
使用系统调用open函数打开目标设备文件。例如,如果目标设备节点是/dev/input/event0,则可以使用open("/dev/input/event0", O_RDONLY)来打开它。
打开成功后,open函数会返回一个文件描述符(file descriptor),用于后续的数据读取操作。
三、读取数据
①定义数据结构体:
定义一个struct input_event结构体变量,用于存储从设备文件中读取的输入事件数据。
②发起读操作:
使用系统调用read函数从打开的设备文件中读取数据。read函数的参数包括文件描述符、指向数据缓冲区的指针、以及要读取的字节数sizeof(struct input_event)。
③解析数据:
读取到的数据会被存储在struct input_event结构体变量中。可以解析这个结构体变量的各个成员(如time、type、code、value)来获取具体的输入事件信息。
四、处理数据
根据解析得到的输入事件信息,用户空间应用程序可以执行相应的处理逻辑。例如,如果读取到的是一个按键按下事件,则可以触发相应的按键处理函数。
五、关闭设备文件
当不再需要读取输入事件数据时,使用系统调用close函数关闭打开的设备文件。
注意:
读取输入事件数据是一个持续的过程,因为输入设备可能会不断地产生事件。因此,用户空间应用程序通常会使用一个循环来不断地读取和处理输入事件数据。
应用层代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc,char *argv[])
{
int fd;
struct input_event ev;
//调用一下open函数
fd=open("/dev/input/event4",O_RDWR);
if(fd > 0)
{
printf("open success fd=%d\r\n",fd);
}
while(1)
{
//获取驱动层硬件的电平状态
read(fd,&ev,sizeof(ev));
switch(ev.type)
{
case EV_KEY:
switch(ev.code)
{
case KEY_A:
if(ev.value == 1)
{
printf("key up\r\n");
}
else if(ev.value == 0)
{
printf("key down\r\n");
}
break;
}
break;
}
}
//关闭对应的文件描述符
close(fd);
}
驱动层代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/input.h>
//定义一个key引脚相关的结构体
struct key_init{
int num; //序号
int gpio; //引脚编号
char name[10]; //引脚名称
int irq; //中断号
unsigned long flags;//中断的触发方式
};
//声明变量
static struct key_init keys[]={
{0,5,"key0",0,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING},
{},
};
//定义一个input_dev核心数据结构体变量
struct input_dev* dev_add;
//中断处理函数
static irqreturn_t xxx_handler_key(int irq, void *dev)
{
int s;
//接收一下,传递进来的结构体对象
struct key_init *p =(struct key_init *)dev;
//输出观察一下
printk("p->gpio=%d , p->name=%s\r\n",p->gpio,p->name);
//获取对应引脚的电平状态
s=gpio_get_value(keys[0].gpio); //按下按键的时候,对应的引脚电平是低电平
//输出观察一下
printk("s=%d\r\n",s);
//上报事件
input_report_key(dev_add,KEY_A,s);
//上报同步事件
input_sync(dev_add);
return IRQ_HANDLED;
}
//入口函数
static int __init xxx_init(void)
{
int ret;
//判断对应的引脚是否合法--对应的引脚是否被占用
ret=gpio_is_valid(keys[0].gpio);
printk("ret=%d\r\n",ret);
//注册对应的gpio口
ret=gpio_request(keys[0].gpio,keys[0].name);
printk("ret=%d\r\n",ret);
//获取对应的中断编号
keys[0].irq=gpio_to_irq(keys[0].gpio);
printk("keys[0].irq=%d\r\n",keys[0].irq);
//注册中断API
ret=request_irq(keys[0].irq,xxx_handler_key,keys[0].flags,keys[0].name,&keys[0]);
if(ret == 0)
{
printk("request_irq success\r\n");
}
//申请input_dev
dev_add = input_allocate_device();
if(dev_add != NULL)
{
printk("input_allocate_device success\r\n");
}
//初始化input_dev
input_set_capability(dev_add,EV_KEY,KEY_A);
printk("input_set_capability success\r\n");
//注册input_dev
ret = input_register_device(dev_add);
if(ret == 0)
{
printk("input_register_device success\r\n");
}
return 0;
}
//出口函数
static void __exit xxx_exit(void)
{
//注销input_dev对象
input_unregister_device(dev_add);
printk("input_unregister_device success\r\n");
//释放指定的input_dev对象
input_free_device(dev_add);
printk("input_free_device success\r\n");
//释放中断
free_irq(keys[0].irq,&keys[0]);
printk("free_irq success\r\n");
//释放对应的GPIO口
gpio_free(keys[0].gpio);
printk("gpio_free success\r\n");
}
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
现象: