0%

输入系统应用编程基础

1. 输入系统框架及调试

1.1 框架概述

image-20230917174044630

  1. APP 发起读操作,若无数据则休眠;

  2. 用户操作设备,硬件上产生中断;

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

    读取到数据,转换为标准的输入事件,向核心层汇报。 所谓输入事件就是一个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 对输入事件的处理:

  6. APP获得数据的方法有2种:直接访问设备节点( 比如/dev/input/event0,1,2,...),或者通过tslib、libinput这类库来间接访问设备节点。这些库简化了对数据的处理。

1.2 编写APP需要掌握的知识

1.2.1 APP可以得到什么数据

image-20230918201156815

  • timeval结构体:

    • tv_sec:表示自1970年1月1日00:00:00(即UNIX纪元起点)以来的秒数
    • tv_usec:表示微秒数,即秒后面的零头
  • input_event结构体:

    • type:表示触发了哪类事件;比如EV_KEY表示按键类,EV_REL表示相对位移(例如鼠标),EV_ABS 表示 绝对位置(比如触摸屏)

      image-20230918202331798

    • code:表示该类事件下的哪一个事件;例如EV_KEY表示键盘,键盘上有很多按键,每个分别对应不同事件:

      image-20230918202559709

      对于EV_ABS事件,触摸屏提供绝对位置信息,包括X,Y还有压力方向的值:

      image-20230918202823767

    • value:表示事件的值;例如按键的值可以是0(按下)或者1(松开)

(1) 使用命令读取设备上报数据

调试输入系统时,直接执行类似下面的命令,然后操作对应的输入设备即可读出数据:

1
hexdump /dev/input/event0

部分输出如下:

image-20230918205712204

根据输出信息,结合input-event-codes.h,即可知道操作触发了那类事件中的那个事件,触发的值是什么

(2) 事件的结尾标识

APP读取输入事件时,不同设备可能会上报不同数量的事件,但是所有设备的驱动程序都会在上报所有的数据之后,最后上报一个“同步事件”,表示数据上报完成。APP读到该事件时知道已经读完当前数据。“同步事件”也是一个input_event结构体,他的type、code、value属性都为0,如上图所示。

1.2.2 查询开发板上有哪些设备节点

(1) 使用命令查询设备节点:
1
ls /dev/input/* -l

在开发板上输出:

1
2
crw-rw---- 1 root input 13, 64 Jan  1 00:00 /dev/input/event0
crw-rw---- 1 root input 13, 65 Jan 1 00:00 /dev/input/event1
(2) 使用命令查询设备信息及其对应的设备节点
1
cat /proc/bus/input/devices

部分输出:

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/input1
U: Uniq=
H: Handlers=event1 evbug //设备节点:/dev/input/event1
B: PROP=2
B: EV=b
B: KEY=1c00 0 0 0 0 0 0 0 0 0 0
B: ABS=6e18000 0
  1. I:设备ID

    该参数由结构体input_id来描述:

    image-20230918210100361

  2. N:设备名称

  3. P:系统层次结构中设备的物理路径(physical path to the device in the system hierarchy)

  4. S:位于 sys 文件系统的路径(sysfs path)

  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(设备上的指示灯)
(3) 理解位图

还是以上面的输出为例:

1
2
3
B: EV=b                             //b:1011
B: KEY=1c00 0 0 0 0 0 0 0 0 0 0
B: ABS=6e18000 0 //6e18000 0:0110 1110 0001 1000 0000 0000 0000 0000
  • EV=b:根据其二进制第0,1,3位为1,可知其支持值为0,1,3这三类事件,查询input-event-codes.h可知其支持EV_SYN同步事件、EV_KEY按键事件、`EV_ABS绝对位移事件
  • ABS=6e18000 0:根据下面input_dev结构体,可知每一个数据都是unsigned long为32位。同理得到其二进制表示,查询input-event-codes.h可知该设备具体支持绝对位移类事件中的哪些子事件
(4) 内核如何表示一个输入设备

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

image-20230918211408770

该数据结构和shell输出的设备信息正好对应。

2. 不使用库读取输入设备信息

2.1 获取输入设备基本信息

简单了解一下针对输入设备的使用ioctl()获取设备信息:

2.1.1 获取input_id结构体

1
2
struct input_id id;
err = ioctl(fd, EVIOCGID, &id);

一般ioctl()函数在成功时返回0,失败时返回-1;在个别情况下,返回值也包含有效信息(例如读取到的字节数)。继续向下寻找EVIOCGID的定义:

1
2
3
4
5
6
7
8
9
/* /include/linux/input.h         */
#define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */
/* /include/asm-generic/ioctl.h */
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
  • dir:为_IOC_READ表示APP要读数据,为_IOC_WRITE表示APP要写数据
  • size:表示ioctl能传输数据的最大字节数
  • type、nr 的含义由具体的驱动程序决定

2.1.2 获取evbit数组(支持哪几类事件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int evbit[2];
len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); // sizeof返回的是字节数
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");
}

根据宏定义EV_MAX=0x1f理论上来说,一个int足矣存放下所有事件类型。继续寻找EVIOCGBIT

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

之所以EVIOCGBIT(0, sizeof(evbit))传入参数0,原因在内核驱动代码中,不过做应用只需要按照如下对应关系传入参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
#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 获取输入设备的事件信息

获取输入设备事件信息的四种方式:

  1. 查询:
  2. 休眠-唤醒
  3. POLL/SELECT
  4. 异步通知方式

2.2.1 查询和休眠-唤醒方式

  • 所谓查询就是以非阻塞方式(O_NONBLOCK)打开设备文件,在死循环中使用read函数不停的读取文件内容;如果读取到就返回数据,否则就返回失败
  • 而休眠-唤醒则是以阻塞方式打开设备文件。如果文件中没有数据,则APP就会在内核态休眠,有数据时驱动程序会把 APP 唤醒,read 函数恢复执行并返回数据给 APP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct input_event event;

if (argc == 3 && !strcmp(argv[2], "noblock"))
fd = open(argv[1], O_RDWR | O_NONBLOCK);
else
fd = open(argv[1], O_RDWR);

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);
}

2.2.2 POLL/SELECT方式

poll()和select()类似,用来等待判断所监视的文件是否准备好了文件I/O。

1
2
3
4
5
6
7
8
9
10
#include <poll.h>
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
/* Type used for the number of file descriptors. */
typedef unsigned long int nfds_t;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 当成功时返回一个正数,表示具有非零revents字段的数量(每个事件对应revents中的1bit)。返回0表示超时;出现错误时返回-1,并设置errno

  • poll可以监视的文件事件类型有:

事件类型 说明
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

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct pollfd fds[1];
nfds_t nfds = 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");

2.2.3 异步通知方式

上面三种方式都是同步方式,APP只能等待驱动程序发送数据,这期间不能做其他事情。而异步通知方式就是APP可以做其他事情,驱动有数据之后会给APP发信号,APP收到信息执行相应的信号处理函数,类似于中断

考虑异步通知的实现细节:

  1. 信号的类型是什么?

    include/uapi/asm-generic/signal.h中:

    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
    26
    27
    28
    29
    30
    #define SIGHUP		 1
    #define SIGINT 2
    #define SIGQUIT 3
    #define SIGILL 4
    #define SIGTRAP 5
    #define SIGABRT 6
    #define SIGIOT 6
    #define SIGBUS 7
    #define SIGFPE 8
    #define SIGKILL 9
    #define SIGUSR1 10
    #define SIGSEGV 11
    #define SIGUSR2 12
    #define SIGPIPE 13
    #define SIGALRM 14
    #define SIGTERM 15
    #define SIGSTKFLT 16
    #define SIGCHLD 17
    #define SIGCONT 18
    #define SIGSTOP 19
    #define SIGTSTP 20
    #define SIGTTIN 21
    #define SIGTTOU 22
    #define SIGURG 23
    #define SIGXCPU 24
    #define SIGXFSZ 25
    #define SIGVTALRM 26
    #define SIGPROF 27
    #define SIGWINCH 28
    #define SIGIO 29

    一般IO事件常用信号类型是SIGIO,表示有IO事件需要处理

  2. APP收到信号之后,应该调用什么函数进行处理?

    1
    2
    3
    4
    #include <signal.h>
    typedef void (*sighandler_t)(int);

    sighandler_t signal(int signum, sighandler_t handler);

    使用signal()函数将将信号关联到信号处理程序

  3. APP和驱动程序之间如何建立联系?

    APP中可以打开设备文件;驱动程序需要得知APP的进程号

    1
    2
    3
    4
    #include <unistd.h>
    #include <fcntl.h>

    int fcntl(int fd, int cmd, ... /* arg */ );
    • fcntl()用来执行由cmd参数描述的一种针对文件fd的操作。arg参数是否需要,是什么类型由cmd决定
    • 例如cmd的值为 F_SETOWN (int),表示设置文件fd上的SIGIOSIGURG信号的进程ID/进程组ID为arg的值。最常用的做法是调用进程将自己指定为所有者getpid()
    • 返回值:对于F_SENTOWN成功返回0,错误返回-1
  4. 驱动程序使能异步通知功能

    1
    2
    3
    4
    #define FASYNC  O_ASYNC

    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC); // 设置FASYNC位,使能异步通知功能
    • F_GETFL (void):将文件访问模式和文件状态标志作为返回值进行返回,arg参数为void
    • F_SETFL (int):将文件状态标志设置为arg参数的值。忽略访问模式位O_RDONLY, O_WRONLY, O_RDWR和文件创建标志位O_CREAT,O_EXCL,O_NOCTTY,O_TRUNC,该命令仅能修改O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, O_NONBLOCK

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

/* 注册信号处理函数 */
signal(SIGIO, my_sig_handler);
/* 打开驱动程序 */
fd = open(argv[1], O_RDWR | O_NONBLOCK);
/* 把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);
}