23、TSP用户空间接口:input设备节点、ioctl命令、事件读取
好,我们继续往下走。上一章我们把驱动层的上报流程理清楚了,数据从硬件寄存器一路跑到内核的 input 子系统。那问题来了——用户空间的应用程序,到底怎么拿到这些触摸数据?
说白了,就是三个东西:设备节点、ioctl 命令、事件读取。这三样东西,是用户空间和内核空间之间的桥梁。我当年刚接触 Android 触摸时,就是被这三个概念绕晕过,后来自己动手写了个测试程序,才彻底搞明白。
核心逻辑一句话:应用层通过 open() 打开 /dev/input/eventX 节点,用 ioctl 获取设备信息,再用 read() 循环读取 input_event 结构体,解析出触摸坐标、压力、手势等数据。
23.1 input 设备节点长什么样?
Android 系统里,每个输入设备都会在 /dev/input/ 目录下生成一个节点。触摸屏通常是 event0 或 event1,具体是哪个,得看注册顺序。
你可以用 getevent 命令查看:
# adb shell getevent -i
add device 1: /dev/input/event0
name: "ft5x06_ts"
location: "input0"
handler: "event0"
identifier: input/0
我个人习惯,拿到一台新设备,第一件事就是跑 getevent -i,看看触摸屏的节点是哪个。名字里一般带 "ts" 或者 "touch" 字样,很好认。
节点本身是一个字符设备,主设备号 13,次设备号从 64 开始(对应 event0)。你想想看,系统里可能有多个输入设备——按键、触摸屏、鼠标——每个都有自己的 event 节点,互不干扰。
23.2 ioctl 命令:跟驱动“聊两句”
打开节点之后,你不能上来就读数据。你得先问问驱动:你是谁?你能上报什么事件?分辨率是多少?
这些信息,通过 ioctl 来获取。常用的 ioctl 命令有这几个:
| 命令 | 作用 | 返回结构体 |
|---|---|---|
| EVIOCGNAME(len) | 获取设备名称 | char[] |
| EVIOCGBIT(ev, len) | 获取支持的事件类型/按键码 | unsigned long[] |
| EVIOCGABS(abs) | 获取绝对轴信息(X/Y/压力范围) | struct input_absinfo |
| EVIOCGRAB | 独占设备(调试时慎用) | int |
举个例子,获取触摸屏的 X 轴分辨率:
#include <linux/input.h>
int fd = open("/dev/input/event0", O_RDWR);
struct input_absinfo abs_x;
ioctl(fd, EVIOCGABS(ABS_MT_POSITION_X), &abs_x);
printf("X min=%d, max=%d, res=%d\n",
abs_x.minimum, abs_x.maximum, abs_x.resolution);
避坑指南:我曾经在调试一款触控 IC 时,发现上报的 X 坐标总是偏大。查了半天,原来是驱动里把 ABS_MT_POSITION_X 的 maximum 设错了。用 EVIOCGABS 读出来一看,最大值比实际屏幕宽度多了 200。嗯,这种低级错误,ioctl 一读就现原形。
23.3 事件读取:核心中的核心
拿到设备信息之后,就该干活了——读取触摸事件。内核用 struct input_event 这个结构体来传递数据:
struct input_event {
struct timeval time; // 时间戳,精确到微秒
__u16 type; // 事件类型:EV_ABS, EV_KEY, EV_SYN
__u16 code; // 事件码:ABS_MT_POSITION_X, BTN_TOUCH 等
__s32 value; // 事件值:坐标值、按键状态
};
读取方式很简单——用 read() 循环读:
struct input_event ev;
while (1) {
int ret = read(fd, &ev, sizeof(ev));
if (ret != sizeof(ev)) continue;
switch (ev.type) {
case EV_ABS:
if (ev.code == ABS_MT_POSITION_X)
printf("X = %d\n", ev.value);
else if (ev.code == ABS_MT_POSITION_Y)
printf("Y = %d\n", ev.value);
else if (ev.code == ABS_MT_PRESSURE)
printf("Pressure = %d\n", ev.value);
break;
case EV_KEY:
if (ev.code == BTN_TOUCH)
printf("Touch %s\n", ev.value ? "DOWN" : "UP");
break;
case EV_SYN:
// 同步事件,表示一次触摸数据包结束
printf("--- SYN ---\n");
break;
}
}
这里有个关键点:一次触摸动作会触发多个事件。比如手指按下,驱动会依次上报:
- BTN_TOUCH = 1(按下)
- ABS_MT_POSITION_X = 300
- ABS_MT_POSITION_Y = 500
- ABS_MT_PRESSURE = 80
- SYN_REPORT(同步信号,告诉应用层:这一包数据齐了)
应用层收到 SYN_REPORT 之后,才应该去处理这组坐标。否则你读到一半就去处理,拿到的数据是不完整的。
注意:read() 是阻塞的。如果没有事件,线程会挂起。这在 Android 的 InputReader 里是通过 epoll 来管理的,不会让主线程卡死。你自己写测试程序时,记得开个独立线程去读。
23.4 知识体系总览
下面这张图,把用户空间和内核空间的关系画清楚了。我建议你多看几遍,把每个箭头对应的函数调用记下来。
23.5 实战经验:别踩这些坑
代码看起来简单,但实际项目中坑不少。我挑几个常见的说说:
- 节点权限问题:Android 里 /dev/input/eventX 默认权限是 660,属主是 system。普通 app 如果没有权限,open() 会返回 -1。解决方案是用
InputManager的 API,或者让服务进程提权。 - 多指触摸:每个手指会独立上报 ABS_MT_* 事件,但共用同一个 event 节点。你需要通过
ABS_MT_SLOT来区分是哪个手指。我早期写测试程序时没处理 slot,结果两个手指的数据混在一起,坐标完全乱套。 - 事件丢失:如果 read() 不够快,内核缓冲区满了会丢事件。内核默认缓冲区大小是 64 个 input_event,大约 1KB 左右。高帧率触摸屏(比如 240Hz)上报很快,应用层处理不过来就会丢包。解决办法是用 epoll 边缘触发,或者增大缓冲区(通过 ioctl 设置)。
一个小技巧:调试时可以用 getevent -lt 实时看事件流,加上 -l 参数会把 type/code 翻译成可读的字符串。比如看到 EV_ABS ABS_MT_POSITION_X 00000320,你就知道 X 坐标是 0x320(800 像素)。
好了,这一章的内容就这些。用户空间的接口其实不复杂,核心就是 open → ioctl → read 这个三板斧。你只要把 input_event 结构体吃透,再处理好同步事件,写一个触摸测试程序就是分分钟的事。