版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
2.4 Input设备编程之按键控制
2.4.1 input 子系统简介
Input子系统是Linux对输入设备提供的统一驱动框架。如按键、键盘、触摸屏和鼠标等输入设备的驱动方式是类似的,当出现按键、触摸等操作时,硬件产生中断,然后CPU直接读取引脚电平,或通过SPI、I2C等通讯方式从设备的寄存器读取具体的按键值或触摸坐标,然后把这些信息提交给内核。
使用Input子系统 驱动的输入设备可以通过统一的数据结构提交给内核,该数据结构包括输入的时间、类型、代号以及具体的键值或坐标,而内则通过**/dev/input**目录下的文件接口传递给用户空间。
2.4.2 input子系统框架
作为应用开发人员,可以只基于API使用输入子系统。但是了解内核中输入子系统的框架、了解数据流程,有助于解决开发过程中碰到的硬件问题、驱动问题。Linux系统下的输入系统框架如下图所示:
假设用户程序直接访问 /dev/input/event0 设备节点,或者使用tslib访问触摸屏设备节点,数据的流向如下:
应用程序open()打开输入设备文件后调用read()读数据,此时输入设备没有事件发生,则读不到数据阻塞;
用户操作设备(如按下设备、点击触摸屏),硬件上产生中断;
输入系统驱动层对应的驱动程序处理中断:读取到数据,转换为标准的输入事件(一个struct input_event结构体),向核心层汇报。
核心层可以决定把输入事件转发给上面哪个handler来处理。从handler的名字来看,它就是用来处输入操作的。比如:evdev_handler、kbd_handler、joydev_handler等。
他们作用就是把核心层的数据返回给正在读取的APP,当APP正在调用read()系统调用等待数据时,evdev_handler会把它唤醒,这样APP就可以返回数据。
2.4.3 input事件目录
2.4.3.1 input事件结构
应用程序空间在从input设备read()读取数据时,它的每个数据元素是struct input_event
结构体类型,该结构体在Linux内核源码中其定义在 include/uapi/linux/input.h 文件中,而应用程序空间则定义在 /usr/include/linux/input.h 文件中。
该结构体的定义如下:
struct input_event {
struct timeval time;
__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 */
};
time:该变量用于记录事件产生的时间戳。表示“自系统启动以来过了多少时间”,由秒和微秒(long 类型 32bit)组成。
type:输入设备的事件类型。系统常用的默认类型有EV_KEY、 EV_REL和EV_ABS,分别用于表示按键状态改变事件、相对坐标改变事件及绝对坐标改变事件。其类型定义如下:
/*
Event types
Linux内核源码: include/uapi/linux/input-event-codes.h
应用编程头文件: /usr/include/linux/input-event-codes.h */
#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 #define EV_SND 0x12 #define EV_REP 0x14 #define EV_FF 0x15 #define EV_PWR 0x16 #define EV_FF_STATUS 0x17 #define EV_MAX 0x1f #define EV_CNT (EV_MAX+1)
- **code**:事件代号,表示该类事件下的哪一个事件。例如 在EV_KEY事件类型中,code的值常用于表示键盘上具体的按键,比如数字键1、2、3,字母键A、B、C里等。查看定义
```c
/*
* Keys and buttons
* Linux内核源码: include/uapi/linux/input-event-codes.h
* 应用编程头文件: /usr/include/linux/input-event-codes.h
*/
#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 KEY_MINUS 12
#define KEY_EQUAL 13
#define KEY_BACKSPACE 14
#define KEY_TAB 15
#define KEY_Q 16
...
value :事件的值。对于EV_KEY事件类型,当按键按下时,该值为1;按键松开时,该值为0。
2.4.3.2 input事件设备名
查看/dev/input可以看到很多event*节点,事件编号与设备的联系不是固定的,它通常按系统检测到设备的先后顺序安排event文件的编号。例如:在IGKBoard开发板上查看/dev/input文件夹下,有三个event事件编号。
root@igkboard:~# ls /dev/input/
by-path event0 event1 event2 mice mouse0 touchscreen0
下面提供三个方法查看event编号对应的具体的硬件设备:
查看/dev/input/by-path目录查看事件编号对应的具体的硬件设备
root@igkboard:~# ls -l /dev/input/by-path/
total 0
lrwxrwxrwx 1 root root 9 Sep 25 13:15 platform-20cc000.snvs:snvs-powerkey-event -> ../event0
lrwxrwxrwx 1 root root 9 Sep 25 13:15 platform-21a4000.i2c-event -> ../event1
lrwxrwxrwx 1 root root 9 Sep 25 13:15 platform-keys-event -> ../event2
该目录下的文件实际上都是链接,如event2对应的就是访问开发板的按键的事件设备。由于/dev下的设备都是通过/sys导出的,所以也可以通过/sys/class/input目录查看。
root@igkboard:~# ls /sys/class/input/
event0 event1 event2 input0 input1 input2 mice mouse0
root@igkboard:~# ls /sys/class/input/event2/
consumers dev device power subsystem suppliers uevent
root@igkboard:~# ls /sys/class/input/event2/device
capabilities consumers device event2 id modalias name phys power properties subsystem suppliers uevent uniq
root@igkboard:~# cat /sys/class/input/event2/device/name
keys
查看/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=0018 Vendor=0416 Product=23bb Version=1040
N: Name="Goodix Capacitive TouchScreen"
P: Phys=input/ts
S: Sysfs=/devices/platform/soc/2100000.bus/21a4000.i2c/i2c-1/1-005d/input/input1
U: Uniq=
H: Handlers=kbd mouse0 event1
B: PROP=2
B: EV=b
B: KEY=400 0 0 0 0 0 0 20000000 1 f8000000 0
B: ABS=2658000 3
I: Bus=0019 Vendor=0001 Product=0001 Version=0100
N: Name="keys"
P: Phys=gpio-keys/input0
S: Sysfs=/devices/platform/keys/input/input2
U: Uniq=
H: Handlers=kbd event2
B: PROP=0
B: EV=100003
B: KEY=10000000
可以看到keys对应的就是event2(其H: Handlers=kbd event2 ),下面是每一个设备信息中的I、N、P、S、U、H、B对应的含义:
I:id of the device 设备ID 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 位图 PROP:设备属性 EV:设备支持的事件类型 KEY:此设备具有的键/按钮 MSC:设备支持的其他事件 LED:设备上的指示灯
解释B位图: 比如第二个设备的 “B: EV=b”,用来表示该设备支持哪类输入事件。b的二进制是1011,即bit0、bit1、bit3使能,表示该设备支持0、1、3这三类事件,通过查看struct input_event中type的事件类型可以知道是,EV_SYN、EV_KEY、EV_ABS。至于其他ABS=和KEY=是相应硬件特定的属性值。
使用evtest工具查看事件编号对应的具体的硬件设备
在开发input子系统驱动时,常常会使用 evtest 工具进行测试,它列出了系统当前可用的/dev/input/event0~2输入事件 文件,并且列出了这些事件对应的设备名。具体如下所示:
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: Goodix Capacitive TouchScreen
/dev/input/event2: keys
Select the device event number [0-2]: 2
2.4.4 开发板按键检测实验
2.4.4.1 使用命令行工具检测
IGKBoard开发板可以使用两种方式读取event事件: evtest 和 hexdump 。
evtest 命令使用
在命令行输入evtest命令 ,然后选择需要检测的Input设备对应的event编号,如下可知设备 event2
对应着开发板上的用户按键,然后按下一次用户按键(位置如下图所示)。
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: Goodix Capacitive TouchScreen
/dev/input/event2: keys
Select the device event number [0-2]: 2
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 1664122073.285274, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 1664122073.285274, -------------- SYN_REPORT ------------
Event: time 1664122073.425210, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 1664122073.425210, -------------- SYN_REPORT ------------
Event: time 1664122075.815182, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 1664122075.815182, -------------- SYN_REPORT ------------
Event: time 1664122075.965267, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 1664122075.965267, -------------- SYN_REPORT ------------
输入编号后它列出了event2的一些设备信息,包括驱动版本、设备ID、设备名、支持的事件类型、事件代号以及输入值的取值范围。
此时按下开发板按键(按键位置如下图所示),可以看到它输出了详细的事件信息。输出信息中每一行包含了按键上报事件的具体时间time、事件类型type 1(EV_KEY)、事件代号code 28和具体的值value,该值就是按键按下和释放上报的值。
还有一个SYN_REPORT即为同步事件。同步事件用于实现同步操作、告知接收者本轮上报的数据已经完整。
hexdump 命令使用
linux系统下使用 hexdump 命令可以以16进制形式查看任何文件的数据内容。这样,我们也可以使用它来读取按键设备按下按键时按下的event数据包。如下所示,在开发板上输入以下命令,然后按下一次用户按键。
root@igkboard:~# hexdump /dev/input/event2
0000000 15bf 6331 3829 000b 0001 001c 0001 0000
0000010 15bf 6331 3829 000b 0000 0000 0000 0000
0000020 15bf 6331 e5d0 000c 0001 001c 0000 0000
0000030 15bf 6331 e5d0 000c 0000 0000 0000 0000
解析第一行数据(都是以16进制显示)
0000000:序列,一行有16字节数据;
15bf 6331:秒(时间)
3829 000b:微秒(时间)
0001:type,对应EV_KEY
001c:code,对应KEY_ENTER
0001 0000:value,对应值为1
第二行数据为一次同步事件
第三行数据为按键释放时候,value值为0
第四行数据为一次同步事件
2.4.4.2 按键测试程序应用编程
下面是用户按键测试程序的源代码。
guowenxue@ubuntu20:~/igkboard/apps$ vim keypad.c
/*********************************************************************************
* Copyright: (C) 2021 Guo Wenxue<Email:guowenxue@gmail.com QQ:281143292>
* All rights reserved.
*
* Filename: keypad.c
* Description: This file used to test GPIO button driver builtin Linux kernel
*
* Version: 1.0.0(11/17/2021~)
* Author: Guo Wenxue <guowenxue@gmail.com>
* ChangeLog: 1, Release initial version on "11/17/2021 02:46:18 PM"
*
********************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <libgen.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/input.h>
#include <linux/kd.h>
#include <linux/keyboard.h>
#if 0 /* Just for comment here, Reference to linux-3.3/include/linux/input.h */
struct input_event
{
struct timeval time;
__u16 type; /* 0x00:EV_SYN 0x01:EV_KEY 0x04:EV_MSC 0x11:EV_LED*/
__u16 code; /* key value, which key */
__s32 value; /* 1: Pressed 0:Not pressed 2:Always Pressed */
};
#endif
#define EV_RELEASED 0
#define EV_PRESSED 1
#define BUTTON_CNT 10
/* 在C语言编程中,函数应该先定义再使用,如果函数的定义在函数调用后面,应该前向声明。*/
void usage(char *name);
void display_button_event(struct input_event *ev, int cnt);
int main(int argc, char **argv)
{
char *kbd_dev = "/dev/input/event2"; //默认监听按键设备;
char kbd_name[256] = "Unknown"; //用于保存获取到的设备名称
int kbd_fd = -1; //open()打开文件的文件描述符
int rv=0; // 函数返回值,默认返回0;
int opt; // getopt_long 解析命令行参数返回值;
int size = sizeof (struct input_event);
fd_set rds; //用于监听的事件的集合
struct input_event ev[BUTTON_CNT];
/* getopt_long参数函数第四个参数的定义,二维数组,每个成员由四个元素组成 */
struct option long_options[] = {
/* { 参数名称,是否带参数,flags指针(NULL时将val的数值从getopt_long的返回值返回出去),
函数找到该选项时的返回值(字符)}
*/
{"device", required_argument, NULL, 'd'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
//获取命令行参数的解析返回值
while ((opt = getopt_long(argc, argv, "d:h", long_options, NULL)) != -1)
{
switch (opt)
{
case 'd':
kbd_dev = optarg;
break;
case 'h':
usage(argv[0]);
return 0;
default:
break;
}
}
if(NULL == kbd_dev)
{
/* 命令行argv[0]是输入的命令,如 ./keypad */
usage(argv[0]);
return -1;
}
/* 获取uid 建议以root权限运行确保可以正常运行 */
if ((getuid ()) != 0)
printf ("You are not root! This may not work...\n");
/* 打开按键对应的设备节点,如果错误则返回负数 */
if ((kbd_fd = open(kbd_dev, O_RDONLY)) < 0)
{
printf("Open %s failure: %s", kbd_dev, strerror(errno));
return -1;
}
/* 使用ioctl获取 /dev/input/event*对应的设备名字 */
ioctl (kbd_fd, EVIOCGNAME (sizeof (kbd_name)), kbd_name);
printf ("Monitor input device %s (%s) event on poll mode:\n", kbd_dev, kbd_name);
/* 循环使用 select() 多路复用监听按键事件 */
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结构体类型。
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 usage(char *name)
{
char *progname = NULL;
char *ptr = NULL;
/* 字符串拷贝函数,该函数内部将调用malloc()来动态分配内存,然后将$name
字符串内容拷贝到malloc分配的内存中,这样使用完之后需要free释放内存. */
ptr = strdup(name);
progname = basename(ptr); //去除该可执行文件的路径名,获取其自身名称(即keypad)
printf("Usage: %s [-p] -d <device>\n", progname);
printf(" -d[device ] button device name\n");
printf(" -p[poll ] Use poll mode, or default use infinit loop.\n");
printf(" -h[help ] Display this help information\n");
free(ptr); //和strdup对应,释放该内存
return;
}
/* 该函数用来解析按键设备上报的数据,并答应按键按下的相关信息 */
void display_button_event(struct input_event *ev, int cnt)
{
int i;
static struct timeval pressed_time; //该变量用来存放按键按下的时间,注意static的使用。
struct timeval duration_time; //该变量用来存放按键按下持续时间
for(i=0; i<cnt; i++)
{
/* 当上报的时间type为EV_KEY时候并且,value值为1或0 (1为按下,0为释放) */
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)
{
/* 计算时间差函数 将第一个参数减去第二个参数的值的结果 放到第三个参数之中 */
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);
}
}
}
编写Makefile如下
guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=arm-linux-gnueabihf-gcc
APP_NAME=keypad
all:clean
@${CC} ${APP_NAME}.c -o ${APP_NAME}
clean:
@rm -f ${APP_NAME}
2.4.4.3 交叉编译测试运行
在ubuntu下的相关源码路径下执行make命令将会编译源码生成ARM开发板上的可执行文件。
guowenxue@ubuntu20:~/igkboard/apps$ make
guowenxue@ubuntu20:~/igkboard/apps$ ls
Makefile keypad keypad.c
现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。
root@igkboard:~# tftp -gr keypad 192.168.2.2
接下来,我们给该程序加上执行权限并运行。接着按下开发板上的用户按键并释放,可以看到按键按下、释放和持续时间。
root@igkboard:~# chmod a+x keypad
root@igkboard:~# ./keypad
Monitor input device /dev/input/event2 (keys) event on poll mode:
Keypad[28] pressed time: 1664179462.815300
keypad[28] released time: 1664179462.995239
keypad[28] duration time: 0.179939
Keypad[28] pressed time: 1664179465.95257
keypad[28] released time: 1664179466.5091
keypad[28] duration time: 0.909834
Keypad[28] pressed time: 1664179469.505246
keypad[28] released time: 1664179471.865116
keypad[28] duration time: 2.359870