目录
1.输入系统
1.1 什么是输入系统
1.2 编写 APP 需要掌握的知识
1.内核使用 input_dev 结构体来表示输入设备(如图):
2.APP 可以得到一系列的输入事件,就是一个一个“struct input_event” (如图):
1.3 调试技巧
1.确定设备信息
2.使用命令读取数据
2.不使用库的应用程序
2.1 APP 访问硬件的 4 种方式:妈妈怎么知道孩子醒了
2.2 获取设备信息
2.4 查询方式
2.4 休眠-唤醒方式
3.5 POLL/SELECT 方式
3.7 异步通知方式
1.输入系统
1.1 什么是输入系统
因为输入设备种类繁多,Linux 系统为了统一管理这些输入设备,实现了一套能兼容所有输入设备的框架:输入系统。驱动开发人员基于这套框架开发出程序,应用开发人员就可以 使用统一的 API 去使用设备。
输入系统框架:
假设用户程序直接访问 /dev/input/event0 设备节点,或者使用 tslib 访问设备节点,数据的流程如下:
- APP 发起读操作,若无数据则休眠;
- 用户操作设备,硬件上产生中断;
- 输入系统驱动层对应的驱动程序处理中断:
读取到数据,转换为标准的输入事件,向核心层汇报。 所谓输入事件就是一个“struct input_event”结构体。 - 核心层可以决定把输入事件转发给上面哪个 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.内核使用 input_dev 结构体来表示输入设备(如图):
2.APP 可以得到一系列的输入事件,就是一个一个“struct input_event” (如图):
每个输入事件 input_event 中都含有发生时间:
timeval 表示的是“自系统启动以来过了多少时间”,它是一个结构体,含有 “tv_sec、tv_usec”两项 (即秒、微秒)。
input_event 中更重要的是:type(哪类事件)、code(哪个事件)、 value(事件值)
(1) type:表示哪类事件(对应evbit)
如EV_KEY 表示按键类、EV_REL 表示相对位移(如鼠标),EV_ABS 表示 绝对位置(如触摸屏):
(2)code:表示该类事件下的哪一个事件
比如对于 EV_KEY(按键)类事件,键盘上有很多按键,所以可以有如图这些事件:
(3)value:表示事件值
value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按)
对于触摸屏,value 就是坐标值、压力值
(4)事件间的界限
APP 读取数据时,可以得到一个或多个数据,如一个触摸屏的一个触点会 上报 X、Y 位置信息,也可能会上报压力值。
◼ APP 怎么知道它已经读到了完整的数据?
驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完 毕。APP 读到“同步事件”时,就知道已经读完了当前的数据。
同步事件也是一个 input_event 结构体,它的 type、code、value 三项都 是 0。
1.3 调试技巧
1.确定设备信息
输入设备的设备节点名为/dev/input/eventX(也可能是/dev/eventX,X 表示 0、1、2 等数字)
查看设备节点,可以在板子上执行以下命令:
ls /dev/input/* -l
得到如下图信息:
要知道这些设备节点对应什么硬件,在板子上执行以下命令,获取与 event 对应的相关设备信息:
cat /proc/bus/input/devices
得到下图信息:
(1)I:设备 ID
由结构体 struct input_id 描述:
(2)N:设备名称
(3)P:系统层次结构中设备的物理路径
(4)S:位于 sys 文件系统的路径
(5)U:设备的唯一标识码
(6)H:与设备关联的输入句柄列表
(7)B:bitmaps(位图)
PROP:设备属性
EV:设备支持的事件类型
KEY:此设备具有的键/按钮
MSC:设备支持的其他事件
LED:设备上的指示灯
举一个例子,“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,对应以下这些宏:
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 次完整的数据。
2.不使用库的应用程序
2.1 APP 访问硬件的 4 种方式:妈妈怎么知道孩子醒了
妈妈怎么知道卧室里小孩醒了?
时不时进房间看一下:查询方式
简单,但是累
进去房间陪小孩一起睡觉,小孩醒了会吵醒她:休眠-唤醒
不累,但是妈妈干不了活了
妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟:poll 方式
要浪费点时间,但是可以继续干活。
妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:异步通知
妈妈、小孩互不耽误。
这 4 种方法没有优劣之分,在不同的场合使用不同的方法。
2.2 获取设备信息
通过 ioctl 获取设备信息(之前说过,这个函数功能很强大)
int ioctl(int fd, unsigned long request, ...);
request参数可以理解为你想要 ioctl 做什么
有些驱动程序对 request 的格式有要求,它的格式如下:
dir:为_IOC_READ(即 2)时,表示 APP 要读数据;为_IOC_WRITE(即 4)时,表示 APP 要写数据
size:表示这个 ioctl 能传输数据的最大字节数
type、nr 的含义由具体的驱动程序决定。
一般都是已经有定义好的宏直接使用:
2.4 查询方式
APP 调用 open 函数时,传入“O_NONBLOCK”表示“非阻塞”
APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read 函数会返回数据,否则也会立刻返回错误。
2.4 休眠-唤醒方式
APP 调用 open 函数时,不要传入“O_NONBLOCK”。
APP 调用 read 函数读取数据时,如果驱动程序中有数据,那么 APP 的 read 函数会返回数据;否则 APP 就会在内核态休眠,有数据时才会唤醒。
查询+休眠唤醒方式源码(注释版):
/* 程序功能:读取输入设备信息及事件* 用法:./01_get_input_info /dev/input/event0 [noblock]* noblock: 非阻塞模式打开设备*/
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]; // 存储设备支持的事件类型位图(64位)struct input_event event; // 输入事件结构体// 输入事件类型名称对照表(来自linux/input.h)char *ev_names[] = {"EV_SYN ", // 0x00 同步事件"EV_KEY ", // 0x01 按键事件"EV_REL ", // 0x02 相对坐标事件(如鼠标)"EV_ABS ", // 0x03 绝对坐标事件(如触摸屏)"EV_MSC ", // 0x04 其他杂项事件"EV_SW ", // 0x05 开关事件"NULL ", // 0x06 未使用/* ... 省略部分NULL ... */"EV_LED ", // 0x11 LED控制事件"EV_SND ", // 0x12 声音控制事件"NULL ", // 0x13"EV_REP ", // 0x14 重复事件(如按键重复)"EV_FF ", // 0x15 力反馈事件"EV_PWR ", // 0x16 电源管理事件};// 参数检查:至少需要设备路径参数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信息(通过ioctl)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++) {// 如果某位被置1,表示支持对应事件类型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)) { // 成功读取事件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; // 实际上由于无限循环,不会执行到这里
}
3.5 POLL/SELECT 方式
POLL 机制、SELECT 机制是完全一样的,只是 APP 接口函数不一样。
简单地说,它们就是“定个闹钟”:在调用 poll、select 函数时可以传入 “超时时间”。在这段时间内,条件合适时(比如有数据可读、有空间可写)就会 立刻返回,否则等到“超时时间”结束时返回错误。
⚫ APP 先调用 open 函数时。
⚫ APP 不是直接调用 read 函数,而是先调用 poll 或 select 函数,这 2 个函 数中可以传入“超时时间”。
它们的作用是:如果驱动程序中有数据,则立刻返回; 否则就休眠。
在休眠期间,如果有人操作了硬件,驱动程序获得数据后就会把 APP 唤醒,导致 poll 或 select 立刻返回;如果在“超时时间”内无人操作硬件,则时间到后 poll 或 select 函数也会返回。
⚫ APP 根据 poll 或 select 的返回值判断有数据之后,就调用 read 函数读取 数据时,这时就会立刻获得数据。
⚫ poll/select 函数可以监测多个文件,可以监测多种事件:
在调用 poll 函数时,要指明:
你要监测的文件:哪一个 fd
你想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT
/* 改进版输入设备事件监听程序(使用poll机制)* 用法:./program /dev/input/eventX* 特点:5秒超时检测,避免纯阻塞等待*/
int main(int argc, char **argv)
{int fd; // 设备文件描述符int err; // 错误码int len; // IO操作返回值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]; // poll结构体数组(关键改进)nfds_t nfds = 1; // 监控的文件描述符数量...中间省略和上面一样的部分...// 主事件循环(使用poll改进)while (1) {// 初始化poll结构体(关键改进)fds[0].fd = fd; // 监控的设备fdfds[0].events = POLLIN; // 监控可读事件fds[0].revents = 0; // 实际发生的事件// 调用poll(5秒超时)、nfds=1:监控的文件描述符数量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; // 实际不会执行到此处
}
3.7 异步通知方式
所谓同步,就是“你慢我等你”。
那么异步就是:你慢那你就自己玩,我做自己的事去了,有情况再通知我。
异步通知,就是 APP 可以忙自己的事,当驱动程序用数据时它会主动给 APP 发信号,这会导致 APP 执行信号处理函数。
实际就是:驱动程序通知 APP 时,它会发出 “SIGIO” 信号,表示有“IO 事件”要 处理。
就 APP 而言,你想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟 SIGIO 挂钩。这可以通过一个 signal 函数来“给某个信号注册处理函数”(跟中断回调函数有点像),用法如图:
流程如下:
(1)编写信号处理函数:
static void sig_func(int sig)
{ int val; read(fd, &val, 4); printf("get button : 0x%x\n", val);
}
(2)注册信号处理函数:
signal(SIGIO, sig_func);
(3)打开驱动
fd = open(argv[1], O_RDWR);
(4)把进程 ID 告诉驱动:
fcntl(fd, F_SETOWN, getpid());
fd
:要操作的文件描述符(如设备文件/dev/input/eventX
)。
cmd
:控制命令,F_SETOWN
表示设置文件描述符的所有者。
arg
:所有者进程 ID(getpid()
获取当前进程 PID)
(5)使能驱动的 FASYNC 功能:
//获取文件描述符当前的状态标志
flags = fcntl(fd, F_GETFL); //修改文件描述符的状态标志,添加异步通知功能
fcntl(fd, F_SETFL, flags | FASYNC);
源代码:
int fd; // 全局文件描述符,用于信号处理函数访问/* SIGIO信号处理函数 - 当输入设备有数据时被调用 */
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); }
}/* 主函数:输入设备异步读取示例 */
int main(int argc, char **argv)
{int err;int len;int i;unsigned char byte;int bit;struct input_id id;unsigned int evbit[2]; // 存储设备支持的事件类型位图unsigned int flags;int count = 0;// 输入事件类型名称映射表(对应linux/input.h中的定义)char *ev_names[] = {"EV_SYN ", // 0x00 同步事件"EV_KEY ", // 0x01 按键事件"EV_REL ", // 0x02 相对坐标事件(如鼠标)"EV_ABS ", // 0x03 绝对坐标事件(如触摸屏)/* ... 其他事件类型 ... */"EV_PWR ", // 0x16 电源事件};// 参数检查:必须指定输入设备路径if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 如 ./05_input_read_fasync /dev/input/event0return -1;}/* 注册SIGIO信号处理函数 */signal(SIGIO, my_sig_handler);/* 以非阻塞方式打开输入设备 */fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd < 0){printf("open %s err\n", argv[1]);return -1;}...省略打印设备信息部分.../* 设置异步I/O的关键两步 */// 1. 将当前进程设置为文件描述符的所有者(接收SIGIO信号)fcntl(fd, F_SETOWN, getpid());// 2. 启用文件描述符的异步通知功能flags = fcntl(fd, F_GETFL); // 获取当前文件状态标志fcntl(fd, F_SETFL, flags | FASYNC); // 添加FASYNC标志/* 主循环(演示异步通知机制) *///有消息来就会触发信号函数处理,不影响主进程(像中断回调函数一样)while (1){printf("main loop count = %d\n", count++); // 主程序可执行其他任务sleep(2); // 模拟其他工作}return 0;
}