5、触摸数据上报:坐标计算、多点触控协议、手势识别基础
好,我们继续往下走。上一章我们把驱动加载、中断处理、原始数据读取这些底层流程捋了一遍。现在数据已经拿到手了,但还只是「原始值」——说白了就是一堆电压或者电容变化量。怎么把它变成屏幕上那个精准的触摸点?这就是本章要聊的核心。
我个人习惯把触摸数据上报分成三个层次:坐标计算(把原始值变成坐标)、多点触控协议(把坐标按标准格式发给上层)、手势识别基础(在驱动层做简单的轨迹判断)。咱们一层一层剥开看。
5.1 坐标计算:从原始值到屏幕坐标
触摸屏的原始数据,通常是某个通道上的电容变化量。比如一个互容屏,横轴有X通道,纵轴有Y通道。手指按下去,附近几个通道的电容值都会变化。驱动要做的事,就是从这些变化值里算出精确的坐标。
最常见的算法是重心法,也叫质心法。公式很简单:
X = Σ( Xi * Ci ) / Σ( Ci )
Y = Σ( Yi * Ci ) / Σ( Ci )
其中Xi、Yi是通道的物理位置,Ci是电容变化量。说白了就是加权平均——变化量大的通道,对最终坐标的贡献就大。
核心要点:重心法能实现亚像素级精度。比如两个通道间距是1mm,通过加权平均,理论上可以算出0.1mm甚至更细的坐标。这就是为什么触摸屏的坐标分辨率可以比物理通道数高很多。
我在项目中遇到过一个问题:手指快速滑动时,坐标会跳变。后来发现是原始数据噪声太大,直接算重心会飘。解决办法是加一个低通滤波,比如一阶IIR:
filtered_value = alpha * raw_value + (1 - alpha) * last_filtered_value
alpha取0.3~0.5效果比较好。嗯,这里要注意:alpha太小会感觉「拖影」,太大又滤不掉噪声。我一般先在PC上录一段原始数据,调好参数再固化到驱动里。
5.2 多点触控协议:Linux内核的标准姿势
坐标算出来了,怎么上报给上层?Linux内核有一套标准协议——多点触控协议,分两种:Type A和Type B。现在主流是Type B,咱们重点讲它。
Type B协议的核心思想是:跟踪ID。每个触摸点分配一个唯一的ID,驱动上报时带上这个ID,上层就知道哪个点是新来的、哪个是移动的、哪个是离开的。
上报流程大概是这样的:
// 一个触摸点的完整上报序列
input_mt_slot(input_dev, slot_id); // 选择槽位
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true); // 报告手指按下
input_report_abs(input_dev, ABS_MT_POSITION_X, x);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y);
// 可选:压力、尺寸、触摸面积等
input_report_abs(input_dev, ABS_MT_PRESSURE, pressure);
input_mt_sync(input_dev); // 同步该点
// 所有点上报完后,发送 SYN_REPORT
input_sync(input_dev);
曾经踩过的坑:有一次我忘了在每帧结束时调用input_sync(),结果上层收不到任何触摸事件。调试了一整天,最后发现是同步事件没发。记住:input_sync()是「开枪」的信号,没有它,前面报的数据都白搭。
Type B协议里有个关键概念——槽位(Slot)。每个槽位对应一个触摸点,最多支持几个点就看硬件能力了。驱动里通常这样初始化:
input_set_abs_params(input_dev, ABS_MT_SLOT, 0, MAX_TOUCH_POINTS - 1, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, screen_width, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, screen_height, 0, 0);
你想想看,如果硬件支持10点触控,但驱动只配了5个槽位,那第6个手指按下去就没反应了。所以槽位数一定要和硬件能力匹配。
5.3 手势识别基础:驱动层的轻量级判断
手势识别通常放在应用层做,比如Android的GestureDetector。但有些简单手势,在驱动层做更高效——比如单指滑动、双击唤醒、长按防抖。
我做过一个项目,客户要求在息屏状态下双击屏幕唤醒手机。这个手势如果在应用层做,CPU得一直跑着,功耗扛不住。最后我们在驱动层实现了一个轻量级的双击检测:
// 伪代码:驱动层双击检测
if (touch_event == DOWN) {
record_timestamp();
if (time_since_last_down < DOUBLE_TAP_TIMEOUT) {
if (distance_between_taps < DOUBLE_TAP_DISTANCE) {
report_double_tap_event();
}
}
}
说白了就是记录两次按下事件的时间差和距离差。时间差通常在300ms以内,距离差在20像素以内,就算双击。这个逻辑放在驱动里,CPU不用唤醒,功耗能降一个数量级。
个人经验:驱动层手势识别要「克制」。只做那些对实时性要求极高、或者需要低功耗的场景。复杂手势(比如画圈、多指旋转)还是交给上层做,驱动层做容易把代码搞成一团乱麻。
还有一个常见的驱动层处理——边缘抑制。手机屏幕边缘容易误触,尤其是曲面屏。我习惯在驱动里加一个边缘区域判断:如果触摸点落在屏幕边缘5mm范围内,且没有其他手指在屏幕中央,就忽略这个事件。嗯,这个逻辑虽然简单,但用户体验提升很明显。
5.4 本章知识体系
下面这张图把本章的核心逻辑串起来了。从原始数据到坐标,再到协议上报,最后到手势判断——一条完整的链路。
从这张图可以看得很清楚:原始数据经过坐标计算变成可用的坐标值,再通过Type B协议打包上报,最后在驱动层做一些轻量级手势判断。每一层都有它的职责,也都有它的坑。
我个人觉得,触摸驱动最难的不是写代码,而是调参数。坐标滤波的alpha值、双击检测的时间窗口、边缘抑制的宽度——这些参数都得根据具体硬件和场景去试。我曾经为了一个曲面屏的边缘误触问题,调了整整两周参数,最后发现是玻璃盖板的介电常数和预想的不一样。
嗯,这一章的内容就到这里。下一章我们会聊到触摸屏的调试与测试,包括怎么用getevent抓数据、怎么分析中断延迟、以及常见的硬件故障定位方法。到时候再细说。