文章目录
- 1- input子系统介绍
- 2- input事件目录
- (1)struct input_event 结构体
- (2)type(事件类型):
- (3)code(事件编码)
- (4)value(事件的值)
- 3- input事件设备名
- (1)查看event编号对应的硬件设备方法一
- (2)查看event编号对应的硬件设备方法二(按键检测)
- 4- 编程进行按键测试
- (1)源码
- (2)linux运行
- (3)开发板运行
1- input子系统介绍
Input子系统是Linux对输入设备提供的统一驱动框架。如按键、键盘、触摸屏和鼠标等输入设备的驱动
方式是类似的,当出现按键、触摸等操作时,硬件产生中断,然后CPU直接读取引脚电平,或通过SPI、
I2C等通讯方式从设备的寄存器读取具体的按键值或触摸坐标,然后把这些信息提交给内核。
使用Input子系统驱动的输入设备可以通过统一的数据结构提交给内核,该数据结构包括输入的时间、
类型、代号以及具体的键值或坐标,而内则通过/dev/input目录下的文件接口传递给用户空间。
Linux系统下的输入系统框架如下图所示:
事件传送的方向:硬件驱动层–>子系统核心–>事件处理层–>用户空间
-
==硬件驱动层:==输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容
-
==子系统核心层:==承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理,链接其他两个层之间的纽带与桥梁, 向下提供驱动层的接口, 向上提供事件处理层的
接口。 -
==事件处理层:==主要和用户空间进行交互,将硬件驱动层传来的事件报告给用户程序
假设用户程序直接访问 /dev/input/event1 设备节点,或者使用tslib访问触摸屏设备节点,数据的流向
如下: -
应用程序open()打开输入设备文件后调用read()读数据,此时输入设备没有事件发生,则读不到数
据阻塞; -
用户操作设备(如按下设备、点击触摸屏),硬件上产生中断;
-
输入系统驱动层对应的驱动程序处理中断:读取到数据,转换为标准的输入事件(一个struct
input_event结构体),向核心层汇报。 -
核心层可以决定把输入事件转发给上面哪个handler来处理。从handler的名字来看,它就是用来处
输入操作的。比如:evdev_handler、kbd_handler、joydev_handler等。 -
他们作用就是把核心层的数据返回给正在读取的APP,当APP正在调用read()系统调用等待数据
时,evdev_handler会把它唤醒,这样APP就可以返回数据。
2- input事件目录
各层之间通信的基本单位是事件, 任何一个输入设备的动作都可以抽象成一种事件, 如键盘的按下,触摸屏的按下, 鼠标的移动等。
事件有三种属性: 类型(type), 编码(code),值(value), input 子系统支持的所有事件都定义在 input.h 中, 包括所有支持的类型, 所属类型支持的编码等。
应用程序空间在从input设备read()读取数据时,它的每个数据元素是 struct input_event(一个事件) 结构体类型,该结构体在Linux内核源码中其定义在include/uapi/linux/input.h
文件中,而应用程序空间则定义
在 /usr/include/linux/input.h
文件中。
(1)struct input_event 结构体
该结构体的定义如下:
struct input_event {
struct timeval time;//该变量用于记录事件产生的时间戳。表示“自系统启动以来过了多少时间”,由秒和微秒
(long 类型 32bit)组成。
__u16 type;//类型
__u16 code;//编码
__s32 value;//值
};
typedef long __kernel_long_t;
typedef __kernel_long_t __kernel_old_time_t;
typedef __kernel_long_t __kernel_suseconds_t;
//Linux内核源码: include/uapi/linux/time.h
//应用编程头文件: /usr/include/linux/time.h
struct timeval {
__kernel_old_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
(2)type(事件类型):
输入设备的事件类型。系统常用的默认类型有EV_KEY、 EV_REL和EV_ABS,分别用于表示按键状态改变事件、相对坐标改变事件及绝对坐标改变事件。其类型定义如下:
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */
(3)code(事件编码)
表示该类事件下的哪一个事件。例如 在EV_KEY事件类型中,code的值常用于表示键盘上具体的按键,比如数字键1、2、3,字母键A、B、C里等。查看定义:
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
#define KEY_0 11
......
#define BTN_TRIGGER_HAPPY39 0x2e6
#define BTN_TRIGGER_HAPPY40 0x2e7
(4)value(事件的值)
对于EV_KEY事件类型,当按键按下时,该值为1;按键松开时,该值为0。
3- input事件设备名
查看==/dev/input==可以看到很多event*节点,事件编号与设备的联系不是固定的,它通常按系统检测到设
备的先后顺序安排event文件的编号。例如:在IGKBoard开发板上查看/dev/input文件夹下,有2个
event事件编号。
该目录下的文件实际上都是链接,如event1对应的就是访问开发板的按键的事件设备。
root@igkboard:~# ls /dev/input/
by-path event0 event1
下面提供2个方法查看event编号对应的具体的硬件设备:
(1)查看event编号对应的硬件设备方法一
查看==/proc/bus/input/devices==文件查看事件编号对应的具体的硬件设备
root@igkboard:~# cat /proc/bus/input/devices
I: Bus=0019 Vendor=0000 Product=0000 Version=0000
N: Name="20cc000.snvs:snvs-powerkey"
P: Phys=snvs-pwrkey/input0
S: Sysfs=/devices/platform/soc/2000000.bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0
U: Uniq=
H: Handlers=kbd event0
B: PROP=0
B: EV=3
B: KEY=100000 0 0 0
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="keys"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/keys/input/input1
U: Uniq=
H: Handlers=kbd event1
B: PROP=0
B: EV=100003
B: KEY=10000000
可以看到keys对应的就是event1(其H: Handlers=kbd event1 ),下面是每一个设备信息中的I、N、
P、S、U、H、B对应的含义:
- I:设备ID(id of the device)
- N:设备名称(name of the device)
- P:系统层次结构中设备的物理路径(physical path to the device in the system hierarchy)
- S:文件系统的路径(sysfs path 位于sys)
- U:设备的唯一标识码(unique identification code for the device)
- H:与设备关联的输入句柄列表(list of input handles associated with the device)
- B:位图(bitmaps)
B位图(bitmaps) :
PROP:设备属性
EV:设备支持的事件类型
KEY:此设备具有的键/按钮
MSC:设备支持的其他事件
LED:设备上的指示灯
我们来解释一下第一个设备中的“B:EV=3”:3的二进制是110,即bit1,bit2使能,表示设备支持1,2这两类事件,通过查看struct input_event中type的事件类型可以知道是EV_KEY 0x01 /* 按键事件 */ 以及EV_REL 0x02 /* 相对坐标事件 */。
(2)查看event编号对应的硬件设备方法二(按键检测)
使用evtest工具查看事件编号对应的具体的硬件设备。
在开发input子系统驱动时,常常会使用 evtest 工具进行测试,它列出了系统当前可用的/dev/input/event0~1输入事件文件,并且列出了这些事件对应的设备名。
具体操作:evtest执行命令,因为我这个开发板event1对应的就是访问开发板的按键的事件设备,所以我输入1,然后按住用户按键然后松手,就会打印相关信息。
具体如下所示:
root@igkboard:~# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0: 20cc000.snvs:snvs-powerkey
/dev/input/event1: keys
Select the device event number [0-1]: 1
Input driver version is 1.0.1
Input device ID: bus 0x19 vendor 0x1 product 0x1 version 0x100
Input device name: "keys"
Supported events:
Event type 0 (EV_SYN)
Event type 1 (EV_KEY)
Event code 28 (KEY_ENTER)
Key repeat handling:
Repeat type 20 (EV_REP)
Repeat code 0 (REP_DELAY)
Value 250
Repeat code 1 (REP_PERIOD)
Value 33
Properties:
Testing ... (interrupt to exit)
Event: time 1677379264.035611, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 1677379264.035611, -------------- SYN_REPORT ------------
Event: time 1677379264.224893, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 1677379264.224893, -------------- SYN_REPORT ------------
SYN_REPORT即为同步事件。同步事件用于实现同步操作、告知接收者本轮上报的数据已
经完整。
4- 编程进行按键测试
(1)源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/types.h>
#include <linux/kd.h>
#include <linux/keyboard.h>
#include <libgen.h>
#include <getopt.h>
/*此处在头文件中已经被定义了,这里只是让大家好理解
struct input_event
{
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};*/
#define BUTTON_CNT 10
#define EV_RELEASED 0
#define EV_PRESSED 1
void display_button_event(struct input_event *ev, int cnt);
int main(int argc, char * argv[])
{
char *kbd_dev = "/dev/input/event1";//默认的监听按键设备路径,我们想要获取首先需要路径
int kbd_fd = -1;//)open()打开监听设备路径的文件描述符
char kbd_name[256] = "Unknow";//用于获取设备的名称
fd_set rds;//用于监听的事件的集合
int rv = 0;//函数的返回值
int size = sizeof(struct input_event);
struct input_event ev[BUTTON_CNT];
/*普通用户kato的uid为1000,root用户的uid为0*/
if((getuid()) != 0)
{
printf("You are not root. This program will not run.\n");
//return 0;//这里为了运行如果不是rootID也可以运行
}
/*我们需要打开监听设备路径,于是需要文件描述符变量*/
if((kbd_fd = open(kbd_dev, O_RDONLY)) < 0)
{
printf("File %s opening error: %s\n", kbd_dev,strerror(errno));
return -1;
}
printf("kbd_fd:%d\n", kbd_fd);
/*使用ioctl()函数可以获取对应设备的名字*/
if((ioctl(kbd_fd, EVIOCGNAME(sizeof(kbd_name)), kbd_name)) < 0)
{
printf("ioctl get %s name failure: %s\n", kbd_dev, strerror(errno));
return -2;
}
printf("Monitor input device %s(%s) event on poll mode:\n", kbd_dev, kbd_name);
while(1)
{
FD_ZERO(&rds);/*清空select()的读事件集合*/
FD_SET(kbd_fd, &rds);/*将按键设备的文件描述符加入到读事件集合中*/
/*
使用select开启监听并等待多个描述符发生变化,第一个参数最大描述符+1,
2、3、4参数分别是要监听读、写、异常三个事件的文军描述符集合;
最后一个参数是超时时间(NULL-->永不超时,会一直阻塞住)
*/
rv = select(kbd_fd+1, &rds, NULL, NULL, NULL);
if(rv < 0)
{
printf("Select() system call failure: %s\n", strerror(errno));
goto CleanUp;
}
/*监听到按键发生了事件*/
else if(FD_ISSET(kbd_fd, &rds))
{
/*read读取input数据包,数据包为input_event结构体类型的,ev是结构体类型的数组*/
if((rv = read(kbd_fd, ev, size*BUTTON_CNT)) < size)
{
printf("Reading data from kbd_fd failure: %s\n", strerror(errno));
break;
}
else
{
display_button_event(ev, rv/size);
}
}
}
CleanUp:
close(kbd_fd);
return 0;
}
void display_button_event(struct input_event *ev, int cnt)
{
int i;
static struct timeval pressed_time;//按下的时间,timval结构体原型在上面说到过
struct timeval duration_time;//按下持续的时间
for(int i=0; i<cnt; i++)
{
if(EV_KEY==ev[i].type && EV_PRESSED==ev[i].value)
{
pressed_time = ev[i].time;
printf("Keypad[%d] pressed time: %ld.%ld\n", ev[i].code, pressed_time.tv_sec, pressed_time.tv_usec);
}
if(EV_KEY==ev[i].type && EV_RELEASED==ev[i].value)
{
//计算时间差的函数,第1个参数-第2个参数的值的结果保存到第3个参数中
timersub(&ev[i].time, &pressed_time, &duration_time);
printf("keypad[%d] released time: %ld.%ld\n",ev[i].code, ev[i].time.tv_sec, ev[i].time.tv_usec);
printf("keypad[%d] duration time: %ld.%ld\n",ev[i].code, duration_time.tv_sec, duration_time.tv_usec);
}
}
}
(2)linux运行
我的linux上event1也是监控按键设备的,所以在linux上也可以运行。后面我们加载到开发板上运行试一下。
(3)开发板运行
编写Makefile:
CC=arm-linux-gnueabihf-gcc
APP_NAME=key_test
all:clean
@${CC} ${APP_NAME}.c -o ${APP_NAME}
clean:
@rm -f ${APP_NAME}
不会搭建ttfp服务器的可以参考这篇文章:wpa_supplicant无线网络配置imx6ull以及搭建tftp服务器
tftp服务器下载到开发板(192.168.0.134是我虚拟机的IP):
root@igkboard:~# tftp -gr key_test 192.168.0.134
root@igkboard:~# chmod a+x key_test
root@igkboard:~# ./key_test
kbd_fd:3
Monitor input device /dev/input/event1(keys) event on poll mode:
Keypad[28] pressed time: 1677401101.420483
keypad[28] released time: 1677401101.524326
keypad[28] duration time: 0.103843
Keypad[28] pressed time: 1677401102.891474
keypad[28] released time: 1677401103.138384
keypad[28] duration time: 0.246910
我们按住用户按键然后放手,就可以看见打印的信息啦!