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_ABS和EV_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_id和of_device_id。前者是传统方式,后者是设备树方式。现在新项目基本都用设备树了,但老内核上两种都得支持。
避坑指南:
我曾经在调试一款触控芯片时,发现probe函数根本没被调用。查了三天,最后发现是设备树里reg属性写错了。I2C地址是0x38,我写成了0x1c(左移了一位)。记住,设备树里的reg是7位地址,而驱动里通常是8位地址。这个坑,我估计十个工程师里有八个都踩过。
probe函数里一般做这几件事:
- 从设备树读取配置参数(比如中断引脚、复位引脚)
- 初始化I2C通信,读取芯片ID确认硬件正常
- 申请中断,注册中断处理函数
- 初始化Input设备,注册到Input子系统
- 启动工作队列或定时器(如果需要轮询)
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 三者的协作流程
说了这么多,咱们用一张图来总结一下整个数据流:
这张图把整个流程串起来了。你从左边看:设备树告诉I2C驱动“芯片地址是0x38,中断用GPIO1_13”。驱动初始化时读取这些配置,注册中断。当手指触摸屏幕,芯片通过中断通知CPU,驱动在中断处理函数里通过I2C读取坐标数据,然后调用Input子系统的API上报事件。最后,用户空间的getevent或触摸框架就能收到这些事件了。
总结一下,三个关键点:
- 设备树是静态配置,描述硬件连接方式
- I2C驱动是数据搬运工,负责和芯片通信
- Input子系统是事件标准化接口,让上层不用关心底层是什么芯片
这三者缺一不可。任何一个环节出问题,触摸屏就废了。
嗯,我记得刚入行时,带我的老工程师说过一句话:“驱动开发,七分看硬件,三分看代码。” 设备树配错了,I2C地址写反了,中断号搞混了——这些硬件层面的问题,代码写得再漂亮也没用。所以,拿到一块新板子,我建议你先用i2cdetect确认芯片地址,用cat /proc/interrupts确认中断有没有触发。这些基本功,比什么都重要。