3、TSP驱动框架:Linux Input子系统、I2C设备驱动模型、设备树配置

好,咱们今天聊点硬核的。触摸屏驱动,说白了就是让手指的触碰变成系统能理解的事件。这背后有三个东西你必须搞明白:Input子系统I2C设备驱动模型、还有设备树配置。这三者就像三根柱子,撑起了整个TSP驱动。

我个人习惯把这三者的关系比作一个快递系统:
设备树是“地址簿”,告诉内核芯片在哪、怎么连;
I2C驱动是“快递员”,负责把数据从芯片搬回来;
Input子系统是“分拣中心”,把原始数据打包成标准事件,发给上层。

嗯,咱们一个一个拆开讲。

3.1 Linux Input子系统——触摸事件的“翻译官”

先问一个问题:为什么要有Input子系统?
你想想看,如果没有它,每个触摸屏驱动都得自己写一套上报事件的接口。那上层应用就疯了——今天适配这个驱动,明天适配那个驱动。Input子系统就是来统一这件事的。

它的核心结构其实很简单:

  • 核心层(Input Core):管理设备注册、事件分发
  • 事件处理层(Event Handlers):比如evdev,把事件变成/dev/input/eventX
  • 驱动层(Drivers):我们写的TSP驱动就挂在这里

我在项目中遇到过一个问题:某款触控芯片上报的坐标偶尔会跳变。查了半天,发现是驱动里上报事件的顺序错了。记住,先上报ABS_MT_POSITION_X/Y,再上报ABS_MT_TRACKING_ID,最后调用input_sync()。顺序一乱,上层手势识别就全崩了。

核心API,必须记住:

  • input_allocate_device() — 分配input设备
  • input_set_abs_params() — 设置绝对坐标范围
  • input_mt_init_slots() — 初始化多点触控槽位
  • input_mt_slot() + input_mt_report_slot_state() — 上报触摸状态
  • input_sync() — 同步事件,告诉内核“这批数据发完了”

我曾经犯过一个低级错误:忘记在probe函数里调用input_set_capability()设置事件类型。结果驱动加载成功了,但上层收不到任何触摸事件。排查了一整天,最后发现是EV_ABSEV_KEY没设置。嗯,这种坑踩过一次就再也不会忘了。

3.2 I2C设备驱动模型——芯片和CPU的“对话通道”

大多数触控芯片都挂在I2C总线上。为什么?因为I2C引脚少、协议简单,适合这种数据量不大的场景。

I2C设备驱动的写法,其实有固定套路:

static const struct i2c_device_id xxx_ts_id[] = {
    { "xxx_touch", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, xxx_ts_id);

static const struct of_device_id xxx_ts_of_match[] = {
    { .compatible = "vendor,xxx-ts" },
    { }
};
MODULE_DEVICE_TABLE(of, xxx_ts_of_match);

static struct i2c_driver xxx_ts_driver = {
    .probe    = xxx_ts_probe,
    .remove   = xxx_ts_remove,
    .id_table = xxx_ts_id,
    .driver = {
        .name = "xxx_touch",
        .of_match_table = xxx_ts_of_match,
    },
};
module_i2c_driver(xxx_ts_driver);

注意看,这里有两个匹配表:i2c_device_idof_device_id。前者是传统方式,后者是设备树方式。现在新项目基本都用设备树了,但老内核上两种都得支持。

避坑指南:

我曾经在调试一款触控芯片时,发现probe函数根本没被调用。查了三天,最后发现是设备树里reg属性写错了。I2C地址是0x38,我写成了0x1c(左移了一位)。记住,设备树里的reg是7位地址,而驱动里通常是8位地址。这个坑,我估计十个工程师里有八个都踩过。

probe函数里一般做这几件事:

  1. 从设备树读取配置参数(比如中断引脚、复位引脚)
  2. 初始化I2C通信,读取芯片ID确认硬件正常
  3. 申请中断,注册中断处理函数
  4. 初始化Input设备,注册到Input子系统
  5. 启动工作队列或定时器(如果需要轮询)

3.3 设备树配置——硬件信息的“身份证”

设备树(Device Tree)是什么?说白了,就是告诉内核“我这块板子上有什么硬件、怎么连”。以前这些东西都硬编码在板级文件里,现在全挪到.dts文件里了。

一个典型的TSP设备树节点长这样:

&i2c3 {
    status = "okay";
    touchscreen@38 {
        compatible = "vendor,xxx-ts";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
        vdd-supply = <&pm8916_l17>;
        touchscreen-size-x = <1080>;
        touchscreen-size-y = <1920>;
    };
};

我来解释一下每个字段的含义:

属性 含义 注意事项
compatible 匹配驱动的关键字符串 必须和驱动里of_match_table一致
reg I2C从设备地址 7位地址,不要左移
interrupts 中断号和触发方式 注意中断控制器是哪个GPIO bank
reset-gpios 复位引脚 GPIO_ACTIVE_LOW表示低电平复位
touchscreen-size-x/y 触摸屏分辨率 不是屏幕分辨率,是触控IC上报的最大值

重要提醒:

设备树里的属性名,驱动里用of_property_read_u32()devm_gpiod_get()来读取。我曾经见过有人把属性名拼错了,比如把"touchscreen-size-x"写成了"touch-size-x",结果驱动读到的全是0。设备树解析失败不会报错,只会默默返回默认值。这种bug最难查。

3.4 三者的协作流程

说了这么多,咱们用一张图来总结一下整个数据流:

TSP驱动框架数据流 触控芯片(硬件) I2C设备驱动(读取原始数据) Input子系统(事件封装、多点触控协议) 用户空间(手势识别、UI响应) 设备树配置 中断(硬件触发) 设备树提供配置 → I2C驱动读取数据 → Input子系统封装事件 → 用户空间消费事件

这张图把整个流程串起来了。你从左边看:设备树告诉I2C驱动“芯片地址是0x38,中断用GPIO1_13”。驱动初始化时读取这些配置,注册中断。当手指触摸屏幕,芯片通过中断通知CPU,驱动在中断处理函数里通过I2C读取坐标数据,然后调用Input子系统的API上报事件。最后,用户空间的getevent或触摸框架就能收到这些事件了。

总结一下,三个关键点:

  1. 设备树是静态配置,描述硬件连接方式
  2. I2C驱动是数据搬运工,负责和芯片通信
  3. Input子系统是事件标准化接口,让上层不用关心底层是什么芯片

这三者缺一不可。任何一个环节出问题,触摸屏就废了。

嗯,我记得刚入行时,带我的老工程师说过一句话:“驱动开发,七分看硬件,三分看代码。” 设备树配错了,I2C地址写反了,中断号搞混了——这些硬件层面的问题,代码写得再漂亮也没用。所以,拿到一块新板子,我建议你先用i2cdetect确认芯片地址,用cat /proc/interrupts确认中断有没有触发。这些基本功,比什么都重要。

公众号:蓝海资料掘金营,微信deep3321