屏幕点亮了,Linux起来了,QT界面也显示出来了,手指点上去,图标纹丝不动。这个场景我在不少FPGA转嵌入式的朋友那里见过太多回,几乎每个人第一时间都怀疑是内核驱动的问题,然后陷入改驱动、重新编译、烧写、再试的循环里,一折腾就是好几天。实际上,7寸触摸屏驱动这件事,90%的问题根本不在驱动代码本身,而是整条数据链路没有通。从手指按到屏幕上的那一刻起,触摸IC要完成电容变化采样、坐标计算、通过I2C总线送数、SoC的中断控制器上报、内核驱动读取坐标、input子系统分发给应用层,再到QT或Weston做出响应,任何一环断了,现象都一样:没反应。FPGA工程师的优势恰恰是能一路测上来,但如果不懂这条链路,优势也发挥不出来。这篇文章我就以黑金ZYNQ系列开发板配7寸触摸屏为背景,把从硬件连接到内核驱动、再到设备树和实际调试的完整过程讲透。
1. 先搞清楚触摸屏驱动的完整链路,再谈写代码
很多人把"触摸屏驱动"理解成"写几个C文件编进内核",这个理解太窄了。触摸屏驱动在Linux里是一个典型的输入设备驱动,但它同时挂在I2C子系统的底下。也就是说,它既是I2C客户端驱动,又是input设备驱动,一脚踩两个子系统。这种双重身份决定了它的工作模式:probe时通过I2C总线跟触摸IC对话,运行时就通过中断引脚感知"有人摸了",然后去读坐标,最后通过input子系统把坐标事件送出去。
在开始碰代码之前,你必须把整条链路画出来,否则后面遇到的问题根本无从下手。
1.1 7寸屏上那颗触摸IC,决定了后面所有工作量
市面上7寸RGB电容触摸屏模组,触摸部分基本都是一个独立的触摸IC贴在屏幕玻璃上。主流的控制器就两大家族:汇顶的GT911/GT9271,和敦泰的FT5206/FT5316。很多开发板把这两类芯片都兼容,因为模组供应商有时候会根据采购批次换料,排线接口一样,但寄存器地址和初始化流程完全不一样。
| 参数 | GT911 | FT5316/FT5206 |
|---|---|---|
| I2C地址 | 0x5D(部分批次为0x14) | 0x38 |
| 最多触摸点数 | 5点 | 2到5点 |
| 坐标寄存器 | 0x8150起始,每点8字节 | 0x02起始,每点4字节 |
| 中断行为 | 有触摸时INT拉低,读坐标后恢复 | 有触摸时INT拉低,读坐标后恢复 |
| 内核已有驱动 | goodix.c | edt-ft5x06.c |
这个选型对后续工作影响很大。拿到一块屏,第一步就是确认上面的触摸IC型号,最简单的办法是看丝印,或者直接查开发板原理图和BSP里自带的驱动。黑金的大多数7寸屏资料里,GT911和FT5316两种情况都覆盖了,但如果你是自行采购的模组,一定要先确认清楚。用错驱动、compatible字符串对不上,设备树写得再漂亮也白搭。
1.2 坐标从手指传到应用层,中间要经过几道关卡
完整的链路是这样的:
手指触摸 -> 触摸屏感应层电容变化 -> 触摸IC内部完成坐标计算 -> INT引脚拉低通知SoC -> 内核驱动在中断处理中通过I2C读取坐标寄存器 -> 调用input子系统API上报ABS_MT_POSITION_X/Y等事件 -> 内核把事件写入/dev/input/eventX ->应用层(QT、GTK、Weston)通过evdev接口读取 -> UI做出响应。
应用层根本不关心你用的是GT还是FT,它只从event设备节点读标准事件。这里有个常见误区:很多FPGA工程师拿到触摸屏第一反应是写一个驱动把坐标printk出来,结果即使打印出了坐标,QT界面还是不响应,于是又开始怀疑QT配置。其实原因很简单,图形框架走的是evdev,只认input子系统注册的标准事件,不会去读你的printk。所以驱动里必须做input设备注册和事件上报,这一步绕不过去。
1.3 硬件连接检查单:SCL、SDA、INT、RST各就各位
触摸屏的软排线一般引出6根信号:SCL、SDA、INT、RST、VCC、GND。翻车点几乎全在这几根线上。
- VCC:有些模组是3.3V供电,有些是2.8V,直接接3.3V有时候会出现读ID正常但坐标异常的问题,建议查一下模组丝印或规格书。
- RST:必须由SoC的GPIO可控,不能直接硬接到VCC。GT911对复位时序有要求,后面我会详细讲。
- INT:不能随便接一个普通IO就完事,必须接到能触发中断的引脚,而且设备树里中断触发方式也要写对。
- SCL/SDA:I2C是开漏结构,必须有上拉电阻。很多扩展板只把SDA/SCL引脚引出来,板上没有上拉电阻,这是i2cdetect扫不到设备的第一大原因。
排线方向也别小看。触摸排线的金手指方向是朝屏幕玻璃面插入的,插反了一般不会烧,但会导致接触不良,表现为时通时断,特别难排查。我见过不少"驱动加载失败"的案例,最后发现就是排线松了或插反了。
2. FPGA/SoC侧要先解决的三个基础问题:复位时序、I2C通路、中断信号
很多教程直接跳到内核驱动和设备树,但实际上Linux起来之前,硬件侧有三件事必须提前确认好。FPGA/SoC侧不准备好,驱动写得再完美都是空中楼阁。
2.1 GT911的复位时序,说多了都是泪
GT911这颗IC对复位时序是有点"矫情"的。它的要求大致是:上电后等待一段时间,拉低RST保持至少1ms,再拉高,释放后等待几十毫秒,触摸IC才完成初始化,I2C才会正常应答。如果你只是简单地把RST拉高不管,或者复位信号跳变太快,触摸IC可能一直处于异常状态,表现为I2C扫描不到设备,或者能扫描到但读寄存器超时。
FT5316相对宽容一些,上电后给个低脉冲再拉高基本就行。但如果你用的是GT911,复位时序不规范,后面会吃大亏。
如果用的是内核自带的goodix.c驱动,设备树里把reset-gpios和irq-gpios都配好,驱动probe的时候自己会控制复位时序,不需要你在应用层额外操作。但问题在于:有些硬件设计为了省事,把RST引脚直接通过电阻拉到高电平,靠RC上电延时实现复位。这种电路如果RC参数不对,时序就可能不满足要求。我建议在Linux跑起来之前,先用一个最简单的裸机I2C读ID程序,或者直接在uboot里I2C探测一下,确认触摸IC有应答,再往下走。
2.2 I2C总线怎么选:MIO还是EMIO,差别很大
ZYNQ的PS自带两个I2C控制器:I2C0和I2C1。这两个控制器可以配置到两组引脚上,一组是固定的MIO引脚,另一组是EMIO引脚,通过PL侧的可配置IO引出。
| 配置方式 | 引脚位置 | 电平范围 | 优点 | 缺点 |
|---|---|---|---|---|
| MIO | PS固定引脚 | 由MIO Bank电压决定,通常3.3V或1.8V | 不需要PL参与,启动即用 | 引脚固定,扩展不方便 |
| EMIO | PL侧引脚 | 由PL Bank电压决定,常见3.3V | 引脚灵活,可约束到扩展口 | 需要PL逻辑配置好,涉及管脚约束 |
黑金开发板上的7寸屏扩展接口,触摸的I2C信号通常接到PL侧扩展引脚上,所以你需要在Vivado里把PS的I2C控制器通过EMIO引出来,分配好管脚位置。这个过程不复杂:在Block Design里把I2C1(或者I2C0)的接口使能,选择EMIO,然后会生成两个顶层端口,在XDC里约束到具体引脚并设置IO标准即可。关键是别漏了管脚约束,否则PL侧引脚悬空,I2C控制器自然不通。
另外提醒一下:I2C时钟频率不要上来就配400kHz,触摸屏FPC排线长了之后抗干扰能力会下降,高速率下容易出现ACK异常。先从100kHz跑通,稳定后再考虑提速。
2.3 用FPGA的ILA工具去抓I2C波形,这一步比任何调试都管用
FPGA工程师做这事的最大优势,就是可以把ILA(集成逻辑分析仪)直接挂到EMIO引出的SCL、SDA、INT信号上,在硬件层面看到真实的波形。这个手段Linux开发者通常没有,但我们可以。
具体做法是在Vivado里添加ILA IP,把I2C的SCL、SDA,还有触摸INT引脚都接进去,采样深度设置大一点。然后运行Linux,执行i2cdetect或者让驱动probe,回头查看ILA波形。如果能看到SCL/SDA上有正常的地址字节和ACK位,说明物理链路没问题;如果根本没波形,或者波形畸形、幅度不对,问题就在管脚约束、上拉电阻或者排线连接上。这一步能帮你把物理层和软件层彻底隔离,排查效率翻倍。
2.4 纯FPGA方案怎么玩:AXI IIC或者自己写I2C主机
如果你的平台不是ZYNQ这种带ARM硬核的SoC,而是纯FPGA想上Linux,最常见的路线是MicroBlaze软核。这种情况下,Xilinx提供了AXI IIC IP核,你可以直接在Block Design里拖出来用,它实现了I2C主机功能,Linux内核里有对应的i2c-xiic驱动,设备树里按常规I2C控制器节点写就行。
当然,也可以完全用Verilog/VHDL自己写一个I2C主机,挂在AXI总线上。但除非你是想练手或者对面积/性能有特殊要求,否则没必要重复造轮子。AXI IIC这个IP已经非常成熟,配合Linux标准驱动,稳定性是有保障的。MicroBlaze跑Linux本身就已经够折腾了,I2C这种基础外设就别再给自己加戏了。
3. Linux内核驱动:从probe到坐标上报,到底要做什么
先说明一个重要观点:如果你用的触摸IC是GT911或者FT5x06这种主流型号,内核里已经有现成驱动,你根本不需要自己写。你需要做的是把设备树配好、把内核配置项打开,然后验证功能。自己写驱动的场景,一般是拿到了冷门触摸IC,或者你想彻底理解驱动的工作原理。但无论哪种情况,理解驱动内部的工作流程都对调试有巨大帮助。
3.1 内核自带的goodix.c是怎么工作的
以goodix.c为例,它的工作流程大致是:
- 设备树匹配:I2C子系统的核心层扫描总线上的设备,如果发现设备树的compatible字符串和驱动里of_match_table匹配,就调用驱动的probe函数。
- 复位触摸IC:驱动读取设备树里的reset-gpios,按照GT911要求的时序拉低再拉高,完成一次硬件复位。
- 检测设备:通过I2C读取触摸IC的ID寄存器,确认设备在线且型号正确。GT911的检测过程会尝试0x5D和0x14两个地址,所以你会发现即使你的屏是0x14地址,驱动也能识别。
- 初始化input设备:设置设备支持的事件类型、坐标轴范围、多点触摸协议等。
- 请求中断:注册中断处理函数,中断触发方式通常是下降沿。
这里值得多说一句的是,GT911在复位释放的瞬间,INT引脚的电平状态决定了I2C地址。INT拉高对应一个地址,拉低对应另一个地址。这个设计让很多人在一开始栽跟头,因为手动写裸机I2C程序时,如果你在复位期间没处理好INT状态,读到的地址就跟预期不一样。而内核的goodix.c通过自动探测两个地址规避了这个问题。
3.2 edt-ft5x06.c的基本逻辑
FT5316对应的内核驱动是edt-ft5x06.c,它的probe流程跟goodix类似,但寄存器操作更简单。FT5316的坐标读取是从0x02寄存器开始,每个触摸点占4个字节,数据格式是高字节在前还是低字节在前,寄存器手册里写得很清楚。这个驱动早期也支持通过设备树配置是否交换X/Y轴、是否翻转坐标,属性名大概是touchscreen-inverted-x之类的,后面调试章节我会具体说。
3.3 如果必须自己写,抓住四个核心点就够了
自己写触摸驱动的场景不多,但理解核心点能让你在调试现成驱动时更有底气。四个核心点分别是:i2c_driver结构体、坐标读取、input子系统上报、中断处理。
先看一个最小骨架:
#include <linux/i2c.h> #include <linux/input.h> #include <linux/interrupt.h> #include <linux/module.h> #include <linux/of_device.h> struct ts_priv { struct i2c_client *client; struct input_dev *input; }; static int ts_read_coord(struct ts_priv *ts, int *x, int *y, bool *touch) { u8 buf[8]; struct i2c_msg msg[2]; int ret; /* 以GT911为例:先写寄存器地址0x8150,再连续读8字节 */ /* 这里省略具体I2C传输实现,重点是数据格式 */ ret = i2c_smbus_read_i2c_block_data(ts->client, 0x81, 8, buf); if (ret < 0) return ret; *touch = (buf[0] & 0x80) ? true : false; if (*touch) { *x = buf[1] | (buf[2] << 8); *y = buf[3] | (buf[4] << 8); } return 0; } static irqreturn_t ts_irq_handler(int irq, void *dev_id) { struct ts_priv *ts = dev_id; int x, y; bool touch; int ret; ret = ts_read_coord(ts, &x, &y, &touch); if (ret < 0) return IRQ_HANDLED; if (touch) { input_report_key(ts->input, BTN_TOUCH, 1); input_report_abs(ts->input, ABS_X, x); input_report_abs(ts->input, ABS_Y, y); } else { input_report_key(ts->input, BTN_TOUCH, 0); } input_sync(ts->input); return IRQ_HANDLED; } static int ts_probe(struct i2c_client *client) { struct device *dev = &client->dev; struct ts_priv *ts; struct input_dev *input; int ret; ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL); if (!ts) return -ENOMEM; ts->client = client; input = devm_input_allocate_device(dev); if (!input) return -ENOMEM; input->name = "7inch-touchscreen"; input->id.bustype = BUS_I2C; __set_bit(EV_KEY, input->evbit); __set_bit(EV_ABS, input->evbit); __set_bit(BTN_TOUCH, input->keybit); input_set_abs_params(input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(input, ABS_Y, 0, 767, 0, 0); ts->input = input; ret = devm_request_threaded_irq(dev, client->irq, NULL, ts_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "7inch_ts", ts); if (ret) return ret; ret = input_register_device(input); if (ret) return ret; return 0; } static const struct of_device_id ts_of_match[] = { { .compatible = "myvendor,7inch-ts", }, { } }; MODULE_DEVICE_TABLE(of, ts_of_match); static struct i2c_driver ts_driver = { .probe = ts_probe, .id_table = ts_id_table, .driver = { .name = "7inch_ts", .of_match_table = ts_of_match, }, }; module_i2c_driver(ts_driver); MODULE_LICENSE("GPL");这个骨架省略了很多细节,比如GT911多点触摸需要按B协议上报,用input_mt_init_slots初始化槽位,然后input_mt_slot、input_mt_report_slot_state配合使用。但核心逻辑已经清楚:probe里注册input设备、请求中断,中断里读坐标、上报事件。理解了这四件事,你再去看内核自带的goodix.c和edt-ft5x06.c,会发现它们再怎么复杂,也是在这个框架里打转。
3.4 中断处理还是轮询,别在这个问题上犯懒
触摸屏驱动有两种工作模式:中断驱动和轮询。
中断驱动是正道。手指按下时触摸IC的INT引脚拉低,SoC收到中断,驱动去读坐标。这种方式实时性好、CPU占用低、触摸跟手。缺点是中断引脚必须配置正确,设备树里interrupts属性一旦写错,要么中断不触发,要么中断风暴。
轮询模式适合验证硬件,不适合做产品。如果你只是想先确认触摸IC能读到坐标,可以在内核线程里每10ms读一次,printk打印出来看看。但如果你把这个方案用到QT界面里,触摸滑动会明显丢点、卡顿,用户体验很差。
所以我的建议是:验证阶段可以轮询,正式方案必须中断。设备树里interrupt-parent和interrupts这两个属性就是为中断模式准备的,后面专门讲。
4. 设备树与内核配置:一个字节就能决定驱动能不能加载
硬件链路确认没问题,内核驱动也编进去了,接下来就是设备树。这一节是最容易翻车的地方,因为设备树写错不会报编译错误,只会静默地导致驱动无法加载或者中断行为异常,排查起来特别费劲。
4.1 设备树节点怎么写,GT911和FT5316各来一份
设备树的核心作用,是把硬件信息告诉内核:总线上挂了什么设备、地址是多少、中断引脚是哪个、复位引脚是哪个。
GT911的设备树节点通常这样写:
&i2c1 { status = "okay"; clock-frequency = <100000>; gt911@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&gpio0>; interrupts = <54 IRQ_TYPE_EDGE_FALLING>; irq-gpios = <&gpio0 54 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio0 55 GPIO_ACTIVE_HIGH>; }; };FT5316的设备树节点类似:
&i2c1 { status = "okay"; clock-frequency = <100000>; ft5316@38 { compatible = "edt,edt-ft5316", "edt,edt-ft5x06"; reg = <0x38>; interrupt-parent = <&gpio0>; interrupts = <54 IRQ_TYPE_EDGE_FALLING>; reset-gpios = <&gpio0 55 GPIO_ACTIVE_HIGH>; touchscreen-size-x = <1024>; touchscreen-size-y = <768>; }; };这里有几个关键点:
- reg必须是触摸IC实际应答的I2C地址,GT911可能是0x5D也可能是0x14,跟硬件复位时序有关。
- interrupt-parent和interrupts决定中断信号从哪个GPIO进内核。ZYNQ的EMIO GPIO在设备树里通常挂在gpio0节点下,中断号从54开始对应EMIO引脚。如果你对自己的GPIO编号没把握,去查Vivado生成的设备树文件(.dtsi),不要凭记忆写。
- reset-gpios、irq-gpios这些GPIO描述属性,goodix.c和edt-ft5x06.c驱动都会解析,分别用于复位和中断配置。GPIO编号同样要跟实际约束一致。
- IRQ_TYPE_EDGE_FALLING表示下降沿触发。大多数电容触摸IC在有触摸时INT从高拉低,所以下降沿是常见配置。如果你的硬件设计把INT反相了,触发方式也要相应调整,否则中断要么不触发,要么触发后一直在处理。
4.2 中断号和GPIO编号怎么确认,别靠猜
ZYNQ平台的EMIO中断号是有规律可循的,但规律很容易记错。最稳妥的办法是:在Vivado里做完管脚约束后,导出XSA文件,然后在Linux的BSP配套设备树文件里找gpio和interrupt相关的定义。黑金的BSP资料里一般都有现成的设备树参考,直接对照着改就行。
如果你是从零开始没有参考,可以先把设备树里中断号随便填一个,启动后用dmesg看kernel打印,驱动会报出实际的中断申请情况。或者用一个最简单的GPIO中断测试模块,逐个试出有效的中断号。这个过程虽然笨,但可靠。我在实际项目中,见过太多人因为interrupts写错一个数字,卡了整整一天。
4.3 内核配置选项:这两项必须编进内核
触摸驱动有两种编法:编成模块(.ko)或者直接编进内核。对于开发板调试,编进内核更省事,不用考虑模块加载顺序和文件系统路径问题。
配置路径在:
Device Drivers -> Input device support -> Touchscreens需要选中的项:
- Goodix touchscreen(对应CONFIG_TOUCHSCREEN_GOODIX)
- EDT EETI touchscreen(对应CONFIG_TOUCHSCREEN_EDT_FT5X06)
如果你用的是其它触摸IC,就在这个菜单下找对应的驱动。理论上讲,触摸屏驱动都是这个目录下的子项。有些老内核可能没有GT911驱动,那就需要从新内核移植,或者用驱动源码自己编,这个另说。
4.4 编译烧写后,怎么确认设备树和驱动真的在干活
Linux起来之后,用几个简单命令确认状态:
# 确认I2C总线上能看到触摸设备 i2cdetect -y 1 # 查看内核日志中触摸驱动的打印 dmesg | grep -i goodix dmesg | grep -i edt # 查看input设备列表 cat /proc/bus/input/devices # 查看中断是否注册成功 cat /proc/interrupts | grep 7inch如果dmesg里能看到"goodix_ts_probe"或者"I2C Communication with touch device failed"之类的信息,说明驱动已经加载。如果/proc/bus/input/devices里有触摸设备,并且事件名为"7inch-touchscreen"或类似名字,说明input设备注册成功。后面就是用evtest测试坐标了。
这一步最容易出现的问题是:设备树改了、烧写了、也重启了,但dmesg里没有任何触摸相关的输出。这种情况九成是compatible字符串不匹配,或者I2C地址不对导致驱动probe被跳过。优先检查这两项。
5. 实测调试:坐标翻转、漂移、偶发死机的完整排查链路
驱动和设备树都配置好了,触摸屏能动了,但别高兴太早。实测阶段最少会遇到三类问题:坐标方向不对、触摸点漂移抖动、偶发失去响应。下面把排查思路完整过一遍。
5.1 第一步永远是i2cdetect,先看设备在不在
不管现象是什么,第一步永远是确认I2C设备还在总线上。执行:
i2cdetect -y 1这里的1对应I2C控制器序号,要根据你的设备树实际配置确认。
如果能扫描到设备地址(比如0x5D或0x38),说明硬件链路正常,问题在驱动或设备树。如果扫描不到,按这个顺序排查:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 扫描不到任何设备 | SDA/SCL无上拉电阻 | 检查扩展板原理图,补上拉 |
| 扫描不到任何设备 | 复位时序不对,触摸IC没起来 | 用ILA检查RST引脚波形 |
| 扫描地址是0xFF | SDA线被拉低或总线挂死 | 检查SDA是否有异常电平 |
| 时有时无 | 排线接触不良 | 重新插拔,检查金手指方向 |
| 扫描到但读寄存器失败 | I2C速率太高或干扰 | 降速到100kHz再试 |
如果扫描正常但驱动没加载,问题基本锁定在设备树。重点检查compatible字符串、reg地址、以及status属性是否为okay。
5.2 坐标翻转和镜像,设备树属性一行搞定
触摸屏装到设备上,X轴Y轴方向经常跟屏幕显示方向不一致。有的是左右镜像,有的是上下颠倒,有的是X/Y交换。解决办法有两个。
如果是内核自带的edt-ft5x06驱动,设备树里有现成属性:
touchscreen-inverted-x; touchscreen-inverted-y; touchscreen-swapped-x-y;goodix驱动同样支持这些属性。加上之后重新编译设备树,重启即可生效。这个方法比在驱动代码里手动改坐标要干净得多,也更容易维护。
注意一点:坐标翻转不只是正负号问题。如果你的屏默认X范围是0到1023,翻转后应该变成1023减去原始值再上报。内核驱动的属性已经处理好了这一步,不需要你手动做减法。如果用的GT911并且想做屏幕旋转360度,可以配置驱动里的axis映射,或者直接改设备树加touchscreen-swapped-x-y。
5.3 触摸点漂移和乱跳,先查供电和地,再动软件
触摸点十几个像素范围内轻微抖动是正常现象,但如果触摸点乱跳、无操作时也上报事件,那就是硬件问题为主了。
优先检查三件事:
- 供电是否稳定。触摸IC的VCC如果纹波大,采样结果会异常。用示波器看VCC引脚的纹波,必要时加一个10uF电容和0.1uF电容。
- 接地是否可靠。触摸屏排线的GND和开发板的GND如果存在较大阻抗,电磁干扰会让触摸IC读出假坐标。这也是为什么触摸排线要选用屏蔽线的原因。
- 排线是否完全插入。FPC排线插不到位,会导致部分信号线虚接,表现就是触摸时断时续、坐标乱跳。
如果硬件检查完没问题,再去软件层做滤波。比如在驱动里记录上次坐标,如果新坐标和上次坐标的跳变超过一定阈值(比如200像素),丢弃这次数据。这个方法简单有效,但要注意不能过滤掉正常快速滑动。更好的做法是检查触摸IC的寄存器配置,GT911内部有几个寄存器可以调节灵敏度,适当降低灵敏度可以缓解抖动。
5.4 触摸用着用着就不动了,大概率是中断或I2C总线问题
偶发死机的现象一般是:触摸屏刚上电好用,用几分钟后完全没反应,但屏幕显示正常。这个时候先别重启,按下面顺序排查。
先看中断状态:
cat /proc/interrupts | grep 7inch记录中断计数,然后用手指反复触摸,如果计数不动,说明中断没有触发。这时候有两类原因:一是触摸IC挂死了,需要复位;二是中断配置本身有问题。
再查I2C总线状态:
i2cdetect -y 1如果扫描不到设备了,说明I2C总线上的触摸IC失联。这种失联到底是IC内部跑飞还是I2C总线被拉死,可以用ILA抓波形判断。如果SDA被从设备拉低不放,就是I2C总线挂死,常见原因是从设备进入异常状态,需要拉低RST复位。
还有一种很隐蔽的原因:中断处理函数里做了可能导致睡眠的操作。Linux中断上下文不能调用msleep、i2c_transfer等可能睡眠的函数,但如果用了request_threaded_irq并加了IRQF_ONESHOT标志,中断处理是在内核线程里执行的,就可以放心调用I2C传输函数。这个细节在自写驱动时特别重要,搞错了就会出现"一触摸就死机"的怪现象。
5.5 FPGA工程师独有的终极调试手段:ILA抓全波形
前面已经提过ILA,这里再展开说一下。当问题复杂到软件层面已经看不到头绪时,把ILA的采样信号设置为:触摸屏I2C的SCL、SDA、INT、RST。
然后触发条件可以设置为INT下降沿,这样每次触摸都会捕获从INT拉低到驱动读坐标完成的完整I2C波形。通过波形你能看到:
- 中断之后,SoC是否真的发起了I2C读操作
- 读操作的目标地址是否正确
- 从设备是否回了ACK
- 读出来的数据是否有规律
如果一切波形正常但应用层还是没反应,问题就在驱动和input子系统那一段。如果波形不正常,问题就在硬件连接或触摸IC本身。这种"硬件波形+软件日志"双重定位的方式,能把排查时间从几天压缩到几个小时,强烈建议每个做FPGA+Linux的人都掌握。
5.6 用evtest做最终验证,别再用printk看坐标
驱动跑起来之后,最终验证工具推荐evtest。在开发板文件系统里执行:
evtest /dev/input/event0然后手指在屏幕上移动,能看到类似这样的输出:
Event: time 1234.567890, type 3 (EV_ABS), code 0 (ABS_X), value 320 Event: time 1234.567890, type 3 (EV_ABS), code 3 (ABS_X), value 240 Event: time 1234.567890, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1 Event: time 1234.567890, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0看到EV_ABS和BTN_TOUCH事件持续输出,并且数值随手指移动变化,说明从硬件到驱动到input子系统的链路已经全部打通。这个时候再启动QT程序或者其他GUI应用,触摸就能直接用了。如果到这里还不响应,那问题就在应用层的触摸屏配置上,比如QT的QWS_MOUSE_PROTO或环境变量设置,那就属于另一篇文章的内容了。
最后分享一点个人经验
这套东西做下来,我个人最大的体会是:触摸屏驱动在难度上并不高,但它是一个典型的"链路问题",跨了硬件、FPGA逻辑、内核驱动、设备树、应用层五个层面。FPGA工程师做这件事有天然优势,因为我们可以用ILA抓波形,这一点比纯软件开发者强太多。但反过来,如果一上来就埋头看驱动源码,不看波形、不扫描I2C、不确认中断,就会陷入盲目改代码的泥潭。
我建议任何碰到触摸问题的朋友,严格按这个顺序排查:先查排线和上拉,用万用表量出SCL/SDA/INT/复位电平是否正常;再用ILA或者示波器抓一遍上电复位时序和I2C波形;然后在Linux里i2cdetect确认设备在线;接着看dmesg确认驱动加载;最后evtest验证input事件。按照这个顺序走,90%的问题在第一轮就能定位。剩下10%的疑难杂症,往往是硬件偶发问题,需要反复抓波形对比,这时候保持耐心比掌握什么高深技巧更重要。