4、驱动层优化:Linux/Android触摸驱动中断处理与轮询模式选择
驱动层是触摸响应延迟的「第一道关卡」。你想想看,手指点下去,第一个知道这件事的就是驱动。驱动怎么把这件事告诉上层,直接决定了延迟的起点有多高。
我个人习惯把驱动层的优化分成两条路:中断模式和轮询模式。选对了,延迟能砍掉一半。选错了,CPU 跑得冒烟,延迟还下不来。
4.1 中断模式:默认选择,但坑不少
大部分触摸屏驱动默认用中断模式。手指一碰,硬件拉高中断引脚,驱动在中断服务程序(ISR)里读数据,然后上报。听起来很直接,对吧?
但我在项目中遇到过一个问题:某款芯片的中断处理函数里,居然做了 I2C 读写的同步等待。I2C 本身就跑在 400kHz,一次读写几十微秒,再加上中断上下文不能睡眠,整个系统都被拖住了。
正确的做法是:ISR 里只做最轻量的事——标记一个标志位,或者唤醒一个内核线程。真正的数据读取和上报,放到下半部(Bottom Half)去做。
/* 不推荐:在 ISR 里直接读数据 */
static irqreturn_t touch_isr_bad(int irq, void *dev_id)
{
struct touch_data *ts = dev_id;
/* 这里做了 I2C 同步读,耗时约 2ms */
i2c_master_recv(ts->client, ts->buf, 10);
input_report_key(ts->input, BTN_TOUCH, 1);
return IRQ_HANDLED;
}
/* 推荐:ISR 只唤醒工作队列 */
static irqreturn_t touch_isr_good(int irq, void *dev_id)
{
struct touch_data *ts = dev_id;
schedule_work(&ts->work); /* 下半部处理 */
return IRQ_HANDLED;
}
4.2 轮询模式:什么时候该用?
轮询模式听起来很「笨」——CPU 不停地问「你按了吗?你按了吗?」。但有些场景下,轮询反而比中断快。
为什么会这样?因为中断有开销。中断从触发到 CPU 响应,要经过异常向量表、保存上下文、跳转 ISR……这一套下来,少说几十微秒。如果触摸屏的采样率很高(比如 1000Hz),中断频繁触发,这部分开销就不可忽视了。
我做过一个实验:某款触控芯片在 1000Hz 采样率下,中断模式的平均响应延迟是 1.8ms,而轮询模式(用高精度定时器,1ms 轮询一次)只有 0.9ms。轮询反而快了一倍。
| 对比项 | 中断模式 | 轮询模式 |
|---|---|---|
| 延迟稳定性 | 受中断负载影响大 | 稳定,可预测 |
| CPU 占用 | 低(事件触发) | 高(持续占用) |
| 适用场景 | 低频触摸(≤200Hz) | 高频采样(≥500Hz) |
| 实现复杂度 | 简单 | 需要调优轮询间隔 |
4.3 混合策略:我常用的方案
其实没必要非黑即白。我常用的方案是:中断唤醒 + 轮询读取。
具体做法:
- 手指按下时,中断触发,唤醒 CPU
- 唤醒后,驱动切换到轮询模式,以 1ms 间隔连续读取数据
- 手指抬起后,切回中断等待模式
这样既避免了空闲时的 CPU 浪费,又保证了触摸过程中的低延迟。
4.4 Android 特有的考量:Input HAL 与驱动交互
在 Android 系统里,驱动之上还有一层 Input HAL。这里有个容易踩的坑:驱动上报数据后,HAL 层可能会做额外的滤波或坐标转换,这些操作也会增加延迟。
我建议的做法是:
- 驱动层只做最原始的坐标读取,不做任何滤波
- 滤波和校准放到 HAL 层或上层去做
- 驱动和 HAL 之间使用共享内存或快速通道传递数据
嗯,这里要注意:有些厂商的 HAL 实现里,居然用了 Binder 通信来传递触摸数据。Binder 一次调用几十微秒,累积下来延迟就上去了。如果发现这种情况,建议改成共享内存 + 原子操作。
4.5 驱动层优化决策树
说了这么多,到底怎么选?我画了一张图,帮你快速决策:
4.6 避坑指南
最后,分享几个我踩过的坑:
- 中断共享问题:有些 SoC 的触摸中断和别的设备共享一个 IRQ 号。我曾经遇到过触摸中断被另一个设备频繁触发,导致触摸响应被「饿死」。解决办法是确认中断是否独占,或者使用 threaded IRQ。
- 轮询定时器精度:Linux 的普通定时器精度只有 jiffy(通常 4ms),用来做 1ms 轮询根本不够。必须用高精度定时器(hrtimer)。
- 电源管理冲突:轮询模式会阻止 CPU 进入深度睡眠。如果设备在待机时还需要响应触摸(比如唤醒屏幕),轮询模式就不合适了。