LV010-获取输入设备数据
一、如何获取输入设备信息?
1. 如何确认设备信息?
ALPHA 开发板上有一个用户按键 KEY0, 它就是一个典型的输入设备, 如下图所示:

该按键是提供给用户使用的一个 GPIO 按键, 在出厂系统中,该按键驱动基于 input 子系统而实现, 所以在/dev/input 目录下存在 KEY0 的设备节点, 具体是哪个设备节点, 可以通过查看/proc/bus/input/devices 文件得知,查看该文件可以获取到系统中注册的所有输入设备相关的信息,如下所示(这张图是正点原子教程资料里面的):

那么这里的 I、 N、 P、 S、 U、 H、 B 对应的每一行是什么含义呢?
(1)I:d of the device(设备 ID) ,该参数由结构体 struct input_id 来进行描述,驱动程序中会定义这样的结构体:
(2)N:name of the device,设备名称
(3)P:physical path to the device in the system hierarchy,系统层次结构中设备的物理路径。
(4)S:sysfs path位于 sys 文件系统的路径
(5)U:unique identification code for the device(if device has it),设备的唯一标识码 。
(6)H:list of input handles associated with the device,与设备关联的输入句柄列表。
(7)B:bitmaps(位图)
shellPROP:device properties and quirks(设备属性) EV:types of events supported by the device(设备支持的事件类型) KEY:keys/buttons this device has(此设备具有的键/按钮) MSC:miscellaneous events supported by the device(设备支持的其他事件) LED:leds present on the device(设备上的指示灯)值得注意的是 B 位图,比如上图中“ B: EV=b”(触摸屏那个设备)用来表示该设备支持哪类输入事件。 b 的二进制是 1011, bit0、 1、 3 为 1,表示该设备支持 0、 1、 3 这三类事件,即 EV_SYN、 EV_KEY、 EV_ABS。 再举一个例子,下面是我自己使用的4.3寸触摸屏的信息:
shellI: Bus=0018 Vendor=dead Product=beef Version=28bb N: Name="goodix-ts" P: Phys=input/ts S: Sysfs=/devices/virtual/input/input2 U: Uniq= H: Handlers=event1 B: PROP=3 B: EV=b B: KEY=e520 0 0 0 0 0 0 0 0 0 0 B: ABS=2658000 0“ B: ABS=2658000 0”如何理解? 它表示该设备支持 EV_ABS 这一类事件中的哪一些事件。这是 2 个 32 位的数字: 0x2658000、 0x0, 高位在前低位在后, 组成一 个 64 位 的数字 :
0x2658000 00000000这样的话数值为1的位有47、48、50、53、54,即0x2f、0x20、0x30、0x32、0x35、0x36,对应这些宏(input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin):#define ABS_MT_SLOT 0x2f /* MT slot being modified */ #define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */ #define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */ #define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */ #define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */ #define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */ #define ABS_MT_POSITION_X 0x35 /* Center X touch position */ #define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */ #define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */ #define ABS_MT_BLOB_ID 0x38 /* Group a set of packets as a blob */ #define ABS_MT_TRACKING_ID 0x39 /* Unique ID of initiated contact */ #define ABS_MT_PRESSURE 0x3a /* Pressure on contact area */ #define ABS_MT_DISTANCE 0x3b /* Contact hover distance */ #define ABS_MT_TOOL_X 0x3c /* Center X tool position */ #define ABS_MT_TOOL_Y 0x3d /* Center Y tool position */即 这 款 输 入 设 备 支 持 上 述 的、 ABS_MT_SLOT 、ABS_MT_TOUCH_MAJOR 、 ABS_MT_WIDTH_MAJOR 、ABS_MT_POSITION_X 、ABS_MT_POSITION_Y 这些绝对位置事件。
这里其实还有一种办法可以用来确认(这里使用的是出厂系统),我们的输入设备,其对应的设备文件在/dev/input/目录下 :
如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,例如使用 od 命令:
sudo od -x /dev/input/eventX # eventX表示上面的event多少
hexdump /dev/input/eventx # 这个命令也能去读,如果系统有这个命令的话Tips:需要添加 sudo,在 Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作。 要是直接在开发板中的话,看自己是什么用户权限了。
我们可以一个一个试,当执行完命令后按下按键,哪一个读取到了数据,就说明这个就是按键对应的输入设备文件。

比如这里是event2,按下按键的时候就会出现对应的打印信息。这些信息都是什么?我们来分析一下:

type 为 1 , 对 应 EV_KEY ; code 为 0x72,按键的code定义的是十进制数字,所以这里是114,对应KEY_VOLUMEDOWN;上图中还发现有 2 个同步事件:它的 type、 code、 value 都为 0。表示按键上报了 2 次完整的数据。
2. APP 访问硬件的 4 种方式
举个例子:妈妈怎么知道卧室里小孩醒了?
(1)时不时进房间看一下: 查询方式。简单,但是累。
(2)进去房间陪小孩一起睡觉,小孩醒了会吵醒她: 休眠-唤醒。不累,但是妈妈干不了活了。
(3)妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟: poll 方式。有浪费点时间,但是可以继续干活。妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
(4)妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈: 异步通知。妈妈、小孩互不耽误。
这 4 种方法没有优劣之分,在不同的场合使用不同的方法。
3. 获取设备信息
3.1 ioctl函数
过 ioctl 获取设备信息, ioctl 的参数如下:
int ioctl(int fd, unsigned long request, ...);些驱动程序对 request 的格式有要求,它的格式如下(ioctl.h - include/uapi/asm-generic/ioctl.h - Linux source code v4.15 - Bootlin):
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \ // bit29
((type) << _IOC_TYPESHIFT) | \ // bit8
((nr) << _IOC_NRSHIFT) | \ // bit0
((size) << _IOC_SIZESHIFT)) // bit16比如 dir 为_IOC_READ(即 2)时,表示 APP 要读数据;为_IOC_WRITE(即 4)时,表示 APP 要写数据。
size 表示这个 ioctl 能传输数据的最大字节数。
type、 nr 的含义由具体的驱动程序决定。
比如要读取输入设备的 evbit 时, ioctl 的 request 要写为“ EVIOCGBIT(0,size)”, size 的大小可以由自己决定:你想读多少字节就设置为多少。这个宏的定义如下:
#define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) /* get event bits */3.2 代码实例
可以看这里:LV17_INPUT_DEVICE/01_read_input_info/read_input_info.c · linux-dev-org/imx6ull-app-demo,我们编译后执行以下命令:
./app_demo /dev/input/event2
前面我们知道B后面的EV表示支持的事件类型,这里的按键就是0x100003,第0、1、20位为1,也就是0x0、0x1、0x14,就对应这些事件(input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin):
#define EV_SYN 0x00
#define EV_KEY 0x01
#define EV_REP 0x14三、如何获取输入数据?
1. 休眠-唤醒方式
APP 调用 open 函数时,不要传入“ O_NONBLOCK”。APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read 函数会返回数据;否则 APP 就会在内核态休眠,当有数据时驱动程序会把 APP 唤醒, read 函数恢复执行并返回数据给 APP。
1.1 代码示例
代码可以看这里:LV17_INPUT_DEVICE/01_read_input/read_input.c · linux-dev-org/imx6ull-app-demo,编译后得到可执行程序。
执行程序时需要传入参数,这个参数就是对应的输入设备的设备节点(设备文件),程序中会对传参进行校验。程序中首先调用 open()函数打开设备文件,之后在 while 循环中调用 read()函数读取文件,将读取到的数据存放在 struct input_event 结构体对象中,之后将结构体对象中的各个成员变量打印出来。
程序中使用了阻塞式 I/O 方式读取设备文件,所以当无数据可读时 read 调用会被阻塞,直到有数据可读时才会被唤醒!
1.2 开发板验证
我们在串口终端执行以下命令:
./app_demo /dev/input/event2
程序运行后,执行按下 KEY0、松开 KEY0 等操作,终端将会打印出相应的信息,如上图所示。
第一行中 type 等于 1,表示上报的是按键事件 EV_KEY, code=114, 打开 input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin 头文件进行查找,可以发现 cpde=114 对应的是键盘上的 KEY_VOLUMEDOWN 按键,这个是 ALPHA 开发板出厂系统已经配置好的。 而 value=1 表示按键按下,所以整个第一行的意思就是按键 KEY_VOLUMEDOWN被按下。
第二行, 表示上报了 EV_SYN 同步类事件(type=0)中的 SYN_REPORT 事件(code=0), 表示本轮数据已经完整、报告同步。
第三行, type 等于 1,表示按键类事件, code 等于 114、value 等于 0,所以表示按键 KEY_VOLUMEDOWN被松开。
第四行,又上报了同步事件。
所以整个上面 4 行的打印信息就是开发板上的 KEY0 按键被按下以及松开这个过程, 内核所上报的事件以及发送给应用层的数据 value。 我们试试长按按键 KEY0, 按住不放, 如下所示:

可以看到上报按键事件时,对应的 value 等于 2,表示长按状态。
2. 查询方式
APP 调用 open 函数时,传入“ O_NONBLOCK”表示“非阻塞”。APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read函数会返回数据,否则也会立刻返回错误。
2.1 代码示例
可以看这里:LV17_INPUT_DEVICE/01_read_input_noblock · linux-dev-org/imx6ull-app-demo

2.2 开发板验证
我们执行这个demo的时候注意传入noblock参数,不传的时候它会以休眠唤醒的方式运行,这样我们就会看到按键没有按下的时候一直打印错误信息,当按键按下就会有按键的信息打印出来:
3. POLL/SELECT 方式
3.1 POLL/SELECT介绍
POLL 机制、 SELECT 机制是完全一样的,只是 APP 接口函数不一样。简单地说,它们就是“定个闹钟”:在调用 poll、 select 函数时可以传入“超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到“超时时间”结束时返回错误。用法如下。
(1)APP 先调用 open 函数打开设备节点,打开的时候要使用noblock模式。
(2)APP 不是直接调用 read 函数,而是先调用 poll 或 select 函数,这 2 个函数中可以传入“超时时间”。它们的作用是:如果驱动程序中有数据,则立刻返回;否则就休眠。在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把 APP唤醒,导致 poll 或 select 立刻返回;如果在“ 超时时间”内无人操作硬件,则时间到后 poll 或 select 函数也会返回。 APP 可以根据函数的返回值判断返回原因:有数据?无数据超时返回?
(3)APP 根据 poll 或 select 的返回值判断有数据之后,就调用 read 函数读取数据时,这时就会立刻获得数据。
(4)poll/select 函数可以监测多个文件,可以监测多种事件:
| 事件类型 | 说明 |
|---|---|
| POLLIN | 有数据可读 |
| POLLRDNORM | 等同于 POLLIN |
| POLLRDBAND | Priority band data can be read,有优先级较较高的“ band data”可读 Linux 系统中很少使用这个事件 |
| POLLPRI | 高优先级数据可读 |
| POLLOUT | 可以写数据 |
| POLLWRNORM | 等同于 POLLOUT |
| POLLWRBAND | Priority data may be written |
| POLLERR | 发生了错误 |
| POLLHUP | 挂起 |
| POLLNVAL | 无效的请求,一般是 fd 未 open |
注意:在调用 poll 函数时,要指明: 要监测哪一个文件:哪一个 fd ;想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT 。
示例代码如下:
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
fds[0].fd = fd;
fds[0].events = POLLIN;
ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}3.2 代码示例
3.2.1 POLL
可以看这里:LV17_INPUT_DEVICE/01_read_input_poll · linux-dev-org/imx6ull-app-demo
3.2.2 SELECT
可以看这里:LV17_INPUT_DEVICE/01_read_input_select · linux-dev-org/imx6ull-app-demo
3.2 开发板验证
编译后我们执行以下命令:
./app_demo /dev/input/event2然后会有如下打印信息:

会发现,即便我们以noblock的方式打开了节点,也不会再直接返回,当有数据的时候直接返回并打印信息,当没有数据,超过5000ms的时候,超时返回。POLL和SELECT的实验现象是一样的,这里就不重复写了。
4. 异步通知
4.1 什么是异步通知?
所谓同步,就是“你慢我等你”。那么异步就是:你慢那你就自己玩,我做自己的事去了,有情况再通知我。所谓异步通知,就是 APP 可以忙自己的事,当驱动程序用数据时它会主动给APP 发信号,这会导致 APP 执行信号处理函数。
“ 发信号”,这只有 3 个字,却可以引发很多问题:
- 谁发?驱动程序发。
- 发什么?信号。
- 发什么信号?SIGIO。
- 怎么发?内核里提供有函数 。
- 发给谁?APP, APP 要把自己告诉驱动。
- APP 收到后做什么?执行信号处理函数。
- 信号处理函数和信号,之间怎么挂钩? APP 注册信号处理函数 。
- 内核里有那么多驱动,你想让哪一个驱动给我们的APP发 SIGIO 信号?APP 要打开驱动程序的设备节点。
- 驱动程序怎么知道要发信号给我们的APP而不是别人的APP? APP 要把自己的进程 ID 告诉驱动程序。
- APP 有时候想收到信号,有时候又不想收到信号:应该可以把 APP 的意愿告诉驱动:设置 Flag 里面的 FASYNC 位为 1,使能“异步通知”。
4.1.1 有哪些信号?
Linux 系统中有很多信号,在 Linux 内核源文件 include/uapi/asm-generic/signal.h(signal.h - include/uapi/asm-generic/signal.h - Linux source code v4.15 - Bootlin )中,有很多信号的宏定义:
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
// ......
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIOSIGIO在驱动中很常用,表示有IO事件。驱动程序通知 APP 时,它会发出“ SIGIO”这个信号,表示有“ IO 事件”要处理。
4.1.2 信号注册
就 APP 而言,想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟SIGIO 挂钩。这可以通过一个 signal 函数来“给某个信号注册处理函数”,用法如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);(1)编写信号处理函数,比如signal_handler();
(2)调用signal函数进行注册,函数的第一个参数代表哪个信号,第二个参数就是刚才编写的信号处理函数。
4.1.3 基本步骤
- (1)编写信号处理函数
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}- (2)注册信号处理函数:
signal(SIGIO, sig_func);- (3)打开驱动(设备节点 )
fd = open(argv[1], O_RDWR);- (4)把进程 ID 告诉驱动
fcntl(fd, F_SETOWN, getpid());- (5)使能驱动的 FASYNC 功能
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);4.2 代码示例
可以看这里:LV17_INPUT_DEVICE/01_read_input_fasync · linux-dev-org/imx6ull-app-demo
4.3 开发板验证
我们执行以下命令:
./app_demo /dev/input/event2会看到,我们一直在进程中循环打印循环的次数,当有按键来的时候就会打印出按键的相关信息。
