IM6ULL学习总结(四-七-1)输入系统应用编程

第7章 输入系统应用编程

7.1 什么是输入系统

⚫ 先来了解什么是输入设备?

常见的输入设备有键盘、鼠标、遥控杆、书写板、触摸屏等等,用户通过这些输入设备与 Linux 系统进行数据交换。

⚫ 什么是输入系统?

输入设备种类繁多,能否统一它们的接口?既在驱动层面统一,也在应用程序层面统一?可以的。

Linux 系统为了统一管理这些输入设备,实现了一套能兼容所有输入设备的框架:输入系统。驱动开发人员基于这套框架开发出程序,应用开发人员就可以使用统一的 API 去使用设备。

7.2 输入系统框架及调试

7.2.1 框架概述

作为应用开发人员,可以只基于 API 使用输入子系统。但是了解内核中输入子系统的框架、了解数据流程,有助于解决开发过程中碰到的硬件问题、驱动问题

输入系统框架如图 7.1 所示:

用户空间:指的是APP层也就是我们所说的应用层。

APP可以直接访问驱动节点,也可通过库来访问

输入系统事件层:是给APP层提供访问接口

输入系统核心层:接受来自底层的事件

输入系统驱动层:硬件驱动层直接从硬件获取数据然后将数据转换成为事件上报

APP输入设备节点的数据流程:(可以通过直接访问设备节点或者使用库的方式)

1、APP 发起读操作,若无数据则休眠;(应用程序休眠)(这里是由上而下)

2、用户操作设备,硬件上产生中断;(这里开始是由下而上看)

3、输入系统驱动层对应的驱动程序处理中断:

读取到数据,转换为标准的输入事件,向核心层汇报。

所谓输入事件就是一个"struct input_event"结构体。

4、核心层可以决定把输入事件转发给上面哪个 handler 来处理

从 handler 的名字来看,它就是用来处输入操作的。有多种 handler,比

如:evdev_handler、kbd_handler、joydev_handler 等等。

最常用的是 evdev_handler:它只是把 input_event 结构体保存在内核

buffer 等,APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备,每个 APP 都可以获得同一份输入事件。(也就说一个输入事件可以被多个应用程序读取到)

当 APP 正在等待数据时,evdev_handler 会把它唤醒,这样 APP 就可以返回数据。

5、APP 对输入事件的处理:

APP 获 得 数 据 的 方 法 有 2 种 : 直 接 访 问 设 备 节 点 ( 比 如

/dev/input/event0,1,2,...),或者通过 tslib、libinput 这类库来间接访

问设备节点。这些库简化了对数据的处理。

7.2.2 编写 APP 需要掌握的知识

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

使用 input_dev 结构体来表示输入设备

c 复制代码
struct input_dev {
	const char *name;
	const char *phys;
	const char *uniq;
	struct input_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)];
	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)];
	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、APP可以得到什么数据?

可以得到一系列的输入事件,就是一个一个"struct input_event",

c 复制代码
struct input_event {
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;
};

struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};

每个输入事件 input_event 中都含有发生时间:timeval 表示的是"自系

统启动以来过了多少时间",它是一个结构体,含有"tv_sec、tv_usec"两项(即秒、微秒)。

输入事件 input_event 中更重要的是:type(哪类事件)、code(哪个事件)、

value(事件值),细讲如下:

1、type:表示哪类事件

比如 EV_KEY 表示按键类、EV_REL 表示相对位移(比如鼠标),EV_ABS 表示绝对位置(比如触摸屏)。

c 复制代码
/*
 * 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)

2.2、code:表示该类事件下的哪一个事件

比如对于 EV_KEY(按键)类事件,它表示键盘。键盘上有很多按键,比如数字键 1、2、3,字母键 A、B、C 里等。

c 复制代码
#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
c 复制代码
#define ABS_X			0x00
#define ABS_Y			0x01
#define ABS_Z			0x02
#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

2.3、value:表示事件值

对于按键,它的 value 可以是 0(表示按键被按下)、1(表示按键被松开)、

2(表示长按);

2.4、事件之间的界线

APP 读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报 X、Y 位置信息,也可能会上报压力值。

◼ APP 怎么知道它已经读到了完整的数据?

驱动程序上报完一系列的数据后,会上报一个"同步事件",表示数据上报完

毕。APP 读到"同步事件"时,就知道已经读完了当前的数据。

同步事件也是一个 input_event 结构体,它的 type、code、value 三项都

是 0。

3 、输入子系统支持完整的 API 操作

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

7.2.3 调试技巧

1、确定设备信息

输入设备的设备节点名为/dev/input/eventX(也可能是/dev/eventX,X

表示 0、1、2 等数字)。查看设备节点,可以执行以下命令:

c 复制代码
ls /dev/input/* -l
ls /dev/event* -l

怎么知道这些设备节点对应什么硬件呢?可以在板子上执行以下命令

c 复制代码
cat /proc/bus/input/devices

这条指令的含义就是获取与 event 对应的相关设备信息,可以看到类似以下的结果:

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

1、I:id 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(位图)

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

2、使用命令读取数据

c 复制代码
hexdump /dev/input/event0

在开发板上执行上述命令之后,点击按键或是点击触摸屏,就会打印图 7.11 信息:

图 7.11 中 的 type 为 3 ,对应 EV_ABS ; code 为 0x35 对 应ABS_MT_POSITION_X;code 为 0x36 对应 ABS_MT_POSITION_Y。

上图中还发现有 2 个同步事件:它的 type、code、value 都为 0。表示电

容屏上报了 2 次完整的数据。

7.3 不使用库的应用程序示例

7.3.1 输入系统支持完整的 API 操作

7.3.2 APP 访问硬件的 4 种方式:

妈妈怎么知道孩子醒了

1、时不时进房间看一下:查询方式

简单,但是累

2、进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒

不累,但是妈妈干不了活了

3、妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll 方式

要浪费点时间,但是可以继续干活。

4、妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知

妈妈、小孩互不耽误。

7.3.3 获取设备信息

通过 ioctl 获取设备信息,ioctl 的参数如下:

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

比如 dir 为_IOC_READ(即 2)时,表示 APP 要读数据;为_IOC_WRITE(即 4)时,表示 APP 要写数据。

⚫ size 表示这个 ioctl 能传输数据的最大字节数。

⚫ type、nr 的含义由具体的驱动程序决定。

比如要读取输入设备的 evbit 时,ioctl 的 request 要写为"EVIOCGBIT(0,

size)",size 的大小可以由你决定:你想读多少字节就设置为多少。这个宏的定义如下

c 复制代码
/* 这个程序是APP直接访问/dev/input/event0文件 
通过ioctl函数的特殊工能
如EVIOCGID   获取输入设备的id信息
EVIOCGBIT    获取输入设备支持的事件

*/

#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
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 ",
      "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_RDONLY:只读模式。O_WRONLY:只写模式。O_RDWR:读写模式
  if (fd < 0) {
    printf("open %s err\n", argv[1]);
    return -1;
  }
  err = ioctl(fd, EVIOCGID, &id); //获取输入id
  if (err == 0) {
    printf("bustype = 0x%x\n", id.bustype);
    printf("vendor	= 0x%x\n", id.vendor);
    printf("product = 0x%x\n", id.product);
    printf("version = 0x%x\n", id.version);
  }
  /* 获取输入设备支持的事件类型*/
  len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);/* EVIOCGBIT(0, sizeof(evbit)): 这是一个宏,用于生成一个特定的请求码。这个请求码告诉ioctl函数我们想要获取设备支持的事件类型的信息。第一个参数0指定我们对"顶层"的事件类型感兴趣,如按键事件、鼠标事件等。 */
  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)) {
          printf("%s", ev_names[i * 8 + bit]);
        }
      }
      printf("\n");
    }
  }
  return 0;
}

7.3.4 查询方式

APP 调用 open 函数时,传入"O_NONBLOCK"表示"非阻塞"。

APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read

函数会返回数据,否则也会立刻返回错误。

./02_input_read /dev/input/event1 nonblock

./02_input_read /dev/input/event1

两种调用方式 第一种方式O_NONBLOCK 没有输入事件时会返回 read err, 第二种 read不到APP会休眠

c 复制代码
/* 这个程序是APP直接访问/dev/input/event1文件  以非阻塞方式(查询方式读取)
通过ioctl函数的特殊工能
如EVIOCGID   获取输入设备的id信息
EVIOCGBIT    获取输入设备支持的事件
通过read函数 读取/dev/input/event1 当有输入事件触发时可以读到了
*/

#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv) {
  int fd;
  int err;
  int len;
  int i;
  unsigned char byte;
  int bit;
  struct input_id id; //输入设备id结构体
  struct input_event event;
  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 ",
      "EV_SND ", "NULL ",   "EV_REP ", "EV_FF	", "EV_PWR ",
  };
  if (argc <2) {
    printf("Usage :%s <dev> [nonblock]\n", argv[0]);
    return -1;
  }
  if (argc ==3&&!strcmp(argv[2], "nonblock"))/* strcmp函数是C语言中的一个字符串比较函数,用于比较两个字符串是否相等。其返回值为整型,具体如下:若str1等于str2,则返回0。若str1大于str2,则返回正整数。若str1小于str2,则返回负整数。 */
  {
    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;
  }
  err = ioctl(fd, EVIOCGID, &id); //获取输入id
  if (err == 0) {
    printf("bustype = 0x%x\n", id.bustype);
    printf("vendor	= 0x%x\n", id.vendor);
    printf("product = 0x%x\n", id.product);
    printf("version = 0x%x\n", id.version);
  }
  /* 获取输入设备支持的事件类型*/
  len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);/* EVIOCGBIT(0, sizeof(evbit)): 这是一个宏,用于生成一个特定的请求码。这个请求码告诉ioctl函数我们想要获取设备支持的事件类型的信息。第一个参数0指定我们对"顶层"的事件类型感兴趣,如按键事件、鼠标事件等。 */
  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)) {
          printf("%s", ev_names[i * 8 + bit]);
        }
      }
      printf("\n");
    }
  }
  while (1)
  {
    len = read(fd, &event, sizeof(event));
    if (len== sizeof(event))/* read函数用于从文件描述符中读取数据。其返回值表示实际读取的字节数,如果返回0表示已经读取到文件末尾 */
    {
        printf("get event : type = 0x%x,code = 0x%x,value = 0x%x\n",event.type,event.code,event.value);        
    }
    else
    {
        printf("read err\n");
    }
  }
  return 0;
}

7.3.5 休眠-唤醒方式

APP 调用 open 函数时,不要传入"O_NONBLOCK"。

APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read

函数会返回数据;否则 APP 就会在内核态休眠,当有数据时驱动程序会把 APP 唤

醒,read 函数恢复执行并返回数据给 APP。
和上面共用一个程序选择第二种方式运行

7.3.6 POLL/SELECT 方式

1、功能介绍

POLL 机制、SELECT 机制是完全一样的,只是 APP 接口函数不一样。

简单地说,它们就是"定个闹钟":在调用 poll、select 函数时可以传入

"超时时间"。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会立刻返回,否则等到"超时时间"结束时返回错误。

用法如下。

⚫ APP 先调用 open 函数时。

⚫ APP 不是直接调用 read 函数,而是先调用 poll 或 select 函数,这 2 个函数中可以传入"超时时间"。它们的作用是:如果驱动程序中有数据,则立刻返回;否则就休眠。在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把 APP唤醒,导致 poll 或 select 立刻返回;如果在"超时时间"内无人操作硬件,则时间到后 poll 或 select 函数也会返回。APP 可以根据函数的返回值判断返回

原因:有数据?无数据超时返回?

⚫ APP 根据 poll 或 select 的返回值判断有数据之后,就调用 read 函数读取数据时,这时就会立刻获得数据。

⚫ poll/select 函数可以监测多个文件,可以监测多种事件:

在调用 poll 函数时,要指明:

⚫ 你要监测哪一个文件:哪一个 fd

⚫ 你想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT

最后,在 poll 函数返回时,要判断状态

poll方法

c 复制代码
#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>
#include <poll.h>


/* ./01_get_input_info /dev/input/event0 */
int main(int argc, char **argv)
{
	int fd;
	int err;
	int len;
	int ret;
	int i;
	unsigned char byte;
	int bit;
	struct input_id id;
	unsigned int evbit[2];
	struct input_event event;
	struct pollfd fds[1];
	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 ",
		"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;
	}

	err = ioctl(fd, EVIOCGID, &id);
	if (err == 0)
	{
		printf("bustype = 0x%x\n", id.bustype );
		printf("vendor	= 0x%x\n", id.vendor  );
		printf("product = 0x%x\n", id.product );
		printf("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)) {
					printf("%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);
		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");
		}
	}

	return 0;
}

SELECT方式
SELECT使用方法

1、声明一个 fd_set readfds; 声明一个用于表示文件描述符集合的数据类型 这里是读监视

2、FD_ZERO(&readfds); 清楚fd_set

3、 FD_SET(fd, &readfds);将文件描述符(要监视的)添加到fd_set

4、select( nfds,&readfds, NULL, NULL , &tv); 调用select函数 正常:返回值为监控到的文件个数,返回0表示超时,返回-1表示错误 nfds 是最大的文件描述符+1 也就是fd+1 tv 是时间描述结构体

5、FD_ISSET(fd, &readfds) 判断 fd是不是在就绪文件中 返回值0 不在 返回值非零 在

c 复制代码
/* 这个程序是APP直接访问/dev/input/event1文件  以select机制
通过ioctl函数的特殊工能
如EVIOCGID   获取输入设备的id信息
EVIOCGBIT    获取输入设备支持的事件
通过read函数 读取/dev/input/event1 当有输入事件触发时可以读到了
这是都是以非阻塞或阻塞方式打开文件都无关了
*/

#include <bits/types/struct_timeval.h>
#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>

#include <sys/time.h>

int main(int argc, char **argv) {
  int fd;
  int err;
  int len;
  int i;
  char ret;
  unsigned char byte;
  int bit;
  struct input_id id; //输入设备id结构体
  struct input_event event;
  unsigned int evbit[2];
  int nfds;
  struct timeval tv;/* tv_sec 表示秒数,是一个长整型(long int)变量,用于表示自1970年1月1日以来的秒数。tv_usec 表示微秒数,是一个长整型(long int)变量,用于表示秒数之后的微秒部分。 */
  fd_set readfds;/* fd_set 是一个用于表示文件描述符集合的数据类型。它通常与一些 I/O 多路复用函数(如 select()、poll())一起使用,用于监视多个文件描述符的状态。 */
  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 ",
      "EV_SND ", "NULL ",   "EV_REP ", "EV_FF	", "EV_PWR ",
  };
  if (argc <2) {
    printf("Usage :%s <dev> [nonblock]\n", argv[0]);
    return -1;
  }
  if (argc ==3&&!strcmp(argv[2], "nonblock"))/* strcmp函数是C语言中的一个字符串比较函数,用于比较两个字符串是否相等。其返回值为整型,具体如下:若str1等于str2,则返回0。若str1大于str2,则返回正整数。若str1小于str2,则返回负整数。 */
  {
    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;
  }
  err = ioctl(fd, EVIOCGID, &id); //获取输入id
  if (err == 0) {
    printf("bustype = 0x%x\n", id.bustype);
    printf("vendor	= 0x%x\n", id.vendor);
    printf("product = 0x%x\n", id.product);
    printf("version = 0x%x\n", id.version);
  }
  /* 获取输入设备支持的事件类型*/
  len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);/* EVIOCGBIT(0, sizeof(evbit)): 这是一个宏,用于生成一个特定的请求码。这个请求码告诉ioctl函数我们想要获取设备支持的事件类型的信息。第一个参数0指定我们对"顶层"的事件类型感兴趣,如按键事件、鼠标事件等。 */
  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)) {
          printf("%s", ev_names[i * 8 + bit]);
        }
      }
      printf("\n");
    }
  }
 while(1)
 {
    /* 设置时间 */
    tv.tv_sec =5;
    tv.tv_usec =5;
    FD_ZERO(&readfds);/* 在使用 fd_set 之前,需要先初始化并清空它,可以使用 FD_ZERO() 宏来实现: */
    FD_SET(fd, &readfds);/* 接下来,可以使用 FD_SET() 宏将文件描述符添加到 fd_set 中: */
    nfds =fd +1; /* nfds 是最大的文件句柄+1, 注意: 不是文件个数, 这与poll不一样 */ 
    /* 	函数原型为		int select(int nfds, fd_set *readfds, fd_set *writefds,
    
              fd_set *exceptfds, struct timeval *timeout); 
        * 我们为了"read"而监测, 所以只需要提供readfds
    */
    /* nfds:需要监视的最大文件描述符值加一。
        readfds、writefds、exceptfds:分别是指向 fd_set 结构体的指针,用来指定需要监视的读、写、异常事件的文件描述符集合。
        timeout:是一个 struct timeval 结构体指针,用来指定 select() 的超时时间,如果设置为 NULL,select() 将一直阻塞直到有文件描述符就绪或被信号中断。
        select() 函数返回就绪文件描述符的数量,如果超时则返回 0,发生错误则返回 -1。
     */
   
    ret = select( nfds,&readfds, NULL, NULL , &tv);
    if (ret>0)
    {
        /*  */
        if(FD_ISSET(fd, &readfds))/* FD_ISSET 是一个宏,用于检查指定的文件描述符是否在 fd_set 中被设置(即就绪)。它接受两个参数:要检查的文件描述符和一个指向 fd_set 结构体的指针,然后返回一个非零值(true)表示该文件描述符已经被设置,否则返回 0(false)表示该文件描述符未被设置。 */
        {
            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;
}

7.3.7 异步通知方式

1、功能介绍

所谓同步,就是"你慢我等你"。

那么异步就是:你慢那你就自己玩,我做自己的事去了,有情况再通知我。

所谓异步通知,就是 APP 可以忙自己的事,当驱动程序用数据时它会主动给APP 发信号,这会导致 APP 执行信号处理函数。

仔细想想"发信号",这只有 3 个字,却可以引发很多问题:

⚫谁发:驱动程序发

⚫ 发什么:信号

⚫ 发什么信号:SIGIO

⚫ 怎么发:内核里提供有函数

⚫ 发给谁:APP,APP 要把自己告诉驱动

⚫ APP 收到后做什么:执行信号处理函数

⚫ 信号处理函数和信号,之间怎么挂钩:APP 注册信号处理函数

小孩通知妈妈的事情有很多:饿了、渴了、想找人玩。Linux 系统中也有很多信号,在 Linux 内核源文件include\uapi\asmgeneric\signal.h 中,有很多信号的宏定义:

驱动程序通知 APP 时,它会发出"SIGIO"这个信号,表示有"IO 事件"要处理。

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

除了注册 SIGIO 的处理函数,APP 还要做什么事?想想这几个问题:

⚫ 内核里有那么多驱动,你想让哪一个驱动给你发 SIGIO 信号?

APP 要打开驱动程序的设备节点。那个驱动

⚫ 驱动程序怎么知道要发信号给你而不是别人?APP 要把自己的进程 ID 告诉驱

动程序。哪个APP

⚫ APP 有时候想收到信号,有时候又不想收到信号:应该可以把 APP 的意愿告

诉驱动:设置 Flag 里面的 FASYNC 位为 1,使能"异步通知"。

2、应用编程

应用程序要做的事情有这几件:

2.1、编写信号处理函数:

c 复制代码
/**
 * @brief  APP接收到驱动设备节点的异步通知信号处理函数
 * @param  当信号处理函数被调用时,操作系统会将触发该函数调用的信号编号传递给这个形参。通过检查 signal_num 的值,可以确定是哪个信号触发了信号处理函数的调用。
 * @return NULL
 */
void my_sig_handler(int signal_num)
{
    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);		
	}

}

2.2、注册信号处理函数:

c 复制代码
 /* 注册信号处理函数 */
  signal(SIGIO, my_sig_handler);

2.3、打开驱动:

打开驱动(驱动设备节点)文件

2.4、把进程 ID 告诉驱动:

c 复制代码
  /* 把APP的进程号告诉驱动程序 */
  fcntl(fd,F_SETOWN,getpid());/* F_SETOWN 操作命令用于设置文件描述符 fd 的异步 I/O 操作的所有者进程。可以让当前进程成为该文件描述符的拥有者,从而接收与该文件描述符相关的信号。 */

2.5、使能驱动的 FASYNC 功能:

c 复制代码
  /* 使能异步通知 */
  flags = fcntl(fd, F_GETFL);/* 这个命令会获取文件描述符 fd 的状态标志,flags 变量将包含文件的打开方式和其他标志信息。 */
  fcntl(fd, F_SETFL,flags|FASYNC);
c 复制代码
/* 这个程序是APP直接访问/dev/input/event1文件  以异步通知方式
通过ioctl函数的特殊工能
如EVIOCGID   获取输入设备的id信息
EVIOCGBIT    获取输入设备支持的事件
通过read函数 读取/dev/input/event1 当有输入事件触发时可以读到了
*/

#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
int fd;
/**
 * @brief  APP接收到驱动设备节点的异步通知信号处理函数
 * @param  当信号处理函数被调用时,操作系统会将触发该函数调用的信号编号传递给这个形参。通过检查 signal_num 的值,可以确定是哪个信号触发了信号处理函数的调用。
 * @return NULL
 */
void my_sig_handler(int signal_num)
{
    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);		
	}

}
int main(int argc, char **argv) {

  int err;
  int len;
  int i;
  char ret;
  unsigned char byte;
  int bit;
  unsigned int flags;
  struct input_id id; //输入设备id结构体
  struct input_event event;
  unsigned int evbit[2];
  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 ",
      "EV_SND ", "NULL ",   "EV_REP ", "EV_FF	", "EV_PWR ",
  };
  if (argc <2) {
    printf("Usage :%s <dev> [nonblock]\n", argv[0]);
    return -1;
  }
  /* 注册信号处理函数 */
  signal(SIGIO, my_sig_handler);
  if (argc ==3&&!strcmp(argv[2], "nonblock"))/* strcmp函数是C语言中的一个字符串比较函数,用于比较两个字符串是否相等。其返回值为整型,具体如下:若str1等于str2,则返回0。若str1大于str2,则返回正整数。若str1小于str2,则返回负整数。 */
  {
    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;
  }
  err = ioctl(fd, EVIOCGID, &id); //获取输入id
  if (err == 0) {
    printf("bustype = 0x%x\n", id.bustype);
    printf("vendor	= 0x%x\n", id.vendor);
    printf("product = 0x%x\n", id.product);
    printf("version = 0x%x\n", id.version);
  }
  /* 获取输入设备支持的事件类型*/
  len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);/* EVIOCGBIT(0, sizeof(evbit)): 这是一个宏,用于生成一个特定的请求码。这个请求码告诉ioctl函数我们想要获取设备支持的事件类型的信息。第一个参数0指定我们对"顶层"的事件类型感兴趣,如按键事件、鼠标事件等。 */
  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)) {
          printf("%s", ev_names[i * 8 + bit]);
        }
      }
      printf("\n");
    }
  }
  /* 把APP的进程号告诉驱动程序 */
  fcntl(fd,F_SETOWN,getpid());/* F_SETOWN 操作命令用于设置文件描述符 fd 的异步 I/O 操作的所有者进程。可以让当前进程成为该文件描述符的拥有者,从而接收与该文件描述符相关的信号。 */
  /* 使能异步通知 */
  flags = fcntl(fd, F_GETFL);/* 这个命令会获取文件描述符 fd 的状态标志,flags 变量将包含文件的打开方式和其他标志信息。 */
  fcntl(fd, F_SETFL,flags|FASYNC);
  while(1)
  {
    printf("main loop count = %d\n", count++);
	sleep(2);
  }
  return 0;
}
相关推荐
雯宝1 小时前
mnt_init 函数
linux
UIU1142 小时前
递归算法与汉诺塔
c++·学习·算法·c#·递归
律宏阔7 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔7 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴7 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千8 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei378 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei378 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei378 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW8 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux