10. input子系统¶
本章的示例代码目录为:base_linux/ev_test
10.1. input子系统¶
input子系统是Linux对输入设备提供的统一驱动框架。 如按键、键盘、触摸屏和鼠标等输入设备的驱动方式是类似的,当出现按键、触摸等操作时, 硬件产生中断,然后CPU直接读取引脚电平,或通过SPI、I2C等通讯方式从设备的寄存器读取具体的按键值或触摸坐标, 然后把这些信息提交给内核。使用input子系统驱动的输入设备可以通过统一的数据结构提交给内核, 该数据结构包括输入的时间、类型、代号以及具体的键值或坐标,而内核则通过/dev/input目录下的文件接口传递给用户空间。
在Linux内核源码的“Documentation/input”目录包含了input子系统相关的说明。
在板卡默认的出厂镜像中,按键、触摸屏、鼠标、键盘都使用了input子系统驱动, 本章将使用按键操作来讲解input子系统驱动相关知识。
10.2. input事件目录¶
10.2.1. 使用evtest工具测试¶
在开发input子系统驱动时,通常会使用evtest工具进行测试, 此处我们将通过该工具来了解板卡上的输入设备。
在Ubuntu主机下使用如下命令测试:
1 2 3 | evtest
#根据自己主机的输出来选择某个设备测试,下图选择的是“4”鼠标
#根据选择的设备测试,如选择的键盘就按键盘,选择鼠标就移动鼠标
|
如下图:
上图的执行过程说明如下:
运行evtest工具,它列出了系统当前可用的/dev/input/event0~4输入事件文件, 并且列出了这些事件对应的设备名。
我们根据设备名的“SIGMACHIP Usb Mouse”推测它就是接入到板卡的鼠标, 所以输入了它对应的event4事件编号4,实验时请根据自己板卡的输出来选择。
输入编号后它列出了event4的一些设备信息,包括驱动版本、设备ID、设备名、 支持的事件类型、事件代号以及输入值的取值范围。
此时移动鼠标,可以看到它输出了详细的事件信息,如果移动后没有输出,说明你选择的不是鼠标设备,
请退出重新选择。输出信息中每一行包含了鼠标上报事件的具体时间time、
事件类型type 2(EV_REL)、事件代号code1或code0(REL_Y或REL_X)和具体的值value, 该值就是鼠标X/Y的坐标。
10.2.2. input事件结构¶
evtest工具的原理并不神秘,学习本章节后也可以尝试自己使用代码实现它的部分功能。 列出可用事件时,它就是通过查看目录“/dev/input/”实现的。 本示例中主机的“/dev/input”目录的内容如下图所示。
可看到“/dev/input”目录下,有各种event设备暴露到用户空间的访问接口文件, 读取这些文件的内容可获取到设备上报的信息。
在前面GPIO子系统中,direction等设备文件直接使用字符串来记录具体的信息, 所以使用cat命令输出文件的内容时,字符串的形式非常方便我们阅读。 但是event文件包含的信息较多,使用字符串不方便其它程序处理, 它采用了纯粹的内核事件数据结构来记录内容,其它程序使用时应把读取到的内容按数据的结构进行格式化转换, 该数据结构定义如下所示。
1 2 3 4 5 6 | struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
|
time:该变量用于记录事件产生的时间戳,即evtest输出的time值。
type:输入设备的事件类型。系统常用的默认类型有EV_KEY、EV_REL和EV_ABS,分别用于表示按键状态改变事件、 相对坐标改变事件及绝对坐标改变事件,特别地,EV_SYN用于分隔事件,无特别意义。 如果选择鼠标(本章第一个图)evtest输出的type类型为EV_ABS。 相关的枚举值可以参考内核文件include/uapi/linux/input-event-codes.h。
code:事件代号,它以更精确的方式表示事件。例如在EV_KEY事件类型中,code的值常用于表示键盘上具体的按键, 其取值范围在0~127之间,例如按键Q对应的是KEY_Q,该枚举变量的值为16。 如果选择鼠标,evtest输出内容的code分别有ABS_X/ABS_Y,表示上报的是X或Y坐标。
value:事件的值。对于EV_KEY事件类型,当按键按下时,该值为1;按键松开时,该值为0。 如果选择鼠标,中evtest输出的内容里,ABS_X事件类型中的value值表示X坐标, ABS_Y类型中的value值表示Y坐标。
如果同样使用cat命令查看事件文件,当事件出现时,cat把内容转化成字符串, 会看到乱码,使用这样的方式可以简单地查看设备是否上报了事件。
可使用以下方式可进行测试:
1 2 3 4 | #根据自己主机上的事件号修改要查看的具体事件文件
#此处使用的event4是本主机的鼠标设备,注意要使用sudo权限
sudo cat /dev/input/event4
#输入命令后移动鼠标,会看到字符
|
如下图;
与其它文件不同,通常cat命令读取文件内容后就会返回,而此处读取event文件时, 命令会持续地等待输入。
1 2 3 4 5 6 7 8 9 10 11 | #根据自己主机上的事件号修改要查看的具体事件文件
#此处使用的event6是本主机的鼠标设备,注意要使用sudo权限
sudo cat /dev/input/event4
#输入命令后移动鼠标,会看到字符
同样,我们还可以使用hexdump命令来查看输出的命令
hexdump /dev/input/event4
二进制的数据可能会因为驱动的不一样而很难对其进行有效的分析
|
10.2.3. input事件设备名¶
“/dev/input/event*”的事件编号与设备的联系不是固定的,它通常按系统检测到设备的先后顺序安排event文件的编号, 这对编写应用程序控制不太方便,我们可以通过“/dev/input/by-id”或“/dev/input/by-path”目录查看具体的硬件设备, 如下图所示:
图中列出了by-path目录下的内容,该目录下的文件实际上都是链接, 如第一行的platform-5311400.ohci3-controller-usb-0:1:1.0-event-mouse -> ../event4表示”platform-5311400.ohci3-controller-usb-0:1:1.0-event-mouse”文件就是event4的快捷方式, 它就是本主机中使用的鼠标,也就是说访问该文件就是访问该鼠标的事件设备, 而且该文件名与硬件的关系是固定的,后面我们的实验将通过该方式进行访问。
由于/dev下的设备都是通过/sys导出的,所以也可以通过“/sys/class/input”目录查看, 如下图所示:
“/sys/class/input”下包含了各个以事件命名的目录,其对应目录下的device/name文件包含了事件对应的设备名, 如本示例中的“/sys/class/input/event4/device/name”文件的内容为”platform-5311400.ohci3-controller-usb-0:1:1.0-event-mouse”, evtest工具列出的事件与设备名的关系,就是从这里读取的。
10.3. 板卡按键检测实验¶
10.3.1. 实验代码分析¶
在输入事件检测的应用中,通常使用主线程直接循环读取“/dev/input/event*”设备文件获取事件的数据结构, 然后通过消息队列通知其它子线程,从而响应输入操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/input-event-codes.h>
//请根据实际情况修改
const char default_path[] = "/dev/input/by-path/platform-5311400.ohci3-controller-usb-0:1:1.0-event-mouse";
int main(int argc, char *argv[])
{
int fd;
struct input_event event;
char *path;
printf("This is a input device demo.\n");
//若无输入参数则使用默认事件设备
if(argc > 1)
path = argv[1];
else
path = (char *)default_path;
fd = open(path, O_RDONLY);
if(fd < 0){
printf("Fail to open device:%s.\n"
"Please confirm the path or you have permission to do this.\n", path);
exit(1);
}
printf("Test device: %s.\nWaiting for input...\n", path);
while(1){
if(read(fd, &event, sizeof(event)) == sizeof(event)){
//EV_SYN是事件分隔标志,不打印
if(event.type != EV_SYN)
printf("Event: time %ld.%ld, type %d, code %d,value %d\n",
event.time.tv_sec,event.time.tv_usec,
event.type,
event.code,
event.value);
}
}
close(fd);
return 0;
}
|
本代码的说明如下:
第11行:定义默认设备路径, 处使用的是鼠标设备在“/dev/input/by-path”下的链接文件名, 此处不使用“/dev/input/event*”只是为了让程序不受其它输入设备而影响了事件编号。
第22~25行:检查main函数的输入参数,若程序执行时带输入参数, 把第1个输入参数作为要打开的事件设备文件路径, 若程序不带参数,则使用上面的默认设备鼠标设备的事件设备文件。
第27~32行:使用O_RDONLY模式打开事件设备文件,O_RDONLY模式默认是阻塞型的, 而且事件设备文件支持阻塞操作,也就是说,若后面使用read函数读取时, 它会等待事件上报,一直等待至读取成功或失败才会返回。
第36行:在while循环里通过read系统调用读取事件文件, 读取到的内容存储在 “struct input_event”类型的event变量中, “struct input_event”类型就是前面介绍的内核事件数据结构。 若成功读取,我们就可以通过该变量的结构体成员访问到事件的时间戳、类型、代号和值。
第41~45行:输出读取到的event变量的各个成员值,在上报的事件中, 通常会有很多类型为EV_SYN的事件,这种事件是用于分隔的, 无特别意义,所以代码中不输出这类型事件的内容。
值得思考的是,若没有上报事件,第36行的read读取事件设备文件操作会被阻塞, 简单来说就是即使第41行的printf代码不注释掉, 它也不会在持续地在循环里输出,而只有当出现了事件, 触发read退出,后面的printf函数才有机会被执行一次, 然后重新read事件再次阻塞。在这种阻塞的过程中,进程会休眠, 释放它对CPU的占用。
假如我们使用的是GPIO子系统框架来编写按键驱动程序,在应用层的操作中, 需要使用“/sys/class/gpio/gpio*/direction”文件配置为输入方向, 然后使用循环读取“/sys/class/gpio/gpio*/value”文件的值来获得按键的状态, 但由于对value文件的read读取操作不会阻塞,所以进程会不停地读取文件内容来判断按键值,占用CPU宝贵的运算资源。
由于read事件文件操作会阻塞,那么采用这种方式就无法同时检测两个输入设备了, 这种时候可以通过select或poll等IO多路复用的操作达成目的, 这在后续的章节再进行讲解。