2、触摸屏驱动开发:Linux输入子系统框架、触摸屏驱动架构、中断处理与轮询模式、多点触控协议(MT Protocol)
好,咱们进入第二章。这一章是触摸屏驱动的核心骨架。说白了,你要让触摸屏正常工作,就得搞明白三件事:数据怎么从硬件传到内核、内核怎么处理这些数据、用户空间怎么拿到这些数据。Linux 输入子系统就是干这个的。
我个人习惯把输入子系统比作一个「快递系统」。硬件是发货方,用户空间是收货方,中间的内核就是分拣中心和运输车队。你写的驱动,就是那个负责把货物从发货方搬到分拣中心的搬运工。
2.1 Linux 输入子系统框架
先看整体架构。输入子系统分三层:
- 核心层 (Input Core):提供注册接口、事件分发、设备管理。你不需要动它。
- 事件处理层 (Event Handler):比如
evdev、joydev。它把核心层发来的事件转换成用户空间能读的格式。 - 设备驱动层:这就是你写代码的地方。负责和硬件打交道,把触摸数据打包成标准事件上报。
我刚开始做驱动时,总觉得这层抽象多此一举。后来维护一个项目,需要同时支持电阻屏和电容屏,才发现这套框架的好处——驱动层只管上报,上层怎么解析、怎么分发,完全不用操心。
关键数据结构:struct input_dev。每个输入设备对应一个实例。你需要在 probe 函数里分配、设置、注册它。
注册流程大致是这样:
struct input_dev *input_dev;
int ret;
// 1. 分配 input_dev
input_dev = input_allocate_device();
if (!input_dev) {
dev_err(dev, "Failed to allocate input device\n");
return -ENOMEM;
}
// 2. 设置设备属性
input_dev->name = "My Touchscreen";
input_dev->phys = "input/ts";
input_dev->id.bustype = BUS_I2C;
input_dev->dev.parent = &i2c_dev->dev;
// 3. 设置支持的事件类型和按键
set_bit(EV_ABS, input_dev->evbit);
set_bit(EV_KEY, input_dev->evbit);
set_bit(BTN_TOUCH, input_dev->keybit);
// 4. 设置绝对坐标范围
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 1024, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 768, 0, 0);
// 5. 注册
ret = input_register_device(input_dev);
if (ret) {
dev_err(dev, "Failed to register input device\n");
input_free_device(input_dev);
return ret;
}
嗯,这里要注意:input_set_abs_params 的第五个参数是 fuzz(抖动容忍值),第六个参数是 flat(平坦区)。我遇到过触摸点轻微抖动导致光标乱跳的问题,后来把 fuzz 设成 4,问题就解决了。别小看这两个参数。
2.2 触摸屏驱动架构
触摸屏驱动通常遵循这个模式:
- probe:检测硬件、分配资源、注册中断、注册 input 设备。
- 中断处理:硬件触发中断,驱动读取数据。
- 数据解析:根据协议解析触摸坐标、压力、触点 ID。
- 事件上报:调用
input_report_abs、input_sync等函数。 - remove:释放资源、注销设备。
我见过不少新手把数据解析和事件上报混在一起写,结果代码又长又难调试。我的建议是:把「读寄存器」和「上报事件」拆成两个函数。哪怕现在觉得没必要,以后加新功能时会感谢自己的。
个人经验:probe 函数里尽量别做耗时操作。我曾在 probe 里做了一次固件升级,结果系统启动慢了 3 秒,被测试同事追着骂了一周。
2.3 中断处理与轮询模式
这是两种读取数据的方式,各有适用场景。
| 特性 | 中断模式 | 轮询模式 |
|---|---|---|
| 触发方式 | 硬件事件触发 | 定时器周期性查询 |
| CPU 占用 | 低(事件发生时才有开销) | 高(即使无事件也在查) |
| 响应延迟 | 低(微秒级) | 取决于轮询间隔 |
| 适用场景 | 电容屏、有中断引脚的设备 | 电阻屏、无中断引脚的设备 |
| 实现复杂度 | 中等(需处理中断共享、底半部) | 简单(定时器 + 读寄存器) |
中断处理的核心是:上半部(硬中断)只做最必要的事,比如读状态寄存器、清中断标志。真正的数据读取和上报,交给下半部(tasklet 或 workqueue)。
我曾经接手过一个项目,前任把整个数据读取都放在硬中断里。结果触摸屏一按,系统中断延迟飙升,音频都卡顿了。改成 workqueue 后,问题消失。
轮询模式虽然简单,但有个坑:轮询间隔设短了,CPU 飙高;设长了,触摸响应迟钝。我一般从 20ms 开始调,根据实际效果微调。如果硬件支持,还是优先用中断。
避坑指南:我曾经在轮询模式下忘了考虑触摸释放的检测。结果手指抬起来了,系统还以为一直按着。后来加了个「连续 N 次读到无触摸才上报释放」的逻辑,才算解决。
2.4 多点触控协议 (MT Protocol)
这是重点。Linux 内核支持两种多点触控协议:
- Type A:无关联触点。每次上报所有触点的完整状态。适用于不支持硬件追踪的控制器。
- Type B:有关联触点。每个触点有唯一 ID,只上报变化的部分。适用于支持硬件追踪的控制器。
现在主流电容屏控制器都支持 Type B。我建议新项目直接用 Type B,省带宽、效率高。
Type B 上报流程示例:
// 上报第一个触点
input_mt_slot(input_dev, 0);
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true);
input_report_abs(input_dev, ABS_MT_POSITION_X, x1);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y1);
input_report_abs(input_dev, ABS_MT_PRESSURE, p1);
// 上报第二个触点
input_mt_slot(input_dev, 1);
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true);
input_report_abs(input_dev, ABS_MT_POSITION_X, x2);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y2);
input_report_abs(input_dev, ABS_MT_PRESSURE, p2);
// 同步
input_mt_sync_frame(input_dev);
input_sync(input_dev);
注意 input_mt_slot 和 input_mt_sync_frame 的配合。我见过有人忘了调 input_mt_sync_frame,结果多点触摸时触点 ID 乱跳,手势识别完全失效。
Type A 就简单多了,每次上报所有触点,不需要 ID 管理:
// 上报所有触点
input_report_abs(input_dev, ABS_MT_POSITION_X, x1);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y1);
input_mt_sync(input_dev);
input_report_abs(input_dev, ABS_MT_POSITION_X, x2);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y2);
input_mt_sync(input_dev);
input_sync(input_dev);
Type A 的缺点很明显:每次都要上报全部数据,带宽浪费严重。而且用户空间需要自己做触点追踪,增加了复杂度。
下面这张图能帮你快速理解两种协议的区别:
选择哪种协议?我的建议很简单:查你的触摸屏控制器 datasheet。如果硬件提供触点 ID,用 Type B。如果不提供,用 Type A。别自己造轮子去模拟 ID 追踪,内核已经帮你做好了。
小技巧:调试多点触控时,可以用 getevent 命令查看上报的原始事件。配合 evtest 能看到更详细的信息。我调试时习惯开两个终端,一个跑 getevent -lt,一个跑应用,对照着看。
最后提一句,input_mt_init_slots 要在 probe 里调用,指定最大支持触点数和 flags。flags 里 INPUT_MT_DIRECT 表示触摸屏是直接接触式(比如手机屏),INPUT_MT_POINTER 表示是间接式(比如触摸板)。选错了会导致手势识别异常,我踩过这个坑。