4、触摸屏驱动核心:probe函数实现、中断处理、坐标上报、多点触控协议

驱动移植做到这一步,其实真正的硬仗才刚刚开始。前面那些设备树配置、I2C 适配,说白了都是搭台子。probe 函数、中断处理、坐标上报、多点触控协议——这四个东西才是驱动真正唱戏的部分。

我个人习惯把 probe 函数比作驱动的「构造函数」。它不成功,后面一切免谈。而中断处理和坐标上报,则是驱动的心脏跳动。至于多点触控协议,嗯,这里面的坑我踩过不少,待会慢慢聊。

4.1 probe 函数:驱动的入场券

probe 函数什么时候被调用?当内核发现设备树里有一个 compatible 字符串,跟驱动里注册的 of_match_table 匹配上了,就会调用 probe。说白了,就是内核说:「嘿,你的硬件我找到了,你来初始化它吧。」

一个典型的 probe 函数,我通常会做这几件事:

  1. 获取设备树资源——比如中断号、GPIO、供电电压。
  2. 初始化 I2C 客户端——设置设备的寄存器、工作模式。
  3. 分配输入设备结构体——也就是 struct input_dev。
  4. 设置设备属性和事件类型——告诉内核这个设备能上报什么。
  5. 注册输入设备——调用 input_register_device。
  6. 申请中断——注册中断处理函数。

给你看一段我实际项目里用过的 probe 骨架代码:

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

    // 1. 分配私有数据结构
    ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
    if (!ts)
        return -ENOMEM;

    // 2. 获取中断号
    ts->irq = client->irq;
    if (!ts->irq) {
        dev_err(&client->dev, "no IRQ specified\n");
        return -EINVAL;
    }

    // 3. 初始化输入设备
    input = devm_input_allocate_device(&client->dev);
    if (!input)
        return -ENOMEM;

    input->name = "Touchscreen";
    input->id.bustype = BUS_I2C;
    input->dev.parent = &client->dev;

    // 4. 设置支持的事件类型
    __set_bit(EV_ABS, input->evbit);
    __set_bit(EV_KEY, input->evbit);
    __set_bit(BTN_TOUCH, input->keybit);

    // 5. 设置触摸坐标范围
    input_set_abs_params(input, ABS_MT_POSITION_X, 0, X_MAX, 0, 0);
    input_set_abs_params(input, ABS_MT_POSITION_Y, 0, Y_MAX, 0, 0);
    input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);

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

    // 7. 申请中断
    ret = devm_request_threaded_irq(&client->dev, ts->irq, NULL,
                    touch_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                    "touch", ts);
    if (ret) {
        dev_err(&client->dev, "failed to request IRQ\n");
        return ret;
    }

    ts->input = input;
    i2c_set_clientdata(client, ts);

    return 0;
}
我的习惯:probe 函数里尽量用 devm_ 系列 API。这样一旦驱动卸载或者 probe 失败,资源会自动释放,省得你手动 cleanup 写到手软。我曾经在一个项目里忘了释放中断,结果每次 insmod/rmmod 循环三次,系统就崩了——排查了两天才发现是资源泄漏。

4.2 中断处理:别让 CPU 空转

触摸屏的中断处理,我一般用线程化中断。为什么?因为触摸屏的 I2C 读取操作可能会阻塞,如果在顶半部做,会拖死整个系统。

线程化中断的好处很明显:

  • 顶半部只做最轻量的事——比如唤醒一个等待队列。
  • 底半部(线程)可以安心地读 I2C、解析数据、上报坐标。
  • 不会阻塞其他高优先级中断。

来看一个实际的中断处理函数:

static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_data *ts = dev_id;

    // 读取触摸数据
    touch_read_data(ts);

    // 解析并上报
    touch_report_data(ts);

    return IRQ_HANDLED;
}

你可能会问:「为什么不在顶半部直接读 I2C?」

嗯,I2C 通信本身是同步的,如果总线上有其他设备在传输,你的中断处理函数就会被阻塞。在顶半部里阻塞,相当于把整个系统的中断响应都卡住了。我曾经见过一个产品,触摸屏中断处理里直接调了 i2c_master_send,结果系统音频出现爆音——因为音频 DMA 的中断被堵了。

避坑:千万不要在中断顶半部里做 I2C 读写!哪怕你的触摸屏芯片手册上说「响应很快」,也请用线程化中断或者 workqueue。这是血的教训。

4.3 坐标上报:从裸数据到用户空间

坐标上报,说白了就是把触摸屏芯片返回的原始数据,转换成用户空间能理解的坐标值。

原始数据通常长这样:

// 假设从芯片读到的 5 字节数据
byte[0]: 状态标志 (触摸/释放)
byte[1]: X 坐标高字节
byte[2]: X 坐标低字节
byte[3]: Y 坐标高字节
byte[4]: Y 坐标低字节

解析之后,我们需要调用 input 子系统提供的 API 来上报:

static void touch_report_data(struct touch_data *ts)
{
    int x, y;
    u8 buf[5];

    // 读取原始数据
    touch_i2c_read(ts->client, buf, 5);

    // 解析坐标
    x = (buf[1] << 8) | buf[2];
    y = (buf[3] << 8) | buf[4];

    // 上报触摸事件
    input_report_key(ts->input, BTN_TOUCH, 1);
    input_report_abs(ts->input, ABS_MT_POSITION_X, x);
    input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
    input_mt_sync(ts->input);
    input_sync(ts->input);
}

这里有个细节:input_sync 必须调用。它告诉内核:「这一批事件发完了,你可以打包提交给上层了。」如果不调,用户空间永远收不到数据。

核心原则:每次触摸事件结束后,必须调用 input_sync。漏掉它,触摸屏就会「卡死」——不是真的死,而是数据积压在缓冲区里出不去。

4.4 多点触控协议:Type A 还是 Type B?

多点触控协议,Linux 内核提供了两种:Type A 和 Type B。

特性 Type A Type B
适用场景 不支持硬件追踪的触摸屏 支持硬件追踪(有 Slot ID)
上报方式 每次上报所有触摸点 按 Slot 分别上报,只更新变化的点
API 调用 input_mt_sync input_mt_slot + input_mt_report_slot_state
典型芯片 较老的电阻屏、部分电容屏 现代电容屏(如 Goodix、FocalTech)

我个人建议:能用 Type B 就用 Type B。为什么?

  • Type B 更高效——它只上报变化的触摸点,而不是全量刷新。
  • Type B 支持 Slot 概念,可以追踪每个手指的连续移动。
  • Type B 与 MTK(多点触控内核)的集成更自然,手势识别、防抖处理都更方便。

Type B 的典型实现长这样:

static void touch_report_mt(struct touch_data *ts, int slot, int x, int y, bool active)
{
    // 设置当前操作的 Slot
    input_mt_slot(ts->input, slot);
    // 报告触摸状态
    input_mt_report_slot_state(ts->input, MT_TOOL_FINGER, active);

    if (active) {
        // 上报坐标
        input_report_abs(ts->input, ABS_MT_POSITION_X, x);
        input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
    }

    // 别忘了 sync
    input_sync(ts->input);
}
我的经验:Type B 的 Slot 数量一定要跟硬件支持的触摸点数一致。比如芯片支持 5 点触摸,你就设 5 个 Slot。设少了,第 6 根手指按上去就没反应;设多了,浪费内存,而且可能上报一些「幽灵触摸」。

4.5 本章知识体系

下面这张图,是我自己整理的多点触控驱动核心流程。你看一眼,应该能对整个脉络有个直观印象:

触摸屏驱动核心流程 probe 函数 获取设备树资源 注册 input_dev 线程化中断处理 坐标解析与上报 多点触控协议 Type A: 全量上报 Type B: 按 Slot 上报 推荐使用 Type B 支持硬件追踪

从 probe 开始,一路走到坐标上报,中间每一步都有坑。但只要你把设备树资源拿对了、中断处理用线程化、坐标上报别漏 input_sync、多点触控选 Type B——这套组合拳打下来,驱动基本就稳了。

嗯,这一章的内容就到这里。代码你可以直接拿去用,但记得根据你手里的芯片手册调整寄存器地址和数据格式。每个芯片的协议都有细微差别,这是移植工作里最磨人的地方。