23、TSP用户空间接口:input设备节点、ioctl命令、事件读取

好,我们继续往下走。上一章我们把驱动层的上报流程理清楚了,数据从硬件寄存器一路跑到内核的 input 子系统。那问题来了——用户空间的应用程序,到底怎么拿到这些触摸数据?

说白了,就是三个东西:设备节点ioctl 命令事件读取。这三样东西,是用户空间和内核空间之间的桥梁。我当年刚接触 Android 触摸时,就是被这三个概念绕晕过,后来自己动手写了个测试程序,才彻底搞明白。

核心逻辑一句话:应用层通过 open() 打开 /dev/input/eventX 节点,用 ioctl 获取设备信息,再用 read() 循环读取 input_event 结构体,解析出触摸坐标、压力、手势等数据。

23.1 input 设备节点长什么样?

Android 系统里,每个输入设备都会在 /dev/input/ 目录下生成一个节点。触摸屏通常是 event0event1,具体是哪个,得看注册顺序。

你可以用 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;
    }
}

这里有个关键点:一次触摸动作会触发多个事件。比如手指按下,驱动会依次上报:

  1. BTN_TOUCH = 1(按下)
  2. ABS_MT_POSITION_X = 300
  3. ABS_MT_POSITION_Y = 500
  4. ABS_MT_PRESSURE = 80
  5. SYN_REPORT(同步信号,告诉应用层:这一包数据齐了)

应用层收到 SYN_REPORT 之后,才应该去处理这组坐标。否则你读到一半就去处理,拿到的数据是不完整的。

注意:read() 是阻塞的。如果没有事件,线程会挂起。这在 Android 的 InputReader 里是通过 epoll 来管理的,不会让主线程卡死。你自己写测试程序时,记得开个独立线程去读。

23.4 知识体系总览

下面这张图,把用户空间和内核空间的关系画清楚了。我建议你多看几遍,把每个箭头对应的函数调用记下来。

用户空间 (Userspace) App / Framework getevent open / read / ioctl 内核空间 (Kernel) input subsystem TSP driver input_event / input_absinfo 系统调用 事件数据 数据流与关键接口 ① 应用层 open("/dev/input/event0") → 获取文件描述符 ② ioctl(EVIOCGABS) → 获取触摸屏分辨率/压力范围 ③ read() → 阻塞等待 input_event 数据 ④ 解析 type/code/value → 得到触摸坐标和手势 ⑤ 收到 EV_SYN → 表示一包数据完整,可以处理

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 结构体吃透,再处理好同步事件,写一个触摸测试程序就是分分钟的事。


公众号:蓝海资料掘金营,微信deep3321