第1章:Linux内核触摸驱动基础

各位同学,欢迎来到我们的实战课程。今天咱们聊聊触摸驱动的根基——Input子系统框架、触摸屏协议,还有设备树配置和中断处理。这些东西,说白了就是触摸驱动的地基。地基打不牢,后面移植起来会非常痛苦。

我记得刚入行那会儿,接手一个老项目的触摸屏移植。当时对Input子系统一知半解,结果中断处理写错了,导致触摸上报时断时续。嗯,那几天加班到凌晨的滋味,至今难忘。所以这一章,我会把关键点掰开揉碎了讲。

1.1 Input子系统框架:三层架构

Linux的Input子系统,其实就三层:

  • 核心层(Core):负责设备注册、事件分发。你写驱动时基本不用碰它。
  • 事件处理层(Handler):比如evdev、mousedev。把内核事件转成用户空间能读的数据。
  • 设备驱动层(Driver):这才是我们写代码的地方。直接跟硬件打交道。

我个人的习惯是,先搞清楚数据流:手指按下 → 硬件中断 → 驱动读取坐标 → 上报给核心层 → 通过evdev传给应用。你想想看,这个链条上任何一个环节出问题,触摸就不灵了。

核心要点:驱动层只需要调用input_report_key()和input_report_abs()这类函数,剩下的核心层帮你搞定。别自己瞎折腾。

1.2 触摸屏协议:单点 vs 多点

触摸协议分两种,这个必须分清楚。搞混了,上报的数据应用层根本看不懂。

协议类型特点适用场景
单点触摸(Single Touch)一次只报一个点,用BTN_TOUCH + ABS_X/ABS_Y老式电阻屏、简单按键
多点触摸(MT Protocol B)支持多个手指,用ABS_MT_POSITION_X/Y + 槽位管理电容屏、手势识别

我建议你直接上Protocol B。为什么?现在市面上99%的触摸屏都是多点的。Protocol A虽然也能报多点,但槽位管理不清晰,应用层处理起来容易乱。

举个例子,Protocol B的上报流程:

// 手指按下
input_mt_slot(input_dev, 0);  // 槽位0
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true);
input_report_abs(input_dev, ABS_MT_POSITION_X, x);
input_report_abs(input_dev, ABS_MT_POSITION_Y, y);

// 第二根手指
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_sync(input_dev);

避坑指南:我曾经在同步上报前忘了调用input_mt_sync_frame(),结果两根手指的位置老是串。后来查了半天才发现,Protocol B要求每个帧开始前必须调用这个函数。

1.3 设备树配置:让内核认识你的触摸屏

设备树,说白了就是告诉内核:你的触摸屏接在哪个I2C总线上,用哪个中断引脚,电源怎么控制。我见过太多人设备树写错了,驱动死活加载不上。

一个典型的触摸屏设备树节点长这样:

&i2c3 {
    status = "okay";
    
    touchscreen@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <1024>;
        touchscreen-size-y = <600>;
    };
};

这里有几个关键点:

  • compatible:必须跟驱动里的of_match_table匹配。拼错了驱动不认。
  • reg:I2C地址。别跟其他设备冲突。
  • interrupts:中断号和触发方式。我建议用边沿触发,电平触发容易出问题。

注意:我曾经遇到一个坑——设备树里忘了配reset-gpios,结果触摸屏上电后没复位,初始化一直失败。后来加上复位引脚,一切正常。所以电源管理和复位引脚,一定要配全。

1.4 中断处理:触摸响应的关键

触摸屏的中断处理,是驱动里最核心的部分。处理不好,要么丢点,要么响应慢。

标准做法是:

  1. 在probe函数里注册中断:devm_request_threaded_irq()
  2. 中断服务函数里只做一件事:唤醒工作队列或中断线程
  3. 在中断线程或工作队列里读取数据、上报事件

为什么不能直接在中断里读数据?因为I2C读取是慢速操作,在中断上下文里做会阻塞系统。我个人的经验是,用devm_request_threaded_irq()的线程化中断,既简单又安全。

static irqreturn_t touch_irq_handler(int irq, void *dev_id) {
    struct touch_device *ts = dev_id;
    
    // 这里只唤醒线程,不做实际工作
    return IRQ_WAKE_THREAD;
}

static irqreturn_t touch_irq_thread(int irq, void *dev_id) {
    struct touch_device *ts = dev_id;
    
    // 读取触摸数据
    touch_read_data(ts);
    
    // 上报事件
    touch_report_events(ts);
    
    return IRQ_HANDLED;
}

小技巧:我习惯在中断线程里加一个超时保护。如果连续读取失败超过3次,就复位触摸屏。这个机制帮我解决过不少硬件偶发死锁的问题。

1.5 实战中的常见问题

最后,分享几个我踩过的坑:

  • 中断号冲突:设备树里配的中断号,跟其他设备共用了一个引脚。用cat /proc/interrupts检查一下。
  • 坐标范围不对:设备树里配的touchscreen-size-x/y,跟实际硬件不匹配。导致触摸位置偏移。
  • 上报频率太低:中断处理里加了太多延时。触摸响应慢得像蜗牛。

嗯,这一章的内容就到这里。记住,触摸驱动移植,Input子系统框架是骨架,协议是血肉,设备树是身份证,中断处理是灵魂。四者缺一不可。

下一章,咱们会深入I2C通信和具体的触摸芯片初始化流程。到时候见。