Skip to content

LV010-获取输入设备数据

一、如何获取输入设备信息?

1. 如何确认设备信息?

ALPHA 开发板上有一个用户按键 KEY0, 它就是一个典型的输入设备, 如下图所示:

image-20240907150100195

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

image-20240907150157169

那么这里的 I、 N、 P、 S、 U、 H、 B 对应的每一行是什么含义呢?

(1)I:d of the device(设备 ID) ,该参数由结构体 struct input_id 来进行描述,驱动程序中会定义这样的结构体:

image-20241020002407464

(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(位图)

shell
PROP: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寸触摸屏的信息:

shell
I: 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/目录下 :

image-20240907150531593

如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,例如使用 od 命令:

shell
sudo od -x /dev/input/eventX # eventX表示上面的event多少
hexdump /dev/input/eventx    # 这个命令也能去读,如果系统有这个命令的话

Tips:需要添加 sudo,在 Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作。 要是直接在开发板中的话,看自己是什么用户权限了。

我们可以一个一个试,当执行完命令后按下按键,哪一个读取到了数据,就说明这个就是按键对应的输入设备文件。

image-20240907150802216

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

image-20241020090013748

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 的参数如下:

c
int ioctl(int fd, unsigned long request, ...);

些驱动程序对 request 的格式有要求,它的格式如下(ioctl.h - include/uapi/asm-generic/ioctl.h - Linux source code v4.15 - Bootlin):

c
#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 的大小可以由自己决定:你想读多少字节就设置为多少。这个宏的定义如下:

c
#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,我们编译后执行以下命令:

shell
./app_demo /dev/input/event2
image-20241020093213808

前面我们知道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):

c
#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 开发板验证

我们在串口终端执行以下命令:

shell
./app_demo /dev/input/event2
image-20240907151311435

程序运行后,执行按下 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, 按住不放, 如下所示:

image-20240907152031830

可以看到上报按键事件时,对应的 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

image-20241020103717460

2.2 开发板验证

我们执行这个demo的时候注意传入noblock参数,不传的时候它会以休眠唤醒的方式运行,这样我们就会看到按键没有按下的时候一直打印错误信息,当按键按下就会有按键的信息打印出来:

image-20241020095208032

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
POLLRDBANDPriority band data can be read,有优先级较较高的“ band data”可读 Linux 系统中很少使用这个事件
POLLPRI高优先级数据可读
POLLOUT可以写数据
POLLWRNORM等同于 POLLOUT
POLLWRBANDPriority data may be written
POLLERR发生了错误
POLLHUP挂起
POLLNVAL无效的请求,一般是 fd 未 open

注意:在调用 poll 函数时,要指明: 要监测哪一个文件:哪一个 fd ;想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT 。

示例代码如下:

c
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 开发板验证

编译后我们执行以下命令:

shell
./app_demo /dev/input/event2

然后会有如下打印信息:

image-20241020100606809

会发现,即便我们以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 )中,有很多信号的宏定义:

c
#define SIGHUP		 1
#define SIGINT		 2
#define SIGQUIT		 3
#define SIGILL		 4
#define SIGTRAP		 5
// ......
#define SIGWINCH	28
#define SIGIO		29
#define SIGPOLL		SIGIO

SIGIO在驱动中很常用,表示有IO事件。驱动程序通知 APP 时,它会发出“ SIGIO”这个信号,表示有“ IO 事件”要处理。

4.1.2 信号注册

就 APP 而言,想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟SIGIO 挂钩。这可以通过一个 signal 函数来“给某个信号注册处理函数”,用法如下:

c
#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)编写信号处理函数
c
static void sig_func(int sig)
{
    int val;
    read(fd, &val, 4);
    printf("get button : 0x%x\n", val);
}
  • (2)注册信号处理函数:
c
signal(SIGIO, sig_func);
  • (3)打开驱动(设备节点 )
c
fd = open(argv[1], O_RDWR);
  • (4)把进程 ID 告诉驱动
c
fcntl(fd, F_SETOWN, getpid());
  • (5)使能驱动的 FASYNC 功能
c
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 开发板验证

我们执行以下命令:

shell
./app_demo /dev/input/event2

会看到,我们一直在进程中循环打印循环的次数,当有按键来的时候就会打印出按键的相关信息。

image-20241020103043284