2. 触摸技术基础

好,我们直接进入正题。上一章我们聊了多点触摸的硬件基础,这一章要啃的是 Linux 输入子系统的核心。说白了,就是搞清楚你的触摸屏驱动怎么跟内核“对话”,数据怎么从硬件一路跑到用户空间。

我刚开始接触输入子系统时,觉得它就是个黑盒子。后来被一个 bug 折磨了两天,才老老实实把源码啃了一遍。嗯,今天我把这些经验拆成三块讲:input_dev 结构体、事件上报流程、input_handler 匹配机制。这三块搞懂了,你写触摸驱动时心里就有底了。

2.1 input_dev 结构体:驱动的“身份证”

每个输入设备在内核里都对应一个 input_dev 结构体。你可以把它理解成设备的“身份证”——内核靠它知道你是谁、你能干什么。

先看一眼核心字段:

struct input_dev {
    const char *name;           // 设备名,比如 "Goodix Touchscreen"
    const char *phys;           // 物理路径,比如 "i2c-0/0-005d/input0"
    struct input_id id;         // 总线类型、厂商ID、产品ID、版本号

    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];     // 支持的事件类型位图
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];   // 支持的按键码位图
    unsigned long relbit[BITS_TO_LONGS(REL_CNT)];   // 支持的相对坐标位图
    unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];   // 支持的绝对坐标位图
    unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];   // 支持的其他事件位图

    struct input_absinfo absinfo[ABS_CNT];           // 绝对轴信息(分辨率、最大最小值)

    struct list_head h_list;    // 挂载到该设备的 handler 链表
    struct list_head node;      // 全局 input_dev_list 链表节点

    int (*open)(struct input_dev *dev);
    void (*close)(struct input_dev *dev);
    int (*flush)(struct input_dev *dev, struct file *file);
    bool (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
};

重点提醒:位图字段(evbit、keybit、absbit 等)是内核判断设备类型的依据。你设错了,evdev 可能根本不认你的设备。

我个人习惯在 probe 函数里这样初始化:

static int touch_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct input_dev *input_dev;
    int ret;

    input_dev = devm_input_allocate_device(&client->dev);
    if (!input_dev)
        return -ENOMEM;

    input_dev->name = "My Touchscreen";
    input_dev->phys = "i2c-0/0-005d/input0";
    input_dev->id.bustype = BUS_I2C;
    input_dev->id.vendor = 0x1234;
    input_dev->id.product = 0x5678;
    input_dev->id.version = 0x0100;

    // 设置支持的事件类型
    __set_bit(EV_ABS, input_dev->evbit);        // 支持绝对坐标
    __set_bit(EV_KEY, input_dev->evbit);        // 支持按键事件
    __set_bit(BTN_TOUCH, input_dev->keybit);    // 触摸按键

    // 设置触摸点坐标范围
    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);
    input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);

    // 注册设备
    ret = input_register_device(input_dev);
    if (ret) {
        dev_err(&client->dev, "Failed to register input device\n");
        return ret;
    }

    return 0;
}

小技巧:用 devm_input_allocate_device 代替 input_allocate_device,资源自动释放,省得你写 remove 函数时忘了注销。

2.2 事件上报流程:从硬件到用户空间

设备注册好了,接下来就是上报数据。触摸屏驱动上报事件的流程其实很固定,我总结成三步:

  1. 填充事件:调用 input_event() 或封装好的 input_report_abs()、input_report_key()
  2. 同步提交:调用 input_sync() 告诉内核这一批事件发完了
  3. 内核分发:input_event() 内部通过 input_pass_event() 分发给所有匹配的 handler

看一个实际的多点触摸上报例子:

static void touch_report(struct touch_device *touch, int id, int x, int y, int pressure)
{
    struct input_dev *input = touch->input;

    // 第一步:上报触摸点信息
    input_report_abs(input, ABS_MT_SLOT, id);           // 指定槽位
    input_report_abs(input, ABS_MT_TRACKING_ID, id);    // 跟踪ID
    input_report_abs(input, ABS_MT_POSITION_X, x);
    input_report_abs(input, ABS_MT_POSITION_Y, y);
    input_report_abs(input, ABS_MT_TOUCH_MAJOR, pressure);

    // 第二步:同步提交
    input_sync(input);
}

这里有个坑,我曾经踩过:每次上报完触摸点必须调 input_sync()。内核靠 SYN_REPORT 事件来切分数据包。你不调 sync,用户空间收不到完整的一帧数据,触摸点会卡住不动。

事件在内核里的流转路径是这样的:

驱动调用 input_event()
    └─> input_handle_event()        // 事件过滤、去重
        └─> input_pass_event()      // 遍历 dev->h_list,分发给 handler
            └─> handler->event()    // 比如 evdev 的 evdev_event()
                └─> 写入 evdev 客户端缓冲区
                    └─> 唤醒等待的 read() 进程

注意:input_handle_event() 里有个去重逻辑——如果上报的值跟上次一样,内核会直接丢弃。所以触摸驱动里不要重复上报相同的坐标值,否则用户空间可能收不到事件。

我建议你在调试时打开 CONFIG_INPUT_EVBUG 内核选项,它会把所有输入事件打印到内核日志里。不过生产环境千万别开,日志会被刷爆。

2.3 input_handler 匹配机制:谁来处理你的事件?

设备注册了,事件上报了,那谁来消费这些事件?答案是 input_handler。

内核里预置了好几个 handler:

Handler 名称 功能 典型设备节点
evdev 通用事件处理,用户空间通过 /dev/input/eventX 读取 /dev/input/event0
joydev 游戏杆专用 /dev/input/js0
mousedev 模拟鼠标(把绝对坐标转成相对移动) /dev/input/mouse0
keyboard 键盘专用 /dev/tty 等

匹配过程其实不复杂。每个 handler 都有一个 id_table 或 match() 回调。当 input_register_device() 被调用时,内核会遍历所有已注册的 handler,逐个调用 handler->match() 或检查 handler->id_table。

拿 evdev 举例,它的匹配逻辑很简单:

static const struct input_device_id evdev_ids[] = {
    { .driver_info = 1 },   // 匹配所有设备
    { },                    // 终止符
};

static int evdev_connect(struct input_handler *handler, struct input_dev *dev,
                         const struct input_device_id *id)
{
    // 为每个匹配的设备创建一个 evdev 客户端
    // 分配次设备号,创建 /dev/input/eventX 节点
}

说白了,evdev 来者不拒——只要是个输入设备,它就给你创建个 eventX 节点。但有些 handler 会挑设备,比如 mousedev 只匹配设置了 EV_REL 或 EV_ABS 且没有 EV_KEY 的设备。

关键点:你的触摸屏驱动要想被 evdev 识别,evbit 里必须设置 EV_ABS。如果你只设了 EV_KEY,内核会把它当成按键设备,不会创建触摸相关的 event 节点。

匹配完成后,handler 会调用 connect() 回调,创建一个 input_handle 结构体,把它挂到 dev->h_list 上。这样后面上报事件时,input_pass_event() 就能找到对应的 handler 去分发。

我画了一张图,把整个流程串起来:

Linux 输入子系统核心流程 Linux 输入子系统核心流程 驱动调用 input_register_device() 遍历 handler 列表,执行 match() handler->connect() 创建 input_handle 驱动上报事件 input_event() input_pass_event() 分发给 handler 用户空间通过 /dev/input/eventX 读取 匹配依据 handler->id_table 或 handler->match() 检查 dev->evbit 等位图 常见 handler evdev → /dev/input/eventX mousedev → /dev/input/mouseX joydev → /dev/input/jsX 事件上报顺序 1. 填充事件数据 2. 调用 input_sync() 3. 内核自动分发

2.4 避坑指南:我踩过的三个坑

最后分享几个实战中容易翻车的地方:

  • 位图没设全:只设了 EV_ABS 忘了设 EV_KEY,结果 BTN_TOUCH 事件发不出去。用户空间收不到触摸按下/抬起状态。
  • ABS_MT_SLOT 顺序搞错:必须先设 SLOT,再设其他 ABS_MT_* 值。顺序反了,内核会把坐标值关联到错误的槽位。
  • 忘记调 input_sync():这个我犯过好几次。调试时发现触摸点不动了,一查日志,SYN_REPORT 没发出去。

严重警告:不要在中断上下文里调用 input_register_device()!这个函数会睡眠,而中断上下文不允许睡眠。我曾经在客户现场排查一个随机死机问题,最后发现是驱动在中断里注册设备导致的。