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 触摸屏驱动架构

触摸屏驱动通常遵循这个模式:

  1. probe:检测硬件、分配资源、注册中断、注册 input 设备。
  2. 中断处理:硬件触发中断,驱动读取数据。
  3. 数据解析:根据协议解析触摸坐标、压力、触点 ID。
  4. 事件上报:调用 input_report_abs、input_sync 等函数。
  5. 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 的缺点很明显:每次都要上报全部数据,带宽浪费严重。而且用户空间需要自己做触点追踪,增加了复杂度。

下面这张图能帮你快速理解两种协议的区别:

多点触控协议对比:Type A vs Type B Type A(无关联触点) 触点 1:X=120, Y=300, P=50 每次上报完整数据 触点 2:X=400, Y=500, P=45 每次上报完整数据 每次全部上报 input_sync() 缺点:带宽浪费,无 ID 追踪 用户空间需自行关联触点 Type B(有关联触点) Slot 0 (ID=0):X=120, Y=300 只上报变化的数据 Slot 1 (ID=1):X=400, Y=500 只上报变化的数据 仅上报变化 input_mt_sync_frame() + input_sync() 优点:带宽高效,内核自动追踪 ID 用户空间直接获取触点关联

选择哪种协议?我的建议很简单:查你的触摸屏控制器 datasheet。如果硬件提供触点 ID,用 Type B。如果不提供,用 Type A。别自己造轮子去模拟 ID 追踪,内核已经帮你做好了。

小技巧:调试多点触控时,可以用 getevent 命令查看上报的原始事件。配合 evtest 能看到更详细的信息。我调试时习惯开两个终端,一个跑 getevent -lt,一个跑应用,对照着看。

最后提一句,input_mt_init_slots 要在 probe 里调用,指定最大支持触点数和 flags。flags 里 INPUT_MT_DIRECT 表示触摸屏是直接接触式(比如手机屏),INPUT_MT_POINTER 表示是间接式(比如触摸板)。选错了会导致手势识别异常,我踩过这个坑。