FPGA+Linux驱动7寸GT911触摸屏:从设备树到应用层全流程解析
2026/9/16 17:39:22 网站建设 项目流程

最近在折腾黑金的FPGA开发板,想把一块7寸触摸屏在Linux侧完整跑起来,查了一圈发现网上资料散得厉害,要么只讲RGB屏幕点亮的驱动,要么只给个i2c探测命令,真正从设备树到内核模块再到应用层触摸事件串起来的笔记少之又少。这篇就把我这次“FPGA+Linux+7寸触摸屏驱动”整个坎途梳理一遍,从方案选型到踩坑排错都写清楚,给后面做类似项目的朋友一个能直接抄作业的参考。

先交代一下平台背景:我用的是一块黑金的ZYNQ系列FPGA开发板,PL端是自己做的FPGA逻辑,PS端跑了Linux系统,触摸屏是一块7寸1024x600的RGB接口屏,触摸部分是电容式GT911方案。整个任务说白了就是三件事:让Linux内核识别触摸控制器、把触摸数据上报到输入子系统、让上层应用能读到干净的触摸事件。这套思路放在黑金、米联客、正点原子的FPGA SoC板上基本通用,如果你是做其他触摸IC或者用纯ARM Linux,逻辑也一样。

1. 项目整体设计与方案选型

1.1 为什么要在FPGA上跑Linux做触摸屏驱动

很多刚接触FPGA的朋友会有个误区,觉得FPGA就是拿来写Verilog、做时序逻辑的,触摸屏驱动这种“软件活”应该交给ARM或者单片机。但实际上在ZYNQ这类SoC平台上,PS(处理系统)和PL(可编程逻辑)是共存的,ARM核完全可以跑完整的Linux系统,PL端用FPGA做并行接口转换、图像预处理或者自定义外设。触摸屏驱动放在Linux侧来做,本身就是这套架构里最自然、性价比最高的分工。

我这次的具体硬件连接是:触摸屏通过FPC排线接到一块转接板上,转接板再引出I2C信号到ZYNQ的PS端I2C控制器,中断脚和复位脚分别接在PS端的GPIO上。屏幕的RGB显示信号走的是PL端,由FPGA产生像素时钟和数据,触摸这部分独立走PS端的总线。这样设计的好处是驱动任务全部落在Linux侧,可以用现成的内核框架和调试工具,不用在PL端自己造轮子去解析触摸协议。

1.2 触摸屏方案怎么选:电容、电阻还是红外

7寸触摸屏市面上能碰到的主流方案大概有三种:电容式(典型IC是GT911、FT5x06系列)、电阻式(典型IC是XPT2046、ADS7846)、红外式(典型方案是TOUCHKIT支持的红外触摸框)。我这次用的是电容式GT911,理由很直接:7寸RGB屏配电容触摸是工业平板和HMI最常见的组合,GT911支持十点触控,价格便宜,Linux内核里也有对应的驱动,省去不少移植功夫。

电阻屏的好处是便宜、抗干扰强,但只支持单点,而且需要校准,用户体验差一些。红外触摸框则适合大尺寸屏幕,7寸这个规格用红外方案的性价比不高,而且红外触摸对边框结构要求比较高,小尺寸很少见。如果你拿到手的屏幕是TOUCHKIT红外方案,那就要在驱动配置上走的是另一条路,这里先不展开,后面有时间单独写一篇。

核心经验是:定方案之前先确认触摸IC的型号,然后把Linux内核源码里drivers/input/touchscreen/目录翻一翻,看看有没有现成的驱动文件。有,就直接配置;没有,就要掂量一下自己改驱动的成本。我这次就是先在内核里查到了gt9xx.c,心里才踏实下来的。

2. 触摸屏驱动核心细节解析

2.1 触摸IC上电时序和I2C地址确认

GT911这个IC看起来简单,但上有两个坑特别容易踩:上电时序和I2C设备地址。GT911的地址可以通过引脚电平来配置,上拉电阻接高是0x5D,接低是0x14,也有芯片固定是0x28、0x5D之类的。我试过直接在系统里跑i2cdetect,结果发现明明连线正确却检测不到设备,后来查手册才发现复位时序不过关,芯片压根没起来。

GT911的复位时序大概是这样的:INT引脚先拉高,复位引脚拉低,保持至少10ms,然后复位拉高,再等至少50ms,最后INT引脚切换成输入模式或者根据实际电路配置电平。这套时序用GPIO模拟就行,但顺序不能错。如果只是把I2C两根线接上就急着探测,大概率是空手而归。我当时就是把复位时间从10ms加到100ms,又把INT的上下拉关系重新理了一遍,i2cdetect才终于扫出了地址。

关于I2C地址,实际工作中最常见的坑是驱动里写死了一个地址,但你的硬件上IC的地址选择脚接法不一样,导致驱动匹配失败。调试方式很简单,i2cdetect扫出来的地址和驱动源码里I2C_CLIENT_END之前的地址对比一下,不一致就改设备树或者驱动,别死磕。

2.2 设备树怎么描述触摸屏节点

设备树是Linux下硬件描述的核心,触摸屏挂在哪个I2C控制器上、中断脚是哪个GPIO、复位脚是哪个GPIO、坐标轴是否翻转,这些都要写清楚。我板子上的触摸IC挂在I2C0上,设备树节点大致长这样:

&i2c0 { status = "okay"; clock-frequency = <100000>; gt911: gt911@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&gpio0>; interrupts = <54 2>; irq-gpios = <&gpio0 54 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio0 53 GPIO_ACTIVE_HIGH>; touchscreen-inverted-x; touchscreen-inverted-y; touchscreen-swapped-x-y; }; };

这里几个属性值得多说两句。reg是I2C地址,必须和芯片实际配置对上;interrupt-parentinterrupts决定中断脚,有的板子上中断脚并不直接映射到SoC的中断控制器,而是接到PL端做转接,那这里的写法就完全不一样,得先确认硬件的中断通路;irq-gpiosreset-gpios是驱动里做复位时序用的,GPIO编号要根据原理图对着SoC的引脚表换算。

还有一点很隐蔽:GT911支持读取坐标,但坐标轴方向和屏幕的物理安装方向经常不一致,比如屏装上去是竖着的,但逻辑上希望是横着的坐标。设备树里的touchscreen-inverted-xtouchscreen-inverted-ytouchscreen-swapped-x-y就是为了解决这个问题的,但不同内核版本的驱动对这几个属性的解析可能不同,我就在某个版本上遇到过属性写进去没用,最后只能改驱动代码。

2.3 Input子系统上报链路

Linux触摸屏驱动的最终目标是向/dev/input/eventX上报触摸事件,这一层走的是内核的Input子系统。驱动里做的事情,概而言之就是在驱动加载时分配一个input设备,设置好事件类型和坐标范围,然后在中断处理函数或者轮询任务里读取触摸坐标,通过input_report_abs上报ABS_X、ABS_Y、ABS_MT_POSITION_X这类绝对坐标事件,再配合input_sync通知上层这一帧数据同步完成。

GT911支持多点触控,所以驱动里一般用的是Type B协议,需要上报ABS_MT_SLOT来区分触点ID。上层应用如果用Qt或者GTK,这些细节已经被封装好了,直接用QTouchEvent或者通过libinput读设备节点就行;但如果是自己写C程序读event节点,就得解析struct input_event里的type、code、value三要素。调试阶段最常用的工具是evtest,跑起来能看到每一个事件的实时打印,比任何文档都直观。

我自己的体会是,驱动只要能过evtest这一关,触摸屏驱动就算基本通了,剩下的坐标翻转、多点稳定性的微调都是时间问题。遇到evtest里事件完全不出来,优先查中断有没有触发;事件有但坐标乱跳,优先查电源地线和屏排线的屏蔽情况。

3. 实操过程与核心环节实现

3.1 内核配置与GT911驱动编译

Linux内核编译是整个流程里最见功底的环节。我用的是黑金光盘里配套的内核源码,版本是4.x,GT911的驱动在CONFIG_TOUCHSCREEN_GT9XX这个配置项下面。进入内核目录后执行:

make ARCH=arm menuconfig

然后在Device Drivers -> Input device support -> Touchscreens里找到Goodix GT9xx based touchscreen,直接编成模块(M)或者编译进内核(Y)都可以。我自己习惯编成模块,方便调试阶段单独替换,不折腾整个内核镜像。

如果menuconfig里找不到这个选项,大概率是内核源码里根本就没有这个驱动文件,要么换内核版本,要么把驱动源码手动放到drivers/input/touchscreen/目录下然后补Kconfig和Makefile。这一步对新手很不友好,但也不是没法解决,直接把驱动文件复制进去,在Kconfig里仿照其他触摸屏驱动加两行配置项,在Makefile里加一行obj-$(CONFIG_TOUCHSCREEN_GT9XX) += gt9xx.o,然后再回去make menuconfig就能看到了。

配置完成之后开始交叉编译,我的工作环境是Ubuntu虚拟机,交叉编译工具链用的arm-linux-gnueabihf-gcc。编模块的时候要注意内核源码的版本和工具链版本匹配,不然编出来加载的时候会报版本不一致、unknown symbol之类的错。

export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make modules

编完模块后在源码目录下能找到drivers/input/touchscreen/gt9xx.ko,这个文件就是我们要的东西,把它拷贝到开发板的/lib/modules/对应目录或者直接用insmod加载就行。

3.2 设备树编译和启动验证

修改完设备树源码之后,要编译成dtb文件烧到启动分区里。黑金ZYNQ的启动方式是BOOT.bin加image.ub加dtb的组合,设备树编译命令是:

dtc -I dts -O dtb -o devicetree.dtb devicetree.dts

有的黑金资料里会给一个设备树工程目录,直接在里面改然后编译也方便。如果不想重新打包整个启动镜像,有些板子支持从SD卡单独加载dtb,调试阶段特别实用,改一次设备树不用反复擦写整个启动系统。

上电启动之后第一件事就是看内核日志,确认I2C控制器起来了、触摸屏驱动有没有probe成功:

dmesg | grep -i goodix dmesg | grep -i i2c

正常情况会看到类似goodix gt911:成功初始化这样的日志。如果dmesg什么都看不到,先用i2cdetect确认设备在不在总线上:

i2cdetect -y -r 0

命令最后的0是I2C总线编号,要确认你的触摸屏挂的是哪一路。看到设备地址出现在表格里,说明硬件通路通了,问题在驱动配置;什么都没扫到,问题基本在硬件或者复位时序。有一次我扫不到设备,排查半天发现是I2C总线上拉电阻没焊,两根线直接悬空,这种问题看一眼原理图就能定位,别一上来就怀疑驱动。

3.3 evtest验证触摸事件

设备树和驱动都就位之后,检查一下输入设备节点有没有生成:

cat /proc/bus/input/devices

找到Name里带Goodix或者Touchscreen的那一项,记下/dev/input/eventX的编号。然后安装evtest:

apt install evtest evtest /dev/input/event1

这时候用手指在屏幕上滑动,终端里应该会刷出一堆type 3 (EV_ABS)的事件,坐标值会跟着手指位置变化。如果什么事件都没有,但/proc/bus/input/devices里已经有设备节点,大概率是中断没触发。我当时就卡在这,后来发现是设备树里GPIO的active level写反了,中断一直处于错误的电平状态,驱动压根收不到通知。

如果evtest里能出坐标但位置对不上,就是坐标翻转的问题。GT911的驱动源码里通常有gt911_x_to_capacity这类坐标换算函数,可以临时加打印或者直接改翻转逻辑,不过更推荐的方式是先去设备树里把touchscreen-inverted-xtouchscreen-inverted-ytouchscreen-swapped-x-y这三个属性组合调一遍,大多数情况能解决,实在不行再动代码。

3.4 应用层读取触摸数据

触摸屏驱动的最终使用者是应用层的GUI程序。如果你跑的是Qt,在Linux下Qt默认走libinput或者evdev插件,驱动注册的设备节点会被自动识别,不用额外配置。但如果你是在裸环境下自己写读取程序,可以直接读event节点:

#include <linux/input.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main(void) { int fd = open("/dev/input/event1", O_RDONLY); struct input_event ev; if (fd < 0) { perror("open"); return -1; } while (1) { read(fd, &ev, sizeof(ev)); if (ev.type == EV_ABS) { if (ev.code == ABS_X || ev.code == ABS_MT_POSITION_X) printf("X=%d\n", ev.value); else if (ev.code == ABS_Y || ev.code == ABS_MT_POSITION_Y) printf("Y=%d\n", ev.value); } } return 0; }

这个程序是最基础的读取示例,实际项目里还要处理多点触控、按下和释放事件、触点的生命周期管理,复杂性会上去不少,但底层的驱动打通了,应用层的事情就只是时间问题。我在实际项目里一般不会自己写这层解析,直接用现成的GUI框架或者tslib这类触摸库,稳定性比自己轮子好太多。

4. 常见问题与排查技巧实录

4.1 I2C总线上扫不到触摸设备

这是出现频率最高的问题。常规检查路径按顺序大概是:先测I2C总线上的上拉电阻有没有贴、SCL和SDA电平是否正常、触摸屏的供电有没有到位、复位引脚电平对不对、INT引脚有没有被拉死。GT911最麻烦的地方是如果复位时序不对,芯片会一直待在异常状态,I2C上怎么扫都是空的。

我自己的排查经验是,先不看驱动,用示波器或者万用表确认一下INT引脚在上电后有没有一个明显的跳变,有跳变说明芯片在试着请求主机读取数据,链路大概率是通的;没有跳变,就老老实实去看复位时序。软件模拟复位的时候,驱动里有个goodix_i2c_reset之类的函数,可以加打印看它每个步骤执行到哪了,比盲猜效率高得多。

另外有个容易被忽略的点是I2C控制器的时钟频率。GT911最高支持400kHz,但有的板子默认配到了1MHz,芯片可能受不了,导致通信不稳定。设备树里把clock-frequency降到100kHz试试,往往能解决一些莫名其妙的偶发问题。

4.2 设备节点存在但evtest没事件

/proc/bus/input/devices里明明有设备,但evtest一点反应都没有,这种问题的根源通常在中断。要么中断根本没有触发,要么中断请求失败。先用cat /proc/interrupts看看对应的中断号有没有计数,用手指快速点屏幕的同时观察计数有没有增加。计数在涨,说明中断通了,问题在上报逻辑;计数不动,问题在中断配置或者硬件通路。

还有一种情况是驱动注册了input设备,但始终没有使能触摸屏的中断,原因是驱动里的probe逻辑没走完,某个GPIO请求失败导致整个初始化提前返回。这种问题dmesg里一般会有err级别的日志,仔细看goodix相关的输出,基本上能定位到具体是哪个资源没申请到。

实在不行可以在驱动源码里临时加printk,在probe、open、中断处理函数的入口各打一条,跑一遍看执行流程到底断在哪一步。调试内核模块不比调试普通程序,printk是最简单也最有效的武器。

4.3 坐标反向和偏移的处理

坐标反向的解决办法前面提过,优先在设备树里调翻转属性。但要注意驱动对属性的命名在不同内核版本有差异,比如4.4内核用的touchscreen-inverted-x,有些版本则是touchscreen-inverted-y的解析方式相同,另外还有swap-x-ytouchscreen-swapped-x-y的差别。最简单的办法是把驱动源码里of_property_read_bool搜一遍,看它到底认哪些属性。

坐标偏移往往和触摸屏的原始分辨率、驱动初始化的坐标范围有关。GT911的坐标范围出厂默认可能是根据屏幕尺寸自动配置的,但如果你用的是非标准分辨率,驱动里读出来的坐标范围就和屏幕实际像素对不上,表现为触点总在某个角落或者坐标整体偏掉。你可以检查一下/sys/bus/i2c/devices/0-005d/下面的配置文件,看看驱动有没有导出坐标范围的调试接口。

另外还有一个非常现实的问题:电源纹波大、地线没接好,触摸坐标会漂得有规律,比如不触摸的时候还在上报坐标,或者滑动轨迹呈锯齿状。这种问题的排查方向是电源和布线,而不是软件,我之前在一台电源适配器很差的工控机上遇到过,换了个正规电源直接就好了。

4.4 驱动加载时提示版本或符号问题

内核模块加载报version magic不匹配,或者Unknown symbol,这种问题在新手阶段特别常见。版本魔数不匹配通常是因为模块是用不同版本的内核源码编出来的,或者交叉编译工具链版本不一样导致的内核配置差异。解决方法是把内核源码和实际运行的内核二进制严格对应上,最好在开发板上直接确认一下uname -r的结果,然后回头在源码目录里核对。

Unknown symbol则常见于模块依赖其他内核符号,但那个符号没有被导出,或者依赖的模块还没加载。先看一下具体缺哪个符号,用grep在内核源码里搜这个符号的EXPORT_SYMBOL声明,如果是依赖别的模块,就把依赖模块一起加载。GT911这个驱动依赖的符号不多,一般不会出这类问题,但我在做其他外设驱动的时候遇到过几次,经验是先保证所有内置模块都加载完整,再单独装外围的ko。

这些小毛病的排查思路其实都大同小异:看日志、追符号、查配置。内核调试没有捷径,但每一步都有迹可循,静下心来一层层排查,总能找到问题。

5. 一些提升开发效率的小建议

5.1 用好内核自带的调试工具

Linux内核为触摸屏调试提供了不少现成工具,除了evtest之外,lsinputinput-utilshexdump /dev/input/eventX这些都可以用。当evtest不方便安装的时候,直接用hexdump读event节点是最朴素的验证方式:

hexdump -C /dev/input/event1

手指按下去就能看到一串二进制输出,type、code、value都在里面,数据对不对一目了然。内核日志级别也要学会灵活调整,echo 8 > /proc/sys/kernel/printk可以打开更详细的日志,驱动里用dev_dbg打的调试信息就能看到了。

我在调试阶段还会用strace去跟踪应用程序打开input设备节点时的系统调用,确认应用层有没有因为权限、路径等原因打不开设备。有时候应用连不上触摸屏,不是驱动的问题,而是/dev/input/eventX的文件权限问题,加个udev规则把权限放一下就好了。

5.2 模块化开发:驱动和应用同步迭代

触摸屏驱动开发不是一个纯软件的活,它和硬件、内核、应用三个层面都有关系。我的习惯是把整个任务拆成三个里程碑:第一个里程碑是i2cdetect能扫到设备,代表硬件通路通了;第二个里程碑是evtest能看到坐标事件,代表内核驱动通了;第三个里程碑是GUI里能滑动触摸,代表应用层集成了。每个里程碑之间相互独立,卡住的时候定位范围会小很多。

模块化加载的方式也很重要。驱动编成.ko,第一次加载用insmod,出了问题直接rmmod再insmod,改代码重新编译后不用重启系统就能验证新驱动。等彻底稳定了,再把驱动编进内核,或者放到开机自动加载的脚本里。这个小技巧在开发阶段能省下大量反复烧写系统的时间,强烈推荐。

5.3 关于黑金开发板的资料活用

黑金开发板自带的资料里其实已经有了很多细碎的知识点,比如Linux内核编译环境的搭建、设备树的语法介绍、触摸屏原理图的解析,这些东西分散在多个文档里,做项目的时候要认真翻一遍。遇到问题时,代码就是最大的文档,drivers/input/touchscreen/gt9xx.c里面的注释和宏定义能告诉你很多源码层面的设计意图,比网上的二手教程可靠得多。

最后再说一个我自己的体会:触摸屏驱动在嵌入式Linux里算是入门级的驱动程序,但它串起来的I2C、GPIO中断、设备树、Input子系统、内核模块编译这些知识点,几乎覆盖了嵌入式Linux开发的全部核心内容。把这块彻底吃透,后面再做其他外设驱动,流程基本都是一样的套路。

这次在这个项目上从零到点亮触摸屏,前后花了两天时间,大部分时间消耗在复位时序和中断配置上。如果你动手做的时候也卡住了,别气馁,先确认硬件时序对不对,再确认中断通路有没有断,最后才去怀疑驱动代码,这个排查顺序能帮你少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询