LV17-01-输入类设备-01-基础知识

本文主要是输入类设备控制——基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.6的64位版本(一开始使用的是16.04版本,后来进行了升级)
VMware® Workstation 17 Pro 17.0.0 build-20800274
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
其他网站 kernel - Linux source code (v4.15) - Bootlin linux内核源码在线查看
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)

一、输入类设备编程简介  

1. 什么是输入设备?

先来了解什么是输入设备(也称为 input 设备),常见的输入设备有鼠标、键盘、触摸屏、 遥控器、电脑画图板等,用户通过输入设备与系统进行交互。

2. 输入系统框架

2.1 input 子系统

由上面的介绍可知,输入设备种类非常多,每种设备上报的数据类型又不一样,那么 Linux 系统如何管理呢? Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序。

input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。基于 input 子系统注册成功的输入设备,都会在/dev/input 目录下生成对应的设备节点(设备文件), 设备节点名称通常为 eventX(X 表示一个数字编号 0、 1、 2、 3 等),例如 /dev/input/event0、 /dev/input/event1、/dev/input/event2 等, 通过读取这些设备节点可以获取输入设备上报的数据。

2.2 框架概述

输入系统框架如图所示:

image-20241020000713666

3. 读取数据的流程

如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程如下:

(1)应用程序打开/dev/input/event0 设备文件;

(2)应用程序发起读操作(例如调用 read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);

(3)用户操作设备,硬件上产生中断;

(4)输入系统驱动层对应的驱动程序处理中断:读取到数据,转换为标准的输入事件,向核心层汇报。所谓输入事件就是一个“ struct input_event”结构体。

(5)核心层可以决定把输入事件转发给上面哪个 handler 来处理:从 handler 的名字来看,它就是用来处输入操作的。有多种 handler,比如: evdev_handler、 kbd_handler、 joydev_handler 等等。最常用的是 evdev_handler:它只是把 input_event 结构体保存在内核buffer 等, APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备,每个 APP 都可以获得同一份输入事件。(当应用程序正在等待数据时, evdev_handler 会把它唤醒,这样应用程序就可以返回数据。)

(6)当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;

(4)应用程序对读取到的数据进行解析。

当无数据可读时,程序会进入休眠状态(也就是阻塞),例如应用程序读触摸屏数据, 如果当前并没有去触碰触摸屏, 自然是无数据可读; 当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式 I/O 方式下), 当有数据可读时才会被唤醒。

4. 应用程序如何解析数据? 

4.1 内核中怎么表示一个输入设备?

我们先来看一下内核中怎么表示一个输入设备?使用 input_dev 结构体来表示输入设备,它的内容如图所示:

image-20241020001311643

4.2 应用程序得到什么样的数据?

先我们要知道,应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢? 其实每一次 read 操作获取的都是一个 struct input_event 结构体类型数据, 该结构体定义在<linux/input.h>头文件中,它的定义如下:

image-20241020001429236

结构体中的 time 成员变量是一个 struct timeval 类型的变量,timeval 表示的是“自系统启动以来过了多少时间”,它是一个结构体,含有“ tv_sec、 tv_usec”两项(即秒、微秒), 内核会记录每个上报的事件其发生的时间,并通过变量 time 返回给应用程序。时间参数通常不是那么重要,而其它3 个成员变量 type(哪类事件)、 code(哪个事件)、value(事件值) 更为重要。

4.2.1 type

type 用于描述发生了哪一种类型的事件(对事件的分类) , Linux 系统所支持的输入事件类型如下所示(input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* Event types
*/
#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)

以上这些宏定义也是在<linux/input.h>头文件中,所以在应用程序中需要包含该头文件; 一种输入设备 通常可以产生多种不同类型的事件,例如点击鼠标按键(左键、右键,或鼠标上的其它按键)时会上报按键 类事件,移动鼠标时则会上报相对位移类事件。

在type成员中,我们看到有一个同步事件类型 EV_SYN, 同步事件用于实现同步操作、告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次 read 操作只能读取一个 struct input_event 类型数据,例如对于触摸屏来说,一个触摸点的信息包含了 X 坐标、 Y 坐标以及其它信息, 对于这样情况,应用程序需要执行多次 read 操作才能把一个触摸点的信息全部读取出来, 这样才能得到触摸点的完整信息。

那么应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的, 内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、 可以进行同步了。同步类事件中也包含了多种不同的事件,如下所示:

1
2
3
4
5
6
7
8
9
/*
* Synchronization events.
*/
#define SYN_REPORT 0
#define SYN_CONFIG 1
#define SYN_MT_REPORT 2
#define SYN_DROPPED 3
#define SYN_MAX 0xf
#define SYN_CNT (SYN_MAX+1)

所以的输入设备都需要上报同步事件, 上报的同步事件通常是 SYN_REPORT, 而 value 值通常为 0。

4.2.2 code

code 表示该类事件中的哪一个具体事件(input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin), 以上列举的每一种事件类型中都包含了一系列具体事件, 如一个键盘上通常有很多按键, 例如字母 A、B、 C、 D 或者数字 1、 2、 3、 4 等, 而 code变量则告知应用程序是哪一个按键发生了输入事件。每一种事件类型都包含多种不同的事件。

  • 按键类事件:
1
2
3
4
5
6
7
8
9
10
#define KEY_RESERVED 0
#define KEY_ESC 1 //ESC 键
#define KEY_1 2 //数字 1 键
#define KEY_2 3 //数字 2 键
#define KEY_TAB 15 //TAB 键
#define KEY_Q 16 //字母 Q 键
#define KEY_W 17 //字母 W 键
#define KEY_E 18 //字母 E 键
#define KEY_R 19 //字母 R 键
// ......
  • 相对位移事件:
1
2
3
4
5
6
7
8
9
10
11
12
#define REL_X  0x00 //X 轴
#define REL_Y 0x01 //Y 轴
#define REL_Z 0x02 //Z 轴
#define REL_RX 0x03
#define REL_RY 0x04
#define REL_RZ 0x05
#define REL_HWHEEL 0x06
#define REL_DIAL 0x07
#define REL_WHEEL 0x08
#define REL_MISC 0x09
#define REL_MAX 0x0f
#define REL_CNT (REL_MAX+1)
  • 绝对位移事件

触摸屏设备是一种绝对位移设备,它能够产生绝对位移事件; 例如对于触摸屏来说,一个触摸点所包含的信息可能有多种,例如触摸点的 X 轴坐标、 Y 轴坐标、 Z 轴坐标、按压力大小以及接触面积等, 所以 code变量告知应用程序当前上报的是触摸点的哪一种信息(X 坐标还是 Y 坐标、亦或者其它)。绝对位移事件如下:

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
#define ABS_X  0x00 //X 轴
#define ABS_Y 0x01 //Y 轴
#define ABS_Z 0x02 //Z 轴
#define ABS_RX 0x03
#define ABS_RY 0x04
#define ABS_RZ 0x05
#define ABS_THROTTLE 0x06
#define ABS_RUDDER 0x07
#define ABS_WHEEL 0x08
#define ABS_GAS 0x09
#define ABS_BRAKE 0x0a
#define ABS_HAT0X 0x10
#define ABS_HAT0Y 0x11
#define ABS_HAT1X 0x12
#define ABS_HAT1Y 0x13
#define ABS_HAT2X 0x14
#define ABS_HAT2Y 0x15
#define ABS_HAT3X 0x16
#define ABS_HAT3Y 0x17
#define ABS_PRESSURE 0x18
#define ABS_DISTANCE 0x19
#define ABS_TILT_X 0x1a
#define ABS_TILT_Y 0x1b
#define ABS_TOOL_WIDTH 0x1c
// ......

除了以上列举出来的之外,还有很多,我们可以自己浏览<linux/input.h>头文件(这些宏其实是定义在input-event-codes.h 头文件中,该头文件被< linux/input.h >所包含了) , 关于这些具体的事件,后面遇到了再学习。

4.2.3 value

表示事件值 ,内核每次上报事件都会向应用层发送一个数据 value, 对 value 值的解释随着 code 的变化而变化。对于按键事件(type=1) 来说, 如果 code=2(键盘上的数字键 1,也就是 KEY_1), 那么如果 value 等于 1,则表示 KEY_1 键按下; value 等于 0 表示 KEY_1 键松开,如果 value 等于 2,则表示 KEY_1 键长按。再比如, 在绝对位移事件中(type=3),如果 code=0 (触摸点 X 坐标 ABS_X),那么 value 值就等于触摸点的 X 轴坐标值; 同理, 如果 code=1(触摸点 Y 坐标 ABS_Y),此时value 值便等于触摸点的 Y 轴坐标值; 所以对 value 值的解释需要根据不同的 code 值而定。

4.3 事件之间的界线

应用程序读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报 X、 Y 位置信息,也可能会上报压力值。那它怎么知道已经读到了完整的数据?

驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完毕。 APP 读到“同步事件”时,就知道已经读完了当前的数据。同步事件也是一个 input_event 结构体,它的 type、 code、 value 三项都是 0。

5. 输入子系统支持完整的 API 操作

支持这些机制:阻塞、非阻塞、 POLL/SELECT、异步通知。

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

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

1
2
3
4
5
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寸触摸屏的信息:

1
2
3
4
5
6
7
8
9
10
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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 命令:

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

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

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

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

1
#define EVIOCGBIT(ev,len)	_IOC(_IOC_READ, 'E', 0x20 + (ev), len)	/* get event bits */

3.2 代码实例

可以看这里:

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

1
./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):

1
2
3
#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 · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

执行程序时需要传入参数,这个参数就是对应的输入设备的设备节点(设备文件),程序中会对传参进行校验。程序中首先调用 open()函数打开设备文件,之后在 while 循环中调用 read()函数读取文件,将读取到的数据存放在 struct input_event 结构体对象中,之后将结构体对象中的各个成员变量打印出来。

程序中使用了阻塞式 I/O 方式读取设备文件,所以当无数据可读时 read 调用会被阻塞,直到有数据可读时才会被唤醒!

1.2 开发板验证

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

1
./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 · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

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
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 。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
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 · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

image-20241020103825253

3.2.2 SELECT

可以看这里:LV17_INPUT_DEVICE/01_read_input_select · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

image-20241020103904132

3.2 开发板验证

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

1
./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 )中,有很多信号的宏定义:

1
2
3
4
5
6
7
8
9
#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 函数来“给某个信号注册处理函数”,用法如下:

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

4.2 代码示例

可以看这里:LV17_INPUT_DEVICE/01_read_input_fasync · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

image-20241020103955931

4.3 开发板验证

我们执行以下命令:

1
./app_demo /dev/input/event2

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

image-20241020103043284