在上一篇文章中,我们理解了输入子系统的 三层架构 的宏观设计,今天,我们会深入内核源码,并分析现代设备是如何处理 多点触控 的。
1. Input 驱动开发流程
编写一个标准的 Input 驱动,通常要遵循下面的流程:
-
分配与初始化:
- 现代内核强烈建议使用
devm接口,即devm_input_allocate_device(&pdev->dev)。 - 当驱动卸载或初始化失败时,内核会自动释放内存,避免了以往
input_allocate_device常见的内存泄漏隐患。
- 现代内核强烈建议使用
-
配置位图: 这是最关键的一步,这一步我们必须明确告诉内核,我们的设备能产生哪些事件。
- 设置事件大类:
EV_KEY代表按键,EV_ABS代表绝对坐标(触摸屏,摇杆),EV_REL代表相对坐标(鼠标)。 - 具体设置使用
input_set_capability(dev, EV_KEY, KEY_POWER)。 - 对于触摸屏,你必须使用
input_set_abs_params设定 X 和 Y 轴的最大值、最小值、分辨率和平滑度。
- 设置事件大类:
-
注册设备:
- 调用
input_register_device(dev)进行注册设备。 - 一旦调用此函数,设备就正式暴露给用户空间,因此,必须在注册前完成所有能力位图的配置。
- 调用
-
上报事件与同步:
- 在中断处理函数中,将数据打包上报。
- 常用 API :
input_report_key,input_report_abs,input_sync
从上面可以看出,普通字符设备需要自己实现 file_operations,而 Input 驱动只需实现硬件部分和事件上报,Input 子系统自动提供统一接口,大大简化开发。
2. 事件同步机制
初学 Input 子系统的时候经常会遇到 驱动层上报了数据,而用户空间却没有收到 的情况。这通常是因为漏掉了 input_sync。
为什么需要 input_sync 呢?举个例子:
假设你的设备是一个两轴摇杆,硬件上报数据是分先后顺序的:
- 第一步,上报 X=100;
- 第二步,上报 Y=200;
如果不使用 input_sync,用户空间可能会在上报 X=100 之后读到数据,而使用的依然是旧的 Y 值,这就导致在用户空间看到的 Y 值并没有更新。
这样来看就很明确了,input_sync 的本质其实就是发送一个特殊的事件:EV_SYN / SYN_REPORT。它告诉 Handler 层 现在缓冲区里这一堆数据是一个完整的物理动作,从而把这个整体打包发给用户。
3. Multi-touch 协议
处理 多点触控(Multi-touch)曾是 Linux 的难题,直到 MT 协议的引入。目前所有现代触摸屏驱动都必须遵循 Type B (Slotted) 协议。
3.1 核心概念:Slot
在多点触摸设备中,一次触摸帧可能同时存在多个手指。如果像 Type A 协议那样把所有手指的所有数据 顺序上报,用户空间(evdev)很难区分 哪个数据属于哪根手指,尤其当手指数量变化、交叉移动时,更容易出现混乱。
而 Slot 的设计思路是:
- 把触摸屏抽象成一个 固定数量的插槽数组。
- 每个 Slot 代表一个可能的触摸点位置。
- 驱动通过切换 Slot 来告诉内核和用户空间,接下来要更新的是第几个插槽的信息。
- 同一个 Slot 在 不同帧 中可以 持续代表同一根手指,从而实现手指的 持久跟踪。
这样做有几个关键的优点:
- 由于只上报了变化的 Slot ,因此减少了数据的传输。
- 用户空间可以清晰的知道每个 Slot 的状态。
- 支持手指的创建、移动、抬起、交叉等复杂动作。
Slot 数量通常由硬件支持的最大触摸点数决定,例如 10 点触摸屏就初始化 10 个 Slot。
3.2 核心概念:Tracking ID
每一个新的触点被创建时,驱动都会分配一个唯一的正整数 ID,表示这个 Slot 当前有一个有效的触摸点,该值是这个手指的唯一标识。
-1 表示这个 Slot 当前没有手指。
出现一个之前没见过的 TRACKING_ID ,用户空间会认为是一个新的触摸开始了。
把 TRACKING_ID 改成 -1 ,即表示该 Slot 的手指已经抬起。
同一个手指按住不松开持续移动,代表同一个 Slot 保持同一个 TRACKING_ID,只更新位置、压力等信息。
同一个 Slot 在不同时刻可以对应不同的 Tracking ID,旧的手指离开,新的手指可能重新占用这个 Slot。
用户空间主要依靠 Tracking ID 来实现手指跟踪,而 Slot 只是上报时的通道切换机制。
3.3 经典案例分析
在理解了 Type B 协议的原理后,我们来看一下工业级驱动 goodix.c 是如何实现这一逻辑的,下面这段代码展示了从 读取硬件原始报文 到 上报规范化事件 的全过程。
第一步:
touch_num = goodix_ts_read_input_report(ts, point_data);
驱动通过 I2C 协议从触摸屏芯片的寄存器中,一次性读取当前所有触点的数据,point_data 数组里存储的就是硬件生成的原始字节流。
第二步:
这里有一个 for 循环,循环次数就是检测到的手指数量 touch_num。
代码中通过 ts->contact_size 判断是 8 字节还是 9 字节格式,实现了不同硬件版本的兼容性,这体现了驱动程序的健壮性。
如下图,在进行 if 判断之后调用的函数中,确实调用 input_mt_slot 切换插槽,并调用 input_report_abs 上报 X / Y 坐标。
第三步:
最关键的一点,代码连续调用了两个同步函数:
input_mt_sync_frame(ts->input_dev):这是 Type B 协议特有的,它告诉内核 MT 框架,这一帧里没被提到的 Slot,接着内核会自动将其设为不活跃,也就是手指已抬起,它负责管理触点的生命周期。input_sync(ts->input_dev):这个我们上面已经讲过了,是 Input 核心层 的通用同步。它告诉系统,这些数据(包含所有 Slot)已经处理完了,可以发送给用户空间了。
代码中的细节分析:
- 在 Type B 协议中,如果上一帧有 3 个手指,这一帧只有 2 个,驱动程序中不需要手动去停掉那个松开的手指。只要调用了
input_mt_sync_frame,内核会自动发现:这次循环里没有某一个 Slot,那它一定是被抬起来了。然后自动发送一个TRACKING_ID = -1的事件。 这就是现代驱动程序简洁高效的原因。 - 代码中有一行
u8 point_data[2 + GOODIX_MAX_CONTACT_SIZE * GOODIX_MAX_CONTACTS]。这里可以看到驱动开发者如何通过宏定义来预分配内存,2 字节通常是状态码,后面跟着 每个触点的固定大小。这种 静态分配 避免了在中断上下文(即中断处理函数中)进行昂贵的内存分配。
4. 总结
至此,我们已经完成了从 零分配设备 到 上报复杂多点触控数据 的全过程。在内核里,数据已经成功转换成了标准的 input_event 结构,并存放在内核的环形缓冲区中。
但是,这些数据如何变成你手机屏幕上的滑屏动作?如何变成游戏里的技能释放?
在下一篇文章中,我们将跨越内核与用户空间的边界,去分析 /dev/input/eventX 背后隐藏的秘密,以及现代 Linux 桌面是如何通过 libinput 处理这些海量数据的。