1. 输入系统框架及调试
1.1 框架概述

APP 发起读操作,若无数据则休眠;
用户操作设备,硬件上产生中断;
输入系统驱动层对应的驱动程序处理中断:
读取到数据,转换为标准的输入事件,向核心层汇报。 所谓输入事件就是一个
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这类库来间接访问设备节点。这些库简化了对数据的处理。
1.2 编写APP需要掌握的知识
1.2.1 APP可以得到什么数据

timeval结构体:tv_sec:表示自1970年1月1日00:00:00(即UNIX纪元起点)以来的秒数tv_usec:表示微秒数,即秒后面的零头
input_event结构体:type:表示触发了哪类事件;比如EV_KEY表示按键类,EV_REL表示相对位移(例如鼠标),EV_ABS表示 绝对位置(比如触摸屏)
code:表示该类事件下的哪一个事件;例如EV_KEY表示键盘,键盘上有很多按键,每个分别对应不同事件:
对于
EV_ABS事件,触摸屏提供绝对位置信息,包括X,Y还有压力方向的值:
value:表示事件的值;例如按键的值可以是0(按下)或者1(松开)
(1) 使用命令读取设备上报数据
调试输入系统时,直接执行类似下面的命令,然后操作对应的输入设备即可读出数据:
1 | hexdump /dev/input/event0 |
部分输出如下:

根据输出信息,结合input-event-codes.h,即可知道操作触发了那类事件中的那个事件,触发的值是什么
(2) 事件的结尾标识
APP读取输入事件时,不同设备可能会上报不同数量的事件,但是所有设备的驱动程序都会在上报所有的数据之后,最后上报一个“同步事件”,表示数据上报完成。APP读到该事件时知道已经读完当前数据。“同步事件”也是一个input_event结构体,他的type、code、value属性都为0,如上图所示。
1.2.2 查询开发板上有哪些设备节点
(1) 使用命令查询设备节点:
1 | ls /dev/input/* -l |
在开发板上输出:
1 | crw-rw---- 1 root input 13, 64 Jan 1 00:00 /dev/input/event0 |
(2) 使用命令查询设备信息及其对应的设备节点
1 | cat /proc/bus/input/devices |
部分输出:
1 | I: Bus=0018 Vendor=dead Product=beef Version=28bb |
I:设备ID
该参数由结构体
input_id来描述:
N:设备名称
P:系统层次结构中设备的物理路径(physical path to the device in the system hierarchy)
S:位于 sys 文件系统的路径(sysfs path)
U:设备的唯一标识码(unique identification code for the device(if device has it))
H:与设备关联的输入句柄列表(list of input handles associated with the device)
B:位图(bitmaps)
1
2
3
4
5PROP: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 | B: EV=b //b:1011 |
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 结构体来表示输入设备:

该数据结构和shell输出的设备信息正好对应。
2. 不使用库读取输入设备信息
2.1 获取输入设备基本信息
简单了解一下针对输入设备的使用ioctl()获取设备信息:
2.1.1 获取input_id结构体
1 | struct input_id id; |
一般ioctl()函数在成功时返回0,失败时返回-1;在个别情况下,返回值也包含有效信息(例如读取到的字节数)。继续向下寻找EVIOCGID的定义:
1 | /* /include/linux/input.h */ |
dir:为_IOC_READ表示APP要读数据,为_IOC_WRITE表示APP要写数据size:表示ioctl能传输数据的最大字节数type、nr的含义由具体的驱动程序决定
2.1.2 获取evbit数组(支持哪几类事件)
1 | unsigned int evbit[2]; |
根据宏定义EV_MAX=0x1f理论上来说,一个int足矣存放下所有事件类型。继续寻找EVIOCGBIT:
1 |
之所以EVIOCGBIT(0, sizeof(evbit))传入参数0,原因在内核驱动代码中,不过做应用只需要按照如下对应关系传入参数即可:
1 |
2.2 获取输入设备的事件信息
获取输入设备事件信息的四种方式:
- 查询:
- 休眠-唤醒
- POLL/SELECT
- 异步通知方式
2.2.1 查询和休眠-唤醒方式
- 所谓查询就是以非阻塞方式(O_NONBLOCK)打开设备文件,在死循环中使用
read函数不停的读取文件内容;如果读取到就返回数据,否则就返回失败 - 而休眠-唤醒则是以阻塞方式打开设备文件。如果文件中没有数据,则APP就会在内核态休眠,有数据时驱动程序会把 APP 唤醒,read 函数恢复执行并返回数据给 APP
1 | struct input_event event; |
2.2.2 POLL/SELECT方式
poll()和select()类似,用来等待判断所监视的文件是否准备好了文件I/O。
1 |
|
当成功时返回一个正数,表示具有非零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 | struct pollfd fds[1]; |
2.2.3 异步通知方式
上面三种方式都是同步方式,APP只能等待驱动程序发送数据,这期间不能做其他事情。而异步通知方式就是APP可以做其他事情,驱动有数据之后会给APP发信号,APP收到信息执行相应的信号处理函数,类似于中断
考虑异步通知的实现细节:
信号的类型是什么?
在
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一般IO事件常用信号类型是
SIGIO,表示有IO事件需要处理APP收到信号之后,应该调用什么函数进行处理?
1
2
3
4
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);使用signal()函数将将信号关联到信号处理程序
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上的SIGIO和SIGURG信号的进程ID/进程组ID为arg的值。最常用的做法是调用进程将自己指定为所有者getpid() - 返回值:对于
F_SENTOWN成功返回0,错误返回-1
- fcntl()用来执行由
驱动程序使能异步通知功能
1
2
3
4
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); // 设置FASYNC位,使能异步通知功能F_GETFL (void):将文件访问模式和文件状态标志作为返回值进行返回,arg参数为voidF_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 | void my_sig_handler(int sig) |