简介:这份资源是面向 Linux 内核与嵌入式驱动开发者的 Synaptics 触摸屏驱动源码包,主要解决触摸屏、触摸板及触摸显示器在 Linux 系统下通过 I2C/SPI 总线完成数据交互与指令识别的问题,适用于智能手机、平板、笔记本及台式显示器等设备的驱动移植与调试场景。压缩包共 106 个文件,约 2.09MB,包含 9 个 c 源文件、2 个 h 头文件、12 个 sample 示例、3 个 makefile、2 个 kconfig 及 panda_defconfig 等配置,另有 synaptics_reg_access、synaptics_fw_updater 等工具与固件更新相关文件,覆盖驱动主体、寄存器访问、固件升级与编译配置等模块。目前已有 159 人学习下载。借助这些源码与示例,读者可快速理解 I2C/SPI 通信流程、中断与输入子系统接入方式,并参考配置项完成内核编译与设备适配,为触摸驱动开发与排错提供完整参考。
1. Synaptics 触摸 I2C/SPI Linux 驱动:从设备树到中断上报的完整落地路径
很多人在调试触摸屏时,第一反应是“驱动没加载”或者“固件没烧进去”,但实际项目里,Synaptics 触摸控制器在 Linux 下的问题,八成出在 I2C/SPI 总线配置、中断触发方式、电源时序这三件事上。这个标题讲的就是如何让一颗 Synaptics 触摸芯片通过 I2C 或 SPI 接口,在 Linux 内核里跑起来,并且稳定上报坐标、支持多点触控、能进低功耗。它适合正在做嵌入式 Linux 产品、需要把触摸屏从“能亮”调到“能点”的驱动工程师,也适合刚接手触摸子系统、想搞清楚 input 子系统和总线驱动怎么配合的新手。下面按实际调试顺序,从硬件接口选型一路写到中断线程化和固件升级的边界。
2. 先搞清 Synaptics 触摸芯片的 I2C 与 SPI 接口差异
2.1 为什么同一颗芯片会有两种总线版本
Synaptics 的触摸控制器通常以裸片或模组形式出货,同一代触摸 IC 往往同时提供 I2C 和 SPI 两种主机接口。I2C 版本引脚少、走线简单,适合屏幕排线空间紧张、速率要求不高的场景;SPI 版本能跑到几十 MHz,适合高报点率、大尺寸触摸屏,或者需要快速读取原始电容数据做压力感应的方案。选型时先看硬件原理图:如果触摸排线上只有 SCL/SDA 和 INT/RST,那就是 I2C;如果有 CS/CLK/MOSI/MISO,那就是 SPI。不要试图在驱动里“兼容”两种总线,Linux 的 bus 模型不允许一个设备同时挂两条总线,常见做法是写两个独立的 bus driver,共用一套核心解析逻辑。
2.2 设备树里怎么描述总线连接和中断
以 I2C 为例,设备树节点要挂在对应的 i2c 控制器下,SPI 则挂在 spi 控制器下。关键属性包括 reg、interrupt-parent、interrupts、reset-gpios、vdd-supply 等。下面是一个 I2C 触摸节点的最小示例,SPI 版本只需把i2c1换成spi0,并加上spi-max-frequency。
&i2c1 { status = "okay"; clock-frequency = <400000>; touchscreen@20 { compatible = "synaptics,touch-i2c"; reg = <0x20>; interrupt-parent = <&gpio1>; interrupts = <12 IRQ_TYPE_EDGE_FALLING>; reset-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>; vdd-supply = <&vdd_3v3_touch>; pinctrl-names = "default"; pinctrl-0 = <&touch_int_pin &touch_rst_pin>; }; };逻辑说明:reg是 I2C 从地址,常见 0x20 或 0x2C,具体看模组规格书;interrupts里的触发方式必须和硬件中断引脚的实际电平匹配,很多“触摸无反应”其实是这里写成了IRQ_TYPE_LEVEL_LOW而芯片输出的是下降沿脉冲。reset-gpios的极性也要和复位电路一致,低电平复位就写GPIO_ACTIVE_LOW。vdd-supply用于驱动里做电源管理,没有的话可以省略,但低功耗场景建议补上。
2.3 总线速率与上拉电阻的实测边界
I2C 标准模式 100 kHz、快速模式 400 kHz、快速模式+ 1 MHz,Synaptics 触摸芯片一般支持到 400 kHz,部分型号支持 1 MHz。SPI 则常见 10 MHz 到 30 MHz。实际调试时,如果 I2C 波形上升沿太缓,先查上拉电阻:3.3 V 系统常用 2.2 kΩ 到 4.7 kΩ,排线长、负载大时用 2.2 kΩ。SPI 则要注意 CS 建立时间和时钟相位,很多模组要求 CPOL=0、CPHA=0,但也有例外,翻车点在于直接抄了别的屏的参数。我一般会先用示波器看 INT 引脚有没有脉冲,有脉冲说明芯片在工作,问题在总线或驱动;没脉冲就先查供电和复位时序。
3. 写一个能跑通的最小 I2C 触摸驱动
3.1 驱动骨架:probe、中断注册、input 设备分配
Linux 下触摸驱动属于 input 子系统,核心是在 probe 里分配 input_dev、设置能力位、注册中断处理函数,然后在中断里读坐标并上报。下面是一个可编译的 I2C 驱动骨架,省略了具体寄存器读写,重点看流程。
#include <linux/module.h> #include <linux/i2c.h> #include <linux/input.h> #include <linux/interrupt.h> #include <linux/gpio/consumer.h> #include <linux/delay.h> struct syn_touch { struct i2c_client *client; struct input_dev *input; struct gpio_desc *reset_gpio; int irq; }; static irqreturn_t syn_touch_irq(int irq, void *dev_id) { struct syn_touch *ts = dev_id; u8 buf[6]; int ret; /* 读取触摸状态和坐标,具体寄存器地址按芯片手册 */ ret = i2c_smbus_read_i2c_block_data(ts->client, 0x02, sizeof(buf), buf); if (ret < 0) return IRQ_HANDLED; /* 解析出 x、y、压力,上报 input 事件 */ input_report_abs(ts->input, ABS_X, (buf[0] << 8) | buf[1]); input_report_abs(ts->input, ABS_Y, (buf[2] << 8) | buf[3]); input_report_abs(ts->input, ABS_PRESSURE, buf[4]); input_report_key(ts->input, BTN_TOUCH, buf[5] & 0x01); input_sync(ts->input); return IRQ_HANDLED; } static int syn_touch_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct syn_touch *ts; int ret; ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL); if (!ts) return -ENOMEM; ts->client = client; i2c_set_clientdata(client, ts); ts->reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(ts->reset_gpio)) return PTR_ERR(ts->reset_gpio); /* 复位时序:拉低至少 10ms,再拉高,等待固件启动 */ if (ts->reset_gpio) { gpiod_set_value_cansleep(ts->reset_gpio, 0); msleep(20); gpiod_set_value_cansleep(ts->reset_gpio, 1); msleep(100); } ts->input = devm_input_allocate_device(&client->dev); if (!ts->input) return -ENOMEM; ts->input->name = "synaptics-touch"; ts->input->id.bustype = BUS_I2C; input_set_abs_params(ts->input, ABS_X, 0, 4095, 0, 0); input_set_abs_params(ts->input, ABS_Y, 0, 4095, 0, 0); input_set_abs_params(ts->input, ABS_PRESSURE, 0, 255, 0, 0); input_set_capability(ts->input, EV_KEY, BTN_TOUCH); ret = input_register_device(ts->input); if (ret) return ret; ts->irq = client->irq; ret = devm_request_threaded_irq(&client->dev, ts->irq, NULL, syn_touch_irq, IRQF_ONESHOT | IRQF_TRIGGER_FALLING, "syn_touch", ts); if (ret) return ret; return 0; } static const struct i2c_device_id syn_touch_id[] = { { "syn_touch", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, syn_touch_id); static const struct of_device_id syn_touch_of_match[] = { { .compatible = "synaptics,touch-i2c" }, { } }; MODULE_DEVICE_TABLE(of, syn_touch_of_match); static struct i2c_driver syn_touch_driver = { .driver = { .name = "syn_touch", .of_match_table = syn_touch_of_match, }, .probe = syn_touch_probe, .id_table = syn_touch_id, }; module_i2c_driver(syn_touch_driver); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Synaptics touch I2C driver");逻辑说明:devm_request_threaded_irq把中断下半部放到线程里,因为 I2C 读操作可能睡眠,不能在硬中断里直接调用i2c_smbus_read_i2c_block_data。IRQF_ONESHOT保证线程处理完之前中断线不会被重复触发。input_set_abs_params里的最大值 4095 是 12 位 ADC 的常见量程,实际按模组分辨率改。input_sync必须每次上报后调用,否则上层收不到完整事件。
3.2 中断线程化与 I2C 读写的时序配合
上面代码用了IRQF_ONESHOT | IRQF_TRIGGER_FALLING,但设备树里已经写了IRQ_TYPE_EDGE_FALLING,两者取其一即可,重复设置不会报错但容易混淆。线程化中断的好处是可以在处理函数里安全地做 I2C 传输,代价是延迟比硬中断高几十微秒,对触摸报点率 60 Hz 到 120 Hz 来说完全够用。如果发现丢点,先看 I2C 速率是不是太低,400 kHz 下读 6 字节大约 150 微秒,加上线程调度,整体在 1 ms 以内。另一个坑是中断引脚在触摸按下期间可能一直保持低电平,如果配置成边沿触发,按住不动时不会重复上报,需要芯片支持“中断后自动清除”或者驱动里主动读状态寄存器清除中断源。
3.3 用 evtest 和 i2c-tools 验证数据通路
驱动加载后,先确认 I2C 设备被识别:
i2cdetect -y 1如果 0x20 位置显示UU,说明驱动已经绑定;显示20说明设备在但驱动没加载;显示--说明总线或供电有问题。然后运行evtest,选择对应的 input 设备,手指触摸时应该看到ABS_X、ABS_Y、BTN_TOUCH事件。如果 evtest 没输出,先看dmesg | grep syn_touch有没有 probe 成功,再看/proc/interrupts里中断计数有没有增长。中断计数不增长,说明硬件中断没到 CPU,查 INT 引脚电压和触发方式;计数增长但没事件,说明 I2C 读失败,查从地址和寄存器偏移。
4. SPI 版本驱动移植时最容易翻车的四个点
4.1 SPI 模式与时钟极性的匹配
SPI 触摸芯片对 CPOL/CPHA 很敏感,常见模式是 mode 0,但有些模组要求 mode 3。设备树里用spi-cpol和spi-cpha指定,或者用spi-cs-high控制片选极性。翻车现象是:probe 成功,但读出来的坐标全是 0 或 0xFF。先用逻辑分析仪抓 CLK 和 MOSI,确认第一个时钟沿采样的是不是正确位。如果手头没有分析仪,可以写一个简单的spi_write_then_read测试,读芯片 ID 寄存器,ID 不对就调模式。
4.2 DMA 与 PIO 传输的选择
SPI 控制器通常支持 DMA,但触摸数据量小、频率高,用 DMA 反而增加延迟。我一般先用 PIO 模式跑通,确认功能正常后再看 CPU 占用。如果 SPI 控制器驱动默认走 DMA,可以在设备树里加spi-rx-bus-width = <1>和spi-tx-bus-width = <1>强制单线模式,或者用dmas属性禁用。注意:不是所有 SoC 的 SPI 驱动都支持运行时切换,有些只能在 probe 时决定。
4.3 片选信号的手动控制
有些 Synaptics SPI 模组要求片选在两次传输之间保持低电平,或者需要手动拉高拉低来唤醒。Linux SPI 框架默认每次传输后释放 CS,如果模组不兼容,可以用spi->cs_gpiod手动控制,或者在传输前调用spi_set_cs。更常见的做法是在设备树里把 CS 配成 GPIO,驱动里用gpiod_set_value控制。翻车点是:手动控制 CS 后忘了在传输前拉低,导致数据错位。
4.4 中断与 SPI 传输的并发保护
SPI 传输本身是同步的,但中断线程和电源管理回调可能并发访问 SPI 总线。需要在驱动里加互斥锁,比如mutex_lock(&ts->bus_lock)包住所有 SPI 读写。如果用了 runtime PM,还要在runtime_resume里重新初始化 SPI 模式,因为有些控制器在 suspend 后会丢失配置。
5. 避坑与常见问题排查
5.1 触摸坐标跳变或漂移
现象:手指不动,坐标在几十个像素范围内跳动。原因:I2C 读到的原始数据没有做滤波,或者中断触发太频繁导致重复上报。解决:在驱动里加简单的滑动平均,或者依赖芯片内部的滤波配置寄存器。先确认芯片手册里有没有“滤波等级”寄存器,有的话写一个合适的值,比在驱动里做软件滤波更省 CPU。
5.2 probe 成功但 evtest 无事件
现象:dmesg显示 probe 成功,/dev/input/eventX存在,但触摸无反应。原因:中断没注册成功,或者 input 设备没有input_sync。先看/proc/interrupts里中断号计数,不增长就查设备树interrupts属性;增长但无事件,检查input_report_abs后有没有input_sync。另一个常见原因是BTN_TOUCH没有上报,上层认为没有按下。
5.3 低功耗唤醒后触摸失效
现象:系统进入 suspend,唤醒后触摸无反应。原因:suspend 时复位了触摸芯片,resume 后没有重新初始化。解决:在suspend回调里不要拉低复位,或者resume里重新执行复位时序和寄存器配置。如果用了dev_pm_ops,确保resume里调用syn_touch_hw_init。
5.4 I2C 传输超时导致系统卡顿
现象:触摸时系统偶尔卡住几百毫秒。原因:I2C 总线被其他设备占用,或者触摸芯片在忙时拉低时钟。解决:检查同一总线上的其他设备有没有长传输,把触摸芯片单独挂一条 I2C 总线。如果硬件改不了,降低触摸中断频率,或者在驱动里加超时重试而不是无限等待。
5.5 固件升级后设备不识别
现象:通过驱动升级触摸固件后,重启找不到设备。原因:固件升级过程中断电或复位时序不对,芯片进入 bootloader 模式。解决:升级流程里加校验和回滚,升级完成后硬复位一次。如果芯片支持,在设备树里保留一个firmware-name属性,驱动 probe 时检查固件版本,不匹配就自动升级。
6. 进阶:用 debugfs 和 ftrace 定位触摸延迟
触摸延迟的锅不一定在驱动,可能在 input 子系统排队或者上层应用。我一般先在驱动里加ktime_get打时间戳,记录中断触发到input_sync的耗时。如果这段超过 2 ms,查 I2C 速率和线程调度;如果这段正常,用 ftrace 看input_event到应用read的延迟。下面是一个简单的 debugfs 节点示例,用来导出最近 100 次触摸的耗时。
#include <linux/debugfs.h> #include <linux/ktime.h> struct syn_touch_stats { ktime_t last_irq; u32 count; u64 total_ns; }; static struct dentry *syn_debugfs_dir; static int syn_stats_show(struct seq_file *s, void *data) { struct syn_touch *ts = s->private; seq_printf(s, "count: %u\n", ts->stats.count); if (ts->stats.count) seq_printf(s, "avg_ns: %llu\n", div_u64(ts->stats.total_ns, ts->stats.count)); return 0; }逻辑说明:在中断处理函数开头记录last_irq = ktime_get(),在input_sync后累加ktime_ms_delta。debugfs 文件用single_open注册,用户态cat即可查看。参数上,count到 100 后清零重新统计,避免长时间运行后数值溢出。这个节点在产线调试时特别有用,能快速判断是驱动慢还是应用慢。
另一个技巧是用trace_printk配合 ftrace 的function_graph,但trace_printk有性能开销,只在调试时开。正式固件里用 debugfs 统计就够了。我自己的习惯是:每接一块新触摸屏,先跑evtest看原始事件,再用 debugfs 看驱动耗时,最后用getevent看应用层时间戳。三层对不上,问题范围就缩小到某一层。希望帮到你。
本文还有配套的精品资源,点击获取