去年年底拿到一块黑金开发板,板载 FPGA 芯片,原本想跑个 Linux 系统练手,结果在 7 寸触摸屏驱动上卡了整整一周。当时网上资料零零散散,要么只讲 ARM 板子怎么配触摸屏,要么只讲 FPGA 逻辑怎么写 I2C 控制器,很少有把“FPGA + Linux + 7寸触摸屏”这条完整链路讲清楚的。这篇文章就把我实际调试这套组合的过程、原理和踩过的坑都写出来,希望能帮到同样在 Linux 下驱动触摸屏的朋友,尤其是手里刚好有 FPGA 开发板、想跑嵌入式 Linux 的。
1. 项目拆解:7寸触摸屏在FPGA+Linux系统里到底卡在哪
很多人一开始以为,给触摸屏写驱动就是装个内核模块、改一下设备树,其实这只是最后一步。在“FPGA + Linux”这套组合里,触摸屏驱动涉及的面比普通 ARM 板卡宽得多,因为 FPGA 侧的软核 CPU(比如 MicroBlaze、NIOS II,或者直接上 Zynq 里的硬核 ARM)和片内逻辑、外设 IP 之间,需要协同工作。
1.1 先弄清楚触摸屏本身:接口、控制器与模组选型
7 寸触摸屏通常有两种触摸方式:电阻屏和电容屏。现在主流是电容屏,触摸控制器一般集成在模组上,通过 I2C 接口输出触摸坐标。常见的触摸控制 IC 包括 GT911、FT5x06、GT9147 等,这些芯片在 Linux 内核里大多有原厂驱动或社区驱动,问题在于内核版本、I2C 时序、中断引脚、复位时序这些细节需要匹配。
以 GT911 为例,这颗芯片的 I2C 地址可能是 0x5D 或 0x14,取决于 INT 引脚的上拉电阻配置。如果你在驱动里写死地址 0x5D,但硬件上配置成了 0x14,那触摸屏就完全没反应。我第一次调试时没仔细看原理图,地址写错,启动日志里 I2C 探测一直失败,走了不少弯路。
1.2 Linux 驱动在整个链路里的位置:从物理触摸到应用层事件的完整路径
我们可以把触摸屏工作链路拆成四段:
- 第一段:手指触摸屏幕,触摸控制 IC 检测到电容变化,通过 I2C 总线把坐标数据写入内部寄存器。
- 第二段:FPGA 片内的 I2C 控制器或软核 CPU 的 I2C 外设,读取触摸 IC 的寄存器数据。
- 第三段:Linux 内核里的触摸屏驱动(input subsystem 下的驱动)定时读取或中断触发读取坐标,然后上报给 input 子系统。
- 第四段:应用程序通过 /dev/input/eventX 读取触摸事件,完成手势识别、坐标映射等处理。
驱动开发者主要工作在第三段,但前面两个环节如果没打通,驱动写得再完美也白搭。我在调试时先确保 I2C 总线能枚举到设备,再用 i2c-tools 直接读寄存器,确认触摸 IC 工作正常后,才开始做内核驱动。
1.3 方案选型:为什么最终走“软核CPU + Linux + 现成触摸IC驱动”路线
FPGA 上跑 Linux,一般有三种方案:一是用 Zynq 这类带硬核 ARM 的 SoC FPGA,直接在 ARM 上跑 Linux,FPGA 逻辑作为外设;二是用纯 FPGA + 软核 CPU(MicroBlaze/NIOS II),软核跑 Linux;三是 FPGA 只做逻辑,外接 ARM 主控。
我调试的平台属于第二种,软核 CPU 跑 Linux。这种方案的好处是灵活,可以在 FPGA 里自定义 I2C 控制器、中断控制器、DMA 等外设,坏处是软核性能弱,外设 IP 的寄存器时序需要自己核对接好。触摸屏驱动本身用内核自带的 GT911 驱动改动量不大,难点在于让软核 CPU 的中断系统和设备树描述匹配,这部分需要仔细对寄存器基地址和中断号。
2. 触摸屏驱动开发前的准备工作
正式写代码之前,有几项准备工作不能跳过,否则后面调试会很痛苦。这些工作包括硬件连接确认、内核配置、设备树编写、驱动源码准备。
2.1 硬件连接与原理图确认:I2C地址、复位引脚、中断引脚
从硬件上要确认的最重要信息,就是触摸控制 IC 的型号、I2C 地址、复位引脚 GPIO 编号、中断引脚 GPIO 编号。以 7 寸 GT911 模组为例,常见连接是:
- SCL、SDA 接到 FPGA 软核的 I2C 外设引脚;
- INT 引脚接到 FPGA 的一个 GPIO,可配置为中断输入;
- RST 引脚接到另一个 GPIO,用于上电复位;
- I2C 地址由 INT 引脚电平决定。
我在实际调板时发现,INT 引脚在复位期间有特殊时序要求。GT911 要求在上电后先拉低 RST,保持一段时间,再拉高 RST;然后根据 INT 引脚电平决定地址。如果只用简单的 GPIO 拉高拉低,不关注时序,可能触摸 IC 会一直处于异常状态。
2.2 内核配置与设备树:让触摸IC在Linux里“现形”
在 Linux 里,I2C 设备驱动通过设备树节点与硬件绑定。如果内核没有开启相应的 driver,就算设备树写得再对也没用。首先要确保内核开启了 I2C 子系统、GPIO 中断支持、以及对应触摸 IC 的驱动。
内核配置项一般在 Device Drivers -> Input device support -> Touchscreens 下,GT911 对应的配置项是 CONFIG_TOUCHSCREEN_GT9XX,FT5x06 是 CONFIG_TOUCHSCREEN_EDT_FT5X06。编译内核时可以用 menuconfig 搜索看看有没有使能。
设备树节点的核心信息包括 compatible 字符串、reg(I2C 地址)、interrupt-parent 和 interrupts、复位 GPIO、触摸屏分辨率等。compatible 字符串必须和驱动源码里的 of_match_table 一致,否则内核不会绑定驱动。
2.3 选择驱动源码:内核自带、厂商SDK还是手写?
根据触摸 IC 的不同,驱动的获取路径有差异:
- GT911、FT5x06 这类主流触控 IC,内核主线自带驱动,通常直接可用,只需要修改设备树节点配置。
- 一些国产触摸 IC,内核主线可能没有驱动,需要去芯片原厂官网下载 Linux 驱动,或者参考同系列驱动移植。
- 如果触摸 IC 非常冷门,可能要自己根据数据手册写一个 input 子系统驱动,工作量会大不少。
我的建议是先查内核版本对应的源码目录,比如 drivers/input/touchscreen/ 下有没有 gt9xx.c 或 ft5x06.c。如果有,优先用主线驱动,因为社区维护者已经踩过大量坑,稳定性远好于来源不明的厂商驱动。我实际用 GT911 时就是直接改设备树节点,没有动驱动源码。
3. 驱动移植与调试的核心实操
这一章是文章最核心的部分。我会以 GT911 为例,给出设备树节点、内核配置、编译加载、事件验证的完整流程。这套流程同样适用于 FT5x06 等 I2C 触摸 IC。
3.1 设备树节点编写实例:GT911 典型配置
假设 FPGA 软核 CPU 的 I2C 外设节点名为 i2c0,基地址 0x40000000,中断控制器 handle 为 intc。设备树里新增一个子节点,典型内容如下:
&i2c0 { status = "okay"; clock-frequency = <100000>; gt911: gt911@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&intc>; interrupts = <3 2>; pinctrl-names = "default"; reset-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>; irq-gpios = <&gpio0 11 GPIO_ACTIVE_HIGH>; goodix,cfg-group-0 = [ 00 20 03 E0 02 0A 05 01 01 2D 0F 8A 32 05 05 00 ... ]; touchscreen-size-x = <1024>; touchscreen-size-y = <600>; }; };注意 compatible 有两种常见写法:goodix,gt911 或 goodix,gt9xx,具体要看内核驱动里 of_device_id 表支持哪个。reset-gpios 是复位引脚,irq-gpios 是中断引脚。如果你的板子是 GPIO 子系统没有接好,可以先用轮询方式调试,把 poll-interval 加上。
这里给出参数时要注意,中断号要查软核 CPU 的中断控制器分配表,不是随便填的。我调试时误用了别的外设中断号,导致驱动申请中断失败。比较稳妥的做法是先不加 interrupts,用 poll-interval = <10> 轮询模式验证触摸数据是否正常,确认没问题后再切换成中断模式。
3.2 内核编译与模块加载:从零构建可用的触摸屏驱动环境
如果你的系统是直接烧录整个内核镜像,需要重新编译内核并生成新的 boot 镜像;如果能加载内核模块,也可以把触摸驱动编成 .ko 模块。我习惯先把驱动编成模块调试,这样改参数不用反复烧系统。
进到内核源码目录,设置好 ARCH 和 CROSS_COMPILE 环境变量,执行:
make menuconfig在 Device Drivers -> Input device support -> Touchscreens 里找到 Goodix touchscreen,按 M 编译成模块。然后编译模块:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules编译完成后,把 drivers/input/touchscreen/goodix.ko 拷贝到目标系统的 /lib/modules/$(uname -r)/ 下,执行 depmod -a,然后 modprobe goodix。如果设备树节点匹配成功,启动日志会打印类似 “input: Goodix GT911 TouchScreen as /devices/.../input/input0” 的信息。
模块加载后,用 dmesg 查看驱动初始化信息,重点确认是否成功探测到设备、中断申请是否成功、有没有校验和错误。我遇到最多的问题就是 I2C 设备地址不匹配导致 probe 失败,日志里显示 “goodix: failed to read config data”。
3.3 触摸事件验证:从 evtest 到坐标上报
驱动加载成功只是第一步,关键要确认触摸事件能正常上报。用 evtest 工具可以看到 input 设备列表,选择触摸屏设备后,手指点击屏幕,终端会实时打印 ABS_MT_POSITION_X、ABS_MT_POSITION_Y 等事件。
evtest /dev/input/event0如果事件打印正常,说明驱动和硬件链路已经打通。接着可以做一个简单测试:用手指在屏幕左上角和右下角分别点击,记录上报坐标,用这些坐标做后续校准。
如果 evtest 没有打印任何事件,优先检查 /dev/input/ 下是否有 eventX 节点,以及 dmesg 里驱动 probe 是否成功。如果 probe 成功但没中断,多半是中断引脚配置有问题,可以转轮询模式验证。
4. 触摸不准怎么办:校准、坐标变换与稳定性问题
很多时候,驱动能让触摸屏“动起来”,但坐标不对,要么点击位置偏了,要么横竖屏响应方向不对,要么触摸飘移、偶发误触。这些问题不全是驱动 bug,很多是坐标映射和校准环节的问题。
4.1 触摸校准原理:为什么需要校准以及tslib怎么用
触摸屏的原始坐标和 LCD 显示坐标之间,存在比例、偏移、旋转等映射关系。理想情况下两者是线性变换,可以用一个 3x3 矩阵描述,校准的目的就是把这个矩阵求出来。
嵌入式 Linux 里常用 tslib 做校准。tslib 提供 five-point calibration 和 linear transform 等插件,会生成 /etc/pointercal 校准文件。使用前需要设置环境变量:
export TSLIB_TSDEVICE=/dev/input/event0 export TSLIB_CALIBFILE=/etc/pointercal ts_calibrate按屏幕提示点击五个点,tslib 会计算出校准参数,写入 /etc/pointercal。之后所有坐标读取都会经过 tslib 插件变换,触摸位置就准了。
4.2 横竖屏切换与坐标旋转的处理
如果你的 7 寸屏是竖屏安装,但 Linux 显示是横屏,坐标就需要旋转。tslib 有 rotate 插件,可以在配置文件中设置旋转角度。不过更推荐在应用层处理坐标变换,因为 tslib 的旋转只影响通过 tslib 读坐标的程序,而 direct input 方式读 /dev/input/eventX 的程序不受影响。
坐标旋转本质是矩阵变换。90 度旋转的公式比较简单:假设触摸原始坐标是 (x, y),显示宽高是 W、H,顺时针旋转 90 度后显示坐标是 (y, W - x)。这个变换可以直接写进应用的读取逻辑里,避免依赖 tslib。
我在实际项目里,最终没有用 tslib,而是把坐标变换逻辑写在 Qt 应用的 QScreen 事件处理里。因为触摸屏的驱动节点固定,应用可以直接读原始坐标,自定义映射更灵活。
4.3 触摸漂移、误触与干扰的处理经验
电容屏在低温、潮湿、电磁干扰环境下,容易产生坐标漂移或误触。处理这类问题有几个经验:
- 检查电源纹波。触摸 IC 对供电敏感,如果供电纹波大,触摸坐标会跳动厉害。我测量过,在开关电源输出加一个 10uF 陶瓷电容后,漂移明显减少。
- 检查 I2C 总线时序。I2C 速率过高可能导致通信出错,触摸数据偶尔读回 0xFFFF,表现为触摸点偶尔跳到角落。把 clock-frequency 从 400000 降到 100000 往往能改善。
- 检查中断处理函数是否耗时过长。如果中断里读取多个坐标寄存器,而在读取期间 GPIO 抖动,会产生误触。可以在中断里加简单的消抖逻辑。
5. FPGA侧逻辑如何与Linux驱动协同工作
前面讲的驱动移植,更多是通用 Linux 知识。但既然标题是 FPGA 技术教程,必须把 FPGA 侧的逻辑设计也讲透。软核 CPU 跑 Linux,外部设备通过 AXI 总线或自定义 Wishbone 总线挂在 CPU 上,FPGA 内部逻辑要提供正确的寄存器接口和中断信号,Linux 驱动才有东西可操作。
5.1 FPGA在触摸链路中的角色:I2C控制器、中断控制器与寄存器桥
在纯 FPGA + 软核方案中,I2C 控制器有两种实现路径:一是直接用软核 CPU 自带的外设 IP,比如 MicroBlaze 的 AXI IIC IP、NIOS II 的 I2C IP;二是自己在 FPGA 里写一个 I2C 控制器,通过 AXI-Lite 接口挂到总线上。
我用的平台有现成的 AXI IIC IP,省了不少事。但要注意,AXI IIC IP 的驱动对应内核里的 drivers/i2c/busses/i2c-xiic.c,设备树里 compatible 要写对,否则内核识别不了这个 I2C 控制器。
除了 I2C 控制器,FPGA 侧还有一个重要模块是 GPIO 中断控制器。触摸 IC 的 INT 引脚接到 FPGA 的 GPIO,GPIO 检测到边沿后,通过中断控制器上报给 CPU。在 FPGA 代码里,中断边沿检测逻辑要设置好上升沿还是下降沿触发,并且要有足够宽的中断脉冲,否则 CPU 可能收不到。
5.2 AXI-Lite寄存器接口设计:让CPU“看得见”触摸数据
如果你不想用现成 I2C IP,而是自己写 I2C 控制器,那么 AXI-Lite 寄存器接口设计就很关键。一个典型的寄存器接口需要包括:
- 控制寄存器:启动、停止、读、写、中断使能;
- 状态寄存器:忙标志、错误标志、FIFO 空满;
- 数据寄存器:发送数据、接收数据;
- 地址寄存器:从设备地址和寄存器地址。
我自己的经验是,寄存器位定义越简单越好,不要搞太多组合状态。一个寄存器一个功能,驱动侧用 writel/readl 直接访问,省去很多调试时间。中断状态寄存器要支持“写1清0”的模式,这样驱动处理完中断后能及时清除标志,避免重复进入中断。
5.3 裸机驱动到Linux驱动的复用与适配
在做 Linux 驱动之前,我一般会先在裸机环境里用 FPGA 软核的 SDK 把 I2C 控制器调通,读一下触摸 IC 的 ID 寄存器,确认通信正常。裸机程序调试简单,出问题可以直接看寄存器值,定位快速。
裸机程序转到 Linux 驱动时,有几个注意点:
- 裸机用的 baseaddr 是物理地址,Linux 驱动要 ioremap 之后才能访问;
- 裸机的中断处理函数是直接绑定到 Cortex-M 或 MicroBlaze 的中断向量表,Linux 里要注册中断处理函数,且处理函数不能太耗时,耗时操作要放到 workqueue 或 threaded irq 里;
- 裸机的 GPIO 操作很简单,Linux 里要用 GPIO 子系统的 API,而且设备树里的 GPIO 编号要对应 FPGA 内部逻辑的引脚。
6. 踩坑记录与效率工具:让触摸屏驱动开发少走弯路
最后这部分,我把调试过程中遇到的高频问题整理成了速查表,再分享一些能提升效率的命令和技巧。这些问题很多是硬件平台相关的,但在其他嵌入式 Linux 触摸屏项目里也同样适用。
6.1 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| I2C 探测不到触摸 IC | I2C 地址错误、触摸 IC 复位时序不对、I2C 控制器时钟未使能 | i2cdetect 扫描地址,检查复位 GPIO 时序,dmesg 查看 I2C 控制器状态 |
| 驱动 probe 失败 | 设备树 compatible 不匹配、中断号错误 | 核对 of_match_table,确认中断号在软核中断表里有效 |
| 触摸事件不打印 | 驱动未加载、中断未触发、I2C 读坐标超时 | 先用轮询模式验证,再查中断 GPIO 电路 |
| 坐标位置偏移大 | 未校准、坐标分辨率不匹配 | 运行 ts_calibrate,检查设备树 touchscreen-size-x/y 是否与屏幕一致 |
| 触摸偶尔跳点 | I2C 速率过高、电源纹波、静电干扰 | 降低 I2C 速率,检查电源滤波,检查触摸屏排线接地 |
| 横竖屏触摸方向不对 | 坐标未旋转 | 应用层做坐标矩阵变换,不要依赖 tslib |
6.2 实用调试命令与效率技巧
调试 I2C 设备时,i2c-tools 是我必装的一套工具。安装后在终端执行 i2cdetect -y 0,可以列出 I2C 总线 0 上探测到的设备地址。这个命令能快速确认触摸 IC 是否在总线上,以及地址是 0x14 还是 0x5D。
确认设备地址后,用 i2cget 读取触摸 IC 的 ID 寄存器也是一种快速验证方式。GT911 的寄存器 0x8140 保存了芯片 ID,读出 0x39、0x31、0x31 就说明通信正常。这一步能帮你把问题范围缩小到“硬件通信”还是“驱动软件”。
还有一个技巧,就是利用 Linux 的 input 子系统设备节点数量变化判断驱动加载情况。加载 goodix 模块前,/dev/input/ 下可能只有 event0;加载成功后,会多出 event1 或 event2。用 udevadm monitor 也能捕捉到内核动态创建 input 设备的事件,这对确认 probe 是否成功很有帮助。
调试触摸屏驱动时,我习惯先在应用层写一个简单的 Qt 或 GTK 程序,通过 QTouchEvent 或直接读 /dev/input/eventX 打印坐标。应用层能实时反映驱动改动效果,比反复编译内核模块再查日志直接得多。我个人在实际操作中的体会是,软件层面的驱动移植只要设备树和中断号不出错,半天就能完成;真正耗时的往往是硬件信号完整性、复位时序、供电纹波这些偏硬件的问题。建议大家在 LCD 点亮、触摸 IC 通信正常之前,不要着急写驱动,先把基础链路打通,后面会顺很多。
最后再分享一个提升效率的小技巧:FPGA 开发板调试触摸屏时,尽量在 FPGA 逻辑里预留一个调试 GPIO,用于观察触摸 IC 中断信号的时序。用逻辑分析仪抓 INT 引脚波形,确认触摸事件确实产生了中断脉冲,再谈 Linux 端的事情。这个习惯帮我省了大量排查时间,强烈推荐你也试试。