嵌入式Linux输入系统应用编程学习总结
目录
- 嵌入式Linux输入系统应用编程学习总结
- 1. 嵌入式Linux输入系统介绍
- 2. Linux设备输入数据的几个结构体
- 2.1 内核中表示一个输入设备的结构体
- 2.2 APP从输入设备获取的数据类型结构体
- 3. 查看LCD设备信息和输入数据
- 3.1 查看设备信息
- 3.2 使用命令读取输入的数据
- 3.3 4种机制获取LCD设备输入数据
- 3.3.1 使用ioctl获取设备信息
- 3.3.2 查询方式(Polling)和休眠唤醒方式(Blocking I/O)获取设备输入的信息
- 3.3.3 poll方式(Polling with poll)获取设备输入的信息
- 3.3.4 selsct方式(Polling with select)获取设备输入的信息
- 3.3.5 异步通知方式(Asynchronous I/O)获取设备输入的信息
基于韦东山IMX6ULL开发板和配套资料学习
参考教程:韦东山老师教程
1. 嵌入式Linux输入系统介绍
嵌入式Linux输入系统是处理来自各种输入设备(如键盘、鼠标、触摸屏等)事件的核心组件。在嵌入式系统中,输入子系统的设计需要特别考虑资源限制、实时性要求等因素。
从应用层到硬件最底层分为:用户空间(应用层)、内核空间(输入系统事件层、输入系统核心层、输入系统驱动层)、硬件(如键盘、鼠标、触摸屏等):
假设用户程序直接访问/dev/input/event0 设备节点,或者使用 tslib 访问设备节点的流程如下:
- APP 发起读操作,若无数据则休眠
- 用户操作设备,硬件上产生中断
- 输入系统驱动层对应的驱动程序处理中断:读取到数据,转换为标准的输入事件,向核心层汇报,输入事件就是一个“ struct input_event”结构体
- 核心层可以决定把输入事件转发给上面哪个 handler 来处理
- 从 handler 的名字来看,它就是用来处输入操作的。有多种 handler,比如: evdev_handler、 kbd_handler、 joydev_handler 等等
- 最常用的是 evdev_handler:它只是把 input_event 结构体保存在内核 buffer 等, APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备,每个 APP 都可以获得同一份输入事件
- 当 APP 正在等待数据时, evdev_handler 会把它唤醒,这样 APP 就可以返回数据
- APP 对输入事件的处理
- APP 获得数据的方法有 2 种:
- 直接访问设备节点(比如/dev/input/event0,1,2,…)
- 通过 tslib、 libinput 这类库来间接访问设备节点,这些库简化了对数据的处理
- APP 获得数据的方法有 2 种:
2. Linux设备输入数据的几个结构体
基于编写应用程序的角度,只需要理解这些内容:
2.1 内核中表示一个输入设备的结构体
使用 input_dev 结构体来表示输入设备:
/* include/linux/input.h */
struct input_dev {const char *name; /* 设备名称 */const char *phys; /* 设备物理路径 */const char *uniq; /* 设备唯一标识符 */struct input_id id; /* 输入设备ID */unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; /* 设备属性位图 */unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 支持的事件类型位图 */unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 支持的按键位图 */unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 支持的相对轴位图 */unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 支持的绝对轴位图 */unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 支持的多用途位图 */unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /* 支持的LED位图 */unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /* 支持的声音位图 */unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 支持的力反馈位图 */unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /* 支持的开关位图 */unsigned int hint_events_per_packet; /* 每个包的事件提示数 */unsigned int keycodemax; /* 键码最大值 */unsigned int keycodesize; /* 键码大小 */void *keycode; /* 键码映射表 */int (*setkeycode)(struct input_dev *dev,const struct input_keymap_entry *ke,unsigned int *old_keycode); /* 设置键码的回调函数 */int (*getkeycode)(struct input_dev *dev,struct input_keymap_entry *ke); /* 获取键码的回调函数 */struct ff_device *ff; /* 力反馈设备 */unsigned int repeat_key; /* 重复键值 */struct timer_list timer; /* 重复定时器 */int rep[REP_CNT]; /* 重复参数 */struct input_mt *mt; /* 多点触控数据 */struct input_absinfo *absinfo; /* 绝对轴信息数组 */unsigned long key[BITS_TO_LONGS(KEY_CNT)]; /* 当前按键状态位图 */unsigned long led[BITS_TO_LONGS(LED_CNT)]; /* 当前LED状态位图 */unsigned long snd[BITS_TO_LONGS(SND_CNT)]; /* 当前声音状态位图 */unsigned long sw[BITS_TO_LONGS(SW_CNT)]; /* 当前开关状态位图 */int (*open)(struct input_dev *dev); /* 打开设备的回调函数 */void (*close)(struct input_dev *dev); /* 关闭设备的回调函数 */int (*flush)(struct input_dev *dev, struct file *file); /* 刷新设备的回调函数 */int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value); /* 事件处理回调函数 */struct input_handle __rcu *grab; /* 抓取的句柄 */spinlock_t event_lock; /* 事件锁 */struct mutex mutex; /* 互斥锁 */unsigned int users; /* 用户计数 */bool going_away; /* 是否正在移除 */struct device dev; /* 设备结构体 */struct list_head h_list; /* 句柄列表 */struct list_head node; /* 设备节点 */unsigned int num_vals; /* 当前值的数量 */unsigned int max_vals; /* 最大值的数量 */struct input_value *vals; /* 当前值数组 */bool devres_managed; /* 设备资源管理标志 */
};
2.2 APP从输入设备获取的数据类型结构体
可以得到一系列的输入事件,就是一个一个“ struct input_event”:
/* include/uapi/linux/input.h */
struct input_event {struct timeval time; /* 事件发生的时间戳 */__u16 type; /* 事件类型 */__u16 code; /* 事件代码 */__s32 value; /* 事件值 */
};/* include/uapi/linux/time.h */
struct timeval {__kernel_time_t tv_sec; /* 秒 */__kernel_suseconds_t tv_usec; /* 微秒 */
};
其中事件类型type:
/* include/uapi/linux/input-event-codes.h */
/** 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 //LED事件。表示LED状态的变化。用于表示LED的状态变化,如键盘上的Caps Lock灯。
#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)
-
KEY_:表示键盘上的具体按键,如
KEY_A
表示“A”键,KEY_NUMLOCK
表示数字锁定键等。这些按键代码通常定义在/usr/include/linux/input-event-codes.h
文件中。 -
BTN_:虽然主要用于按钮类设备,但某些键盘上的特殊按键或功能也可能使用
BTN_
前缀的代码,如BTN_LEFT
(通常用于鼠标,但在某些上下文中可能表示键盘上的特殊功能键)。
-
-
鼠标事件
-
REL_:表示相对坐标事件,如
REL_X
和REL_Y
分别表示鼠标在X轴和Y轴上的相对移动量,REL_WHEEL
表示鼠标滚轮的滚动方向和量。 -
BTN_:表示鼠标按钮的按下和抬起事件,如
BTN_LEFT
(左键)、BTN_RIGHT
(右键)、BTN_MIDDLE
(中键/滚轮键)等。
-
-
触摸屏事件
-
ABS_:表示绝对坐标事件,如
ABS_MT_POSITION_X
和ABS_MT_POSITION_Y
分别表示触摸屏上触摸点的X轴和Y轴坐标。多点触控协议中可能还包含其他ABS_类型的代码,如ABS_MT_SLOT
(表示多点触控中的触点编号)、ABS_MT_TRACKING_ID
(用于跟踪触点的唯一标识符)等。 -
BTN_TOUCH:表示触摸屏上的触摸事件,当触摸发生时,该代码的值会被设置为1。
-
-
其他事件类型
- EV_MSC、EV_SW、EV_LED、EV_SND、EV_REP、EV_FF、EV_PWR等事件类型也有各自的
code
代码,但这些类型通常用于更专业的输入设备或特定功能,如杂项输入(MSC)、开关输入(SW)、LED控制(LED)、声音输出(SND)、自动重复(REP)、力反馈(FF)和电源管理(PWR)等。
- EV_MSC、EV_SW、EV_LED、EV_SND、EV_REP、EV_FF、EV_PWR等事件类型也有各自的
其中事件值value:
-
按键事件(EV_KEY)
-
0:表示按键被释放或未按下。
-
1:表示按键被按下。
-
2(或其他非0非1值,取决于具体实现):在某些情况下,可能表示按键的长按或其他特殊状态,但这并不是标准的用法,更多依赖于具体设备和驱动程序的实现。
-
-
相对坐标事件(EV_REL)
-
对于
REL_X
和REL_Y
等相对坐标事件,value
表示在X轴或Y轴上的相对移动量。正值通常表示向右或向下的移动,负值表示向左或向上的移动。 -
对于
REL_WHEEL
等滚轮事件,value
的正负表示滚轮滚动的方向,而绝对值通常表示滚动的量或步数。例如,value
为1可能表示滚轮向上滚动一步,而-1则表示向下滚动一步。
-
-
绝对坐标事件(EV_ABS)
-
对于触摸屏和绝对定位设备,
value
表示触摸点或设备在X轴、Y轴上的绝对位置,或者表示其他绝对量(如压力、大小等)。 -
具体的含义取决于
code
字段的值。例如,ABS_X
和ABS_Y
分别表示触摸点在X轴和Y轴上的位置,而ABS_PRESSURE
则表示触摸点的压力值。
-
-
其他事件类型
-
对于其他事件类型(如EV_MSC、EV_SW、EV_LED等),
value
的含义可能因事件类型而异。这些类型的事件通常用于更专业的输入设备或特定功能。 -
例如,在EV_MSC(杂项事件)中,
value
可能用于表示设备的特定状态或配置。在EV_SW(开关事件)中,value
可能表示开关的打开或关闭状态。
-
事件之间的界线:
APP 读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报 X、 Y 位置信息,也可能会上报压力值。
APP 怎么知道它已经读到了完整的数据?
驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完毕。 APP 读到“同步事件”时,就知道已经读完了当前的数据。同步事件也是一个 input_event 结构体,它的 type、 code、 value 三项都是 0。
输入子系统支持:阻塞、非阻塞、 POLL/SELECT、异步通知的机制。
3. 查看LCD设备信息和输入数据
使用韦东山老师IMX6ULL Pro开发板和配套的LCD屏进行实操。
3.1 查看设备信息
输入设备的设备节点名为/dev/input/eventX(也可能是/dev/eventX, X 表示 0、 1、 2 等数字)。查看设备节点,可以执行以下命令:
ls /dev/input/* -l
或
ls /dev/event* -l
可以看到下图类似下面的信息:
可以在板子上执行以下命令,查看这些设备节点对应什么硬件:
cat /proc/bus/input/devices
这条指令的含义就是获取与 event 对应的相关设备信息,可以看到类似以下的结果:
-
I:id of the device(设备 ID):该参数由结构体 struct input_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(if device has it):设备的唯一标识码
-
H:list of input handles associated with the device:与设备关联的输入句柄列表
-
B:bitmaps(位图)
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。
举一个例子,“ B: ABS=2658000 3”如何理解?
它表示该设备支持 EV_ABS 这一类事件中的哪一些事件。这是 2 个 32 位的数 字: 0x2658000、 0x3, 高位在 前低 位在 后, 组成一 个 64 位 的数字 :“ 0x2658000,00000003”,数值为 1 的位有: 0、 1、 47、 48、 50、 53、 54,即:0、 1、 0x2f、 0x30、 0x32、 0x35、 0x36,对应以下这些宏:
即这款输入设备支持上述的 ABS_X 、 ABS_Y 、 ABS_MT_SLOT 、 ABS_MT_TOUCH_MAJOR 、 ABS_MT_WIDTH_MAJOR 、 ABS_MT_POSITION_X 、ABS_MT_POSITION_Y 这些绝对位置事件。
3.2 使用命令读取输入的数据
调试输入系统时,直接执行类似下面的命令,然后操作对应的输入设备即可读出数据:
hexdump /dev/input/event0
在开发板上执行上述命令之后,点击按键或是点击触摸屏,就会打印下图信息:
上图中的 type 为 3 , 对应 EV_ABS ; code 为 0x35 对应 ABS_MT_POSITION_X; code 为 0x36 对应 ABS_MT_POSITION_Y。
上图中还发现有 2 个同步事件:它的 type、 code、 value 都为 0。表示电容屏上报了 2 次完整的数据。
3.3 4种机制获取LCD设备输入数据
读取输入数据的4种方式是:查询方式、休眠唤醒方式、poll方式、异步通知方式。
编程获取LCD设备输入数据之前需要先了解如何获取设备信息。
3.3.1 使用ioctl获取设备信息
ioctl
(input/output control)是Linux中一个非常强大且灵活的函数,用于对设备文件的特定操作进行控制。这个函数提供了一种机制,使得用户空间程序能够向设备驱动程序发送控制命令或查询设备状态,而这些操作通常无法通过普通的read/write调用完成。
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
输入参数:
fd
:文件描述符,指向要控制的设备文件。request
:设备特定的请求码,用于告诉驱动程序要执行哪种操作。...
:一个可变参数列表,具体取决于request
,可以是整数、指针等。
返回值:
- 成功时,
ioctl
返回0; - 失败时,返回-1,并设置
errno
以指示错误类型。
有些驱动程序对 request 的格式有要求,它的格式如下:
比如 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 */
开发板示例代码:
#include <linux/input.h> // 输入设备相关的头文件
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态相关函数
#include <fcntl.h> // 文件控制选项
#include <sys/ioctl.h> // 输入输出控制函数
#include <stdio.h> // 标准输入输出函数/** 程序入口点* 参数:* argc: 命令行参数个数* argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd; // 文件描述符int err; // 错误码int len; // 读取长度int i; // 循环变量unsigned char byte; // 用于处理位掩码的字节int bit; // 位索引struct input_id id; // 输入设备ID结构体unsigned int evbit[2]; // 用于存储事件类型位掩码的数组// 事件类型名称数组char *ev_names[] = {"EV_SYN ", // 同步事件"EV_KEY ", // 按键事件"EV_REL ", // 相对事件"EV_ABS ", // 绝对事件"EV_MSC ", // 其他事件"EV_SW ", // 开关事件"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"EV_LED ", // LED事件"EV_SND ", // 声音事件"NULL ", // 保留"EV_REP ", // 重复事件"EV_FF ", // 力反馈事件"EV_PWR ", // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件fd = open(argv[1], O_RDWR);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor = 0x%x\n", id.vendor ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1<<bit)) { // 检查当前位是否为1printf("%s ", ev_names[i*8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 关闭设备文件close(fd);return 0;
}
开发板示例代码分析:
使用ioctl获取device ID时,参考驱动程序evdev_do_ioctl,需要一个input_id结构体存放device ID的信息:
ioctl中需要使用EVIOCGBIT来获取evbit,分析驱动程序evdev_do_ioctl,_IOC_READ读取信息调用handle_eviocgbit函数中的case 0最终获取evbit:
最后把evbit中读到的数据和ev_names(ev_names中定义的是时间类型type,参考:2.2 APP从输入设备获取的数据类型结构体)中的字符串进行比较,打印出支持的event类型。
上机实验:
3.3.2 查询方式(Polling)和休眠唤醒方式(Blocking I/O)获取设备输入的信息
-
查询方式定义
- 查询方式:在查询方式中,进程不断地检查设备的状态,以确定是否有数据可以读取或写入。这种方式通常通过循环来实现。
-
特点
-
主动检查:进程主动检查设备状态,不依赖于外部事件。
-
CPU占用高:频繁的检查会导致CPU占用率高,不适合高负载应用。
-
实时性:可以实现较高的实时性,因为不需要等待外部中断。
-
查询方式读取数据通用示例代码:
#include <unistd.h> #include <fcntl.h> #include <stdio.h>int main() {int fd = open("/dev/mydevice", O_RDONLY);if (fd < 0) {perror("open");return 1;}char buffer[1024];while (1) {ssize_t bytes_read = read(fd, buffer, sizeof(buffer));if (bytes_read > 0) {// 处理读取的数据printf("Read %zd bytes\n", bytes_read);} else if (bytes_read == 0) {// 设备关闭break;} else {perror("read");break;}// 短暂休眠以减少CPU占用usleep(1000);}close(fd);return 0; }
-
-
休眠唤醒方式定义
- 休眠唤醒方式:在休眠唤醒方式中,进程在没有数据可读或写时进入休眠状态,等待设备产生中断或数据准备好后再被唤醒。这种方式通常通过阻塞I/O操作实现。
-
特点
-
被动等待:进程在没有数据可读或写时进入休眠状态,不占用CPU资源。
-
CPU占用低:只有在数据准备好时才占用CPU资源。
-
实时性较低:需要等待设备中断或内核调度,实时性不如查询方式。
-
休眠唤醒方式读取数据通用示例代码示例代码:
#include <unistd.h> #include <fcntl.h> #include <stdio.h>int main() {int fd = open("/dev/mydevice", O_RDONLY);if (fd < 0) {perror("open");return 1;}char buffer[1024];while (1) {ssize_t bytes_read = read(fd, buffer, sizeof(buffer));if (bytes_read > 0) {// 处理读取的数据printf("Read %zd bytes\n", bytes_read);} else if (bytes_read == 0) {// 设备关闭break;} else {perror("read");break;}}close(fd);return 0; }
-
开发板示例代码:
#include <linux/input.h> // 输入设备相关的头文件
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态相关函数
#include <fcntl.h> // 文件控制选项
#include <sys/ioctl.h> // 输入输出控制函数
#include <stdio.h> // 标准输入输出函数
#include <string.h> // 字符串操作函数
#include <unistd.h> // UNIX标准函数/* * 程序入口点* 参数:* argc: 命令行参数个数* argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd; // 文件描述符int err; // 错误码int len; // 读取长度int i; // 循环变量unsigned char byte; // 用于处理位掩码的字节int bit; // 位索引struct input_id id; // 输入设备ID结构体unsigned int evbit[2]; // 用于存储事件类型位掩码的数组struct input_event event;// 输入事件结构体// 事件类型名称数组char *ev_names[] = {"EV_SYN ", // 同步事件"EV_KEY ", // 按键事件"EV_REL ", // 相对事件"EV_ABS ", // 绝对事件"EV_MSC ", // 其他事件"EV_SW ", // 开关事件"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"EV_LED ", // LED事件"EV_SND ", // 声音事件"NULL ", // 保留"EV_REP ", // 重复事件"EV_FF ", // 力反馈事件"EV_PWR ", // 电源事件};// 检查命令行参数if (argc < 2){printf("Usage: %s <dev> [noblock]\n", argv[0]); // 打印使用说明return -1;}// 检查是否指定了非阻塞模式if (argc == 3 && !strcmp(argv[2], "noblock")){fd = open(argv[1], O_RDWR | O_NONBLOCK); // 以读写和非阻塞模式打开设备}else{fd = open(argv[1], O_RDWR); // 以读写模式打开设备}if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor = 0x%x\n", id.vendor ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1<<bit)) { // 检查当前位是否为1printf("%s ", ev_names[i*8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 无限循环读取并处理输入事件while (1){len = read(fd, &event, sizeof(event)); // 读取一个输入事件if (len == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}else{printf("read err %d\n", len); // 读取错误}}return 0;
}
开发板示例代码分析:
相比使用ioctl获取设备信息的代码相比,在open文件时多了O_NONBLOCK的判断:
使用O_NONBLOCK方式open文件时,表示“非阻塞”,APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据,否则也会立刻返回错误;
不使用O_NONBLOCK方式open文件时,表示“阻塞”,APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据;否则APP就会在内核态休眠,当有数据时驱动程序会把APP唤醒,read函数恢复执行并返回数据给APP。
通过分析驱动程序evdev_read:
增加了一个input_event结构体的event变量,在驱动程序evdev_read中读取到的数据就是从input_event结构体类型的变量拷贝到用户空间的buffer:
上机实验:
3.3.3 poll方式(Polling with poll)获取设备输入的信息
-
定义
- poll方式:
poll
是一个后来引入的多路复用I/O系统调用,旨在解决select
的一些限制。它允许一个进程监视多个文件描述符,等待其中任何一个文件描述符变为可读、可写或发生异常。
- poll方式:
-
函数原型
-
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
-
参数
-
fds
:一个struct pollfd
数组,每个元素包含一个文件描述符及其感兴趣的事件。 -
nfds
:数组中元素的数量。struct pollfd {int fd; // 文件描述符short events; // 要监视的事件short revents;// 实际发生的事件 };
-
timeout
:等待的超时时间,以毫秒为单位,可以是-1
表示无限期等待。注:**
fds
**中events有多种类型,如下图:
-
-
特点
-
动态大小的文件描述符集合:
poll
使用struct pollfd
数组来表示文件描述符集合,数组的大小是动态的,没有固定的上限。 -
不需要重新初始化:
poll
不会修改传入的struct pollfd
数组,因此不需要在每次调用前重新初始化。 -
更好的性能:
poll
在处理大量文件描述符时性能更好,因为它只需要遍历实际使用的文件描述符,而不是固定的集合。 -
poll方式读取数据通用示例代码示例代码:
#include <poll.h> #include <unistd.h> #include <stdio.h>int main() {int fd1 = 0; // 标准输入int fd2 = 1; // 标准输出struct pollfd fds[1];fds[0].fd = fd1;fds[0].events = POLLIN;int ret = poll(fds, 1, 5000); // 5000毫秒超时if (ret < 0) {perror("poll");return 1;} else if (ret == 0) {printf("Timeout occurred! No data after 5 seconds.\n");} else {if (fds[0].revents & POLLIN) {char buffer[1024];ssize_t bytes_read = read(fd1, buffer, sizeof(buffer));if (bytes_read > 0) {printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, buffer);}}}return 0; }
-
开发板示例代码:
#include <linux/input.h> // 输入设备相关的头文件
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态相关函数
#include <fcntl.h> // 文件控制选项
#include <sys/ioctl.h> // 输入输出控制函数
#include <stdio.h> // 标准输入输出函数
#include <string.h> // 字符串操作函数
#include <unistd.h> // UNIX标准函数
#include <poll.h> // poll相关函数和结构体/** 获取输入设备信息并持续监听事件* 参数:* argc: 命令行参数个数* argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd; // 文件描述符int err; // 错误码int len; // 读取长度int ret; // poll返回值int i; // 循环变量unsigned char byte; // 用于处理位掩码的字节int bit; // 位索引struct input_id id; // 输入设备ID结构体unsigned int evbit[2]; // 用于存储事件类型位掩码的数组struct input_event event; // 输入事件结构体struct pollfd fds[1]; // pollfd结构体数组nfds_t nfds = 1; // 监视的文件描述符数量// 事件类型名称数组char *ev_names[] = {"EV_SYN ", // 同步事件"EV_KEY ", // 按键事件"EV_REL ", // 相对事件"EV_ABS ", // 绝对事件"EV_MSC ", // 其他事件"EV_SW ", // 开关事件"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"EV_LED ", // LED事件"EV_SND ", // 声音事件"NULL ", // 保留"EV_REP ", // 重复事件"EV_FF ", // 力反馈事件"EV_PWR ", // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件,使用非阻塞模式fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor = 0x%x\n", id.vendor ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 持续监听事件while (1){fds[0].fd = fd; // 设置文件描述符fds[0].events = POLLIN; // 设置要监视的事件(可读)fds[0].revents = 0; // 清除实际发生的事件ret = poll(fds, nfds, 5000); // 调用poll,等待5000毫秒if (ret > 0){if (fds[0].revents & POLLIN) // 检查是否发生可读事件{while (read(fd, &event, sizeof(event)) == sizeof(event)) // 读取事件{printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}}}else if (ret == 0){printf("time out\n"); // 超时}else{printf("poll err\n"); // poll出错}}return 0;
}
开发板示例代码分析:
分析驱动程序evdev_poll主要是将文件描述符添加到等待队列中,以便在文件描述符的状态发生变化时,能够通知调用者:
上机实验:
3.3.4 selsct方式(Polling with select)获取设备输入的信息
-
定义
- select方式:
select
是一个早期的多路复用I/O系统调用,广泛应用于Unix和类Unix系统中。它允许一个进程监视多个文件描述符,等待其中任何一个文件描述符变为可读、可写或发生异常。
- select方式:
-
函数原型:
-
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
-
参数:
nfds
:要监视的最大文件描述符值加1。readfds
:要监视的可读文件描述符集合。writefds
:要监视的可写文件描述符集合。exceptfds
:要监视的异常条件文件描述符集合。timeout
:等待的超时时间,可以是NULL
表示无限期等待。
-
特点
-
固定大小的文件描述符集合:
select
使用fd_set
结构来表示文件描述符集合,fd_set
的大小是固定的,通常是1024个文件描述符。 -
需要重新初始化:每次调用
select
之前,都需要重新初始化fd_set
,因为select
会修改这些集合。 -
性能问题:随着文件描述符数量的增加,
select
的性能会下降,因为它需要遍历整个fd_set
来检查每个文件描述符的状态。 -
select方式读取数据通用示例代码示例代码:
#include <sys/select.h> #include <unistd.h> #include <stdio.h>int main() {int fd1 = 0; // 标准输入int fd2 = 1; // 标准输出fd_set readfds;FD_ZERO(&readfds);FD_SET(fd1, &readfds);struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;int ret = select(fd1 + 1, &readfds, NULL, NULL, &timeout);if (ret < 0) {perror("select");return 1;} else if (ret == 0) {printf("Timeout occurred! No data after 5 seconds.\n");} else {if (FD_ISSET(fd1, &readfds)) {char buffer[1024];ssize_t bytes_read = read(fd1, buffer, sizeof(buffer));if (bytes_read > 0) {printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, buffer);}}}return 0; }
-
开发板示例代码:
#include <linux/input.h> // 输入设备相关的头文件
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态相关函数
#include <fcntl.h> // 文件控制选项
#include <sys/ioctl.h> // 输入输出控制函数
#include <stdio.h> // 标准输入输出函数
#include <string.h> // 字符串操作函数
#include <unistd.h> // UNIX标准函数
#include <sys/select.h> // select相关函数和结构体
#include <sys/time.h> // 时间相关的头文件/** 获取输入设备信息并持续监听事件* 参数:* argc: 命令行参数个数* argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd; // 文件描述符int err; // 错误码int len; // 读取长度int ret; // select返回值int i; // 循环变量unsigned char byte; // 用于处理位掩码的字节int bit; // 位索引struct input_id id; // 输入设备ID结构体unsigned int evbit[2]; // 用于存储事件类型位掩码的数组struct input_event event; // 输入事件结构体int nfds; // 最大的文件描述符加1struct timeval tv; // 超时时间fd_set readfds; // 用于select的读文件描述符集合// 事件类型名称数组char *ev_names[] = {"EV_SYN ", // 同步事件"EV_KEY ", // 按键事件"EV_REL ", // 相对事件"EV_ABS ", // 绝对事件"EV_MSC ", // 其他事件"EV_SW ", // 开关事件"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"EV_LED ", // LED事件"EV_SND ", // 声音事件"NULL ", // 保留"EV_REP ", // 重复事件"EV_FF ", // 力反馈事件"EV_PWR ", // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件,使用非阻塞模式fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor = 0x%x\n", id.vendor ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 持续监听事件while (1){// 设置超时时间tv.tv_sec = 5; // 超时时间为5秒tv.tv_usec = 0; // 微秒部分为0// 初始化读文件描述符集合FD_ZERO(&readfds); // 先全部清零FD_SET(fd, &readfds); // 将文件描述符fd添加到读集合中// 设置最大的文件描述符加1nfds = fd + 1; // nfds 是最大的文件描述符加1// 调用select,等待文件描述符变为可读或超时ret = select(nfds, &readfds, NULL, NULL, &tv);if (ret > 0) // 有文件描述符准备好{// 再次确认fd有数据if (FD_ISSET(fd, &readfds)){// 读取并处理事件while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}}}else if (ret == 0) // 超时{printf("time out\n"); // 打印超时信息}else // -1: error{printf("select err\n"); // 打印错误信息}}return 0;
}
开发板示例代码分析:
在内核空间中,驱动程序需要支持 select
,通常通过实现 poll
方法来实现。poll
方法是 select
的底层实现,允许内核知道哪些文件描述符已经准备好进行I/O操作。参考poll函数的代码分析。
上机实验:
3.3.5 异步通知方式(Asynchronous I/O)获取设备输入的信息
-
定义
- 异步通知方式:在异步通知方式中,进程发起I/O请求后立即返回,继续执行其他任务。当I/O操作完成时,系统通过回调函数或信号等方式通知进程。这种方式通常通过
aio
(Asynchronous I/O)库实现。
- 异步通知方式:在异步通知方式中,进程发起I/O请求后立即返回,继续执行其他任务。当I/O操作完成时,系统通过回调函数或信号等方式通知进程。这种方式通常通过
-
函数原型
-
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
-
-
参数
-
signum
:指定要处理的信号编号。信号编号是一个整数,用于唯一标识一个信号。例如,SIGINT
表示中断信号(通常由Ctrl+C产生)。 -
handler
:是一个指向函数的指针,该函数用于处理指定的信号。这个函数应该接受一个整型参数(即信号编号),并且没有返回值(即返回类型为void
)。当信号signum
发生时,系统将调用这个函数。 -
返回值:如果调用成功,
signal
函数返回之前的信号处理函数指针;如果调用失败,返回SIG_ERR
。
-
-
信号处理函数
handler
参数可以指向以下几种类型的处理函数:- 用户自定义的处理函数:程序员可以编写自己的信号处理函数,并将其地址作为
handler
参数传递给signal
函数。当信号发生时,系统将调用这个函数。 - 忽略信号:通过将
handler
设置为SIG_IGN
,可以告诉系统忽略指定的信号。但是,请注意,有些信号是不能被忽略的,如SIGKILL
和SIGSTOP
。 - 采用默认处理:通过将
handler
设置为SIG_DFL
,可以告诉系统采用默认的信号处理机制。对于大多数信号来说,默认的处理机制是终止进程。
- 用户自定义的处理函数:程序员可以编写自己的信号处理函数,并将其地址作为
-
特点
-
非阻塞:进程发起I/O请求后立即返回,不等待I/O操作完成。
-
高效:适合高并发和高性能应用,可以充分利用CPU资源。
-
复杂:需要处理回调函数或信号,编程复杂度较高。
-
异步通知方式使用single函数读取数据通用示例代码示例代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <linux/input.h> #include <signal.h>/* 信号处理函数 */ void handle_sigio(int signum) {static struct input_event event;int fd = fileno(stdin); // 假设我们监听的是标准输入,实际应用中应替换为设备文件描述符// 读取事件ssize_t bytes_read = read(fd, &event, sizeof(event));if (bytes_read == sizeof(event)) {printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);} else if (bytes_read == -1) {perror("read");} else {printf("Partial read: %zd bytes\n", bytes_read);} }/* 主函数 */ int main(int argc, char **argv) {int fd;struct sigaction sa;if (argc != 2) {fprintf(stderr, "Usage: %s <device>\n", argv[0]);return -1;}// 打开设备文件fd = open(argv[1], O_RDONLY | O_NONBLOCK);if (fd == -1) {perror("open");return -1;}// 设置文件描述符的所有者fcntl(fd, F_SETOWN, getpid());// 设置文件描述符为异步通知fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_ASYNC);// 注册信号处理函数memset(&sa, 0, sizeof(sa));sa.sa_handler = handle_sigio;sa.sa_flags = 0;if (sigaction(SIGIO, &sa, NULL) == -1) {perror("sigaction");return -1;}// 主循环while (1) {pause(); // 等待信号}close(fd);return 0; }
-
开发板示例代码:
#include <linux/input.h> // 输入设备相关的头文件
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态相关函数
#include <fcntl.h> // 文件控制选项
#include <sys/ioctl.h> // 输入输出控制函数
#include <stdio.h> // 标准输入输出函数
#include <string.h> // 字符串操作函数
#include <unistd.h> // UNIX标准函数
#include <signal.h> // 信号处理相关函数int fd; // 文件描述符// 信号处理函数
void my_sig_handler(int sig)
{struct input_event event; // 输入事件结构体// 读取并处理所有可用的输入事件while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);}
}/* ./05_input_read_fasync /dev/input/event0 */
int main(int argc, char **argv)
{int err; // 错误码int len; // 读取长度int ret; // 通用返回值int i; // 循环变量unsigned char byte; // 用于处理位掩码的字节int bit; // 位索引struct input_id id; // 输入设备ID结构体unsigned int evbit[2]; // 用于存储事件类型位掩码的数组unsigned int flags; // 文件描述符标志int count = 0; // 计数器// 事件类型名称数组char *ev_names[] = {"EV_SYN ", // 同步事件"EV_KEY ", // 按键事件"EV_REL ", // 相对事件"EV_ABS ", // 绝对事件"EV_MSC ", // 其他事件"EV_SW ", // 开关事件"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"NULL ", // 保留"EV_LED ", // LED事件"EV_SND ", // 声音事件"NULL ", // 保留"EV_REP ", // 重复事件"EV_FF ", // 力反馈事件"EV_PWR ", // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 注册信号处理函数signal(SIGIO, my_sig_handler); // 注册SIGIO信号处理函数// 打开驱动程序fd = open(argv[1], O_RDWR | O_NONBLOCK); // 以读写模式和非阻塞模式打开设备文件if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id); // 使用ioctl获取设备ID信息if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor = 0x%x\n", id.vendor ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); // 使用ioctl获取事件类型位掩码if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 把APP的进程号告诉驱动程序fcntl(fd, F_SETOWN, getpid()); // 设置文件描述符的所有者为当前进程// 使能"异步通知"flags = fcntl(fd, F_GETFL); // 获取文件描述符的当前标志fcntl(fd, F_SETFL, flags | FASYNC); // 设置文件描述符为异步通知模式// 主循环while (1){printf("main loop count = %d\n", count++); // 打印主循环计数sleep(2); // 暂停2秒}return 0;
}
开发板示例代码分析:
需要补充fcntl函数知识:
-
函数原型
-
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ...); /* 可变参数,根据cmd的值决定是否需要第三个参数 */
-
-
参数
fd
:文件描述符,表示要操作的文件。cmd
:操作命令,决定fcntl
函数的具体行为。...
:根据cmd
的值,可能需要一个额外的参数,该参数的类型可以是long
或struct flock *
。
-
功能(包括但不限于)
- 复制文件描述符:使用
F_DUPFD
命令可以复制一个现有的文件描述符。 - 获取/设置文件描述符标记:通过
F_GETFD
和F_SETFD
命令,可以获取或设置文件描述符的标记,如FD_CLOEXEC
。 - 获取/设置文件状态标记:使用
F_GETFL
和F_SETFL
命令,可以获取或设置文件的状态标记,如O_APPEND
、O_NONBLOCK
等。 - 获取/设置异步I/O所有权:通过
F_GETOWN
和F_SETOWN
命令,可以获取或设置接收SIGIO或SIGURG信号的进程ID或进程组ID。 - 获取/设置记录锁:使用
F_GETLK
、F_SETLK
和F_SETLKW
命令,可以获取、设置或测试文件的记录锁。
- 复制文件描述符:使用
上机实验: