
第一次在FPGA板卡上接7寸触摸屏我花了一个下午找到的是一根弯折的FPC排线而不是改设备树。后来我才明白触摸驱动这件事从来不是加载一个内核模块这么简单它是完整的一条链路屏幕模组上的触摸控制芯片、FPGA里的总线主控逻辑、设备树描述、内核input子系统事件上报最后才是用户态的校准工具。黑金云课堂这套FPGA技术教程的Linux开发部分把7寸触摸屏驱动当作一个非常典型的软硬件结合案例来讲确实讲到了点子上。网上搜触摸屏驱动大部分结果都是某个芯片在树莓派或者x86工控机上的移植笔记比如XPT2046的SPI时序、FT5x06的I2C地址。这些内容有用但如果你是在FPGA上做Linux开发会很快发现这些笔记缺了最重要的前一半——触摸控制器和CPU之间那截总线、中断、寄存器在FPGA方案里需要你自己设计。这篇文章就把我在这条路上的完整思路整理出来从硬件连接、FPGA逻辑、设备树、内核驱动、用户态校准到实测踩坑一条线串完适合刚把Linux跑上FPGA、正被屏幕和触摸折磨的开发者。1. 一块7寸屏在FPGA板卡上是怎么长出来的1.1 先分清你是在给谁写驱动同样是Linux触摸驱动FPGA平台上其实有两条差别很大的路径。第一类是SoC FPGA典型代表是Xilinx Zynq-7000、Zynq UltraScale和Intel Cyclone V SoC。芯片里集成了一颗ARM硬核Linux跑在ARM上FPGAPL侧主要负责外设扩展。触摸芯片的SPI或I2C引脚可以挂在PL的IO上但总线控制器可以复用PS自带的SPI/I2C外设也可以自己在PL里做一个。这种方案下你的主要精力放在设备树、管脚约束和内核配置上HDL逻辑有时候只是一层引脚转发。第二类是纯FPGA加软核处理器例如Nios II或MicroBlaze上跑Linux。这才是很多老教程里FPGA味最重的方案触摸控制器作为自研外设挂在片上总线上Linux驱动面对的是内存映射寄存器而不是直接操作SPI/I2C控制器。你既要写HDL状态机又要写C驱动两边都要看懂。我见过不少人在论坛里贴出设备树片段问为什么不进中断结果一问才发现板子根本没有ARM硬核Linux跑在软核上设备树里的compatible对不上任何现成驱动。所以第一步不是抄代码是认清楚你的硬件属于哪条路径。1.2 电阻、电容、红外三种触摸方案选型差异7寸屏的触摸方案在FPGA板卡上基本就是三类。我先把它们在Linux主线里的状态整理一下。类型代表芯片接口常用内核配置场景电阻式XPT2046、ADS7846、TSC2007SPICONFIG_TOUCHSCREEN_XPT2046 / ADS7846工控HMI、低成本单点电容式FT5x06、GT911、GT9147I2CCONFIG_TOUCHSCREEN_EDT_FT5X06 / GOODIX量产设备、多点触控红外式各品牌红外框如TouchKitUSB HID / 串口多为usbhid或厂家协议大尺寸自助设备7寸很少用FPGA开发板上电阻式最常见原因很直白标准7寸RGB接口模组大量自带电阻触摸配的触摸控制芯片往往是XPT2046或它的兼容片SPI接口简单到可以用几十行状态机实现不依赖厂商闭源固件。电容屏体验好但I2C时序和初始化配置没有SPI那么透明FocalTech早期芯片甚至要加载一堆寄存器参数主线上edt,edt-ft5x06驱动能覆盖一部分不见得覆盖所有批次的模组。至于红外式7寸面板上基本不用如果你搜到TouchKit红外触摸屏驱动这种关键词那大概率是15寸以上的查询机和自助设备接口通常是USB HID或串口私有协议跟FPGA里的触摸逻辑是完全不同的技术路线别混着查。1.3 触摸和显示是两条独立通路新手最容易绕进去的死胡同是把触摸和LCD当成一回事。实际上触摸模组和LCD模组在物理上是一体的但在信号链路上完全是分开的显示走RGB/MIPI/HDMI由FPGA的时序控制器或SoC的显示控制器负责触摸走SPI/I2C再加一根中断脚。你调通了framebuffer不代表触摸就能工作反过来触摸设备树节点写错也不影响屏幕显示。有个真实的对比案例。同一块7寸屏接到Zynq上显示路径要配置VDMA、时序发生器、屏幕背光占用不少FPGA资源和DDR带宽而触摸路径只需要一根SPI或I2C加上一个中断资源占用几乎可以忽略。所以如果你发现屏幕显示正常但触摸完全没反应别怀疑是VDMA或MIPI配置问题大概率是触摸这条小通路断了——排线没插紧、触摸芯片没供电、中断引脚被复用成了别的功能这些才是常见原因。2. 硬件连接与FPGA侧触摸控制逻辑2.1 XPT2046的引脚连接参考以最经典的XPT2046电阻触摸方案为例。7寸模组上有一个触摸控制芯片它的引脚通过FPC引出到了FPGA板上就是下面这几根线。信号方向作用SCLKFPGA - XPT2046SPI时钟MOSI/DINFPGA - XPT2046下发命令选择读X还是YMISO/DOUTXPT2046 - FPGA回传12位ADC采样值CSFPGA - XPT2046片选低有效PENIRQXPT2046 - FPGA笔按下的中断输出低有效VCC/GND电源多数模组是3.3V几个容易出错的地方。第一电平匹配。很多7寸模组接口是3.3V如果FPGA的IO bank配置成2.5V触摸芯片可能能工作但信号边沿会很难看甚至直接不工作。第二PENIRQ这根脚。部分模组的PENIRQ是开漏输出板上已经做了上拉如果你自己画板一定别忘了在FPGA内部或PCB上加上拉电阻否则按下屏幕时中断脚被拉低松开后又没有确定的高电平Linux里的中断计数会被毛刺刷爆。第三FPC排线尽量不要过度弯折触摸芯片的SPI时钟也就2MHz上下排线质量问题不至于直接导致时序失败但会带来触摸值随机跳变排查起来特别隐蔽。2.2 SPI时序是第一个分水岭XPT2046的读取过程看着简单CS拉低发送一个8位控制字选择X通道、Y通道还是压力然后读回12位ADC数据。但很多第一次写FPGA SPI状态机的人会在第一个分水岭翻车——时钟极性和相位。控制字的格式大致是最高位是起始位接着是通道选择位然后是12/8位模式选择、参考电压选择、掉电模式选择。比如要读X坐标下发0xD0读Y坐标下发0x90。这只是常见的命令举例不同厂家的兼容片可能定义略有出入最终以模组上实际芯片的数据手册为准。在FPGA逻辑里SPI Master状态机本身不难难的是采样边沿。我的习惯做法是SCLK空闲为低在SCLK的下降沿把MOSI上的下一位移出在SCLK的上升沿把MISO采回来。这实际上就是SPI Mode 0。如果你在Linux侧还挂了一个SPI控制器配合着用设备树里的spi-cpha、spi-cpol属性和FPGA里的时序必须一致否则最常见的表现是读回来的坐标恒为某个固定值或者每次采样都不稳定。下面是一段简化到只剩主干的Verilog状态机示意只说明状态转移实际工程还要加时钟分频、去抖、bit计数清零等逻辑。// 简化版 XPT2046 读坐标状态机 localparam IDLE 2d0; localparam SEND 2d1; // 移出 8bit 控制字 localparam READ 2d2; // 采样 12bit ADC localparam DONE 2d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n 1b1; end else begin case (state) IDLE: if (touch_start) begin cs_n 1b0; state SEND; end SEND: if (bit_cnt 4d8) begin bit_cnt 4d0; state READ; end READ: if (bit_cnt 4d11) begin data shift_data; done 1b1; state DONE; end DONE: begin cs_n 1b1; done 1b0; state IDLE; end endcase end end2.3 中断来了不要马上读坐标PENIRQ拉低表示有人碰到了屏幕但这时候坐标大概率还在抖动。机械抖动、ADC建立时间、面板本身的噪声都会让紧跟着读回来的坐标在几十到上百个ADC码值之间跳。硬件上比较稳妥的处理是检测到PENIRQ下降沿后在FPGA里做一个10ms左右的延时然后再启动SPI读取连续读到两次坐标差小于某个阈值才认为坐标稳定可以更新寄存器。这个去抖逻辑在Linux内核里也有对应物。ADS7846兼容驱动有ti,debounce-max、ti,debounce-tol、ti,debounce-rep这几个参数控制连续多少次、误差在多大范围内算有效。如果你在FPGA侧没做任何去抖光在内核里调debounce参数也能缓解但不如硬件上先把脏数据过滤掉来得干净。我的建议是FPGA侧做基础去抖内核侧参数只负责微调两边分工才不容易互相影响。2.4 I2C电容屏能复用PS控制器就复用如果你用的是Zynq这类带硬核的平台电容屏尽量用PS自带的I2C控制器通过EMIO把SDA/SCL引到PL对应的引脚上。这个方案里FPGA只需要做引脚约束和上下拉配置Linux里走标准I2C总线设备树挂一个子节点就行省去自己写I2C Master的麻烦。如果平台没有可复用的I2C控制器或者你想在软核上自己来那就得在FPGA里实现一个轻量I2C Master。I2C比SPI麻烦的地方在于它是半双工、有ACK、有START/STOP等时序要求状态机要比SPI复杂不少。还有一个特别容易忽略的点I2C是开漏总线必须有上拉电阻。很多人在FPGA工程里不管物理上拉直接在代码里把SDA写成强拉高然后折腾半天找不到问题——实际上你看到的高电平只是FPGA内部弱上拉驱动能力根本不够。3. Linux内核驱动从设备树到input子系统3.1 设备树里的触摸节点怎么挂不管是用SPI的XPT2046还是I2C的FT5x06设备树节点都要挂在正确的总线节点下面。下面是一个常见的XPT2046节点写法挂在名为spi0的控制器下。spi0 { status okay; touch: xpt20460 { compatible xpt2046; reg 0; spi-max-frequency 2000000; touchscreen-min-x 100; touchscreen-max-x 3800; touchscreen-min-y 120; touchscreen-max-y 3700; touchscreen-max-pressure 255; }; };要注意touchscreen-min-x、touchscreen-max-x这些值不是屏幕分辨率而是触摸采样值的上下限。具体数值要靠实测获取用一个调试工具先读原始ADC值是最快的。如果你内核版本较老没有xpt2046驱动改用compatible ti,ads7846并按ADS7846的文档补上ti,x-plate-ohms等属性也能驱动兼容芯片。电容屏FT5x06的节点则挂在I2C总线下。i2c1 { status okay; ft5x06: ft5x0638 { compatible edt,edt-ft5x06; reg 0x38; interrupt-parent gpio0; interrupts 7 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 8 GPIO_ACTIVE_LOW; touchscreen-size-x 1024; touchscreen-size-y 600; }; };很多7寸模组上的电容芯片丝印是FT5406或FT6206只要协议兼容edt,edt-ft5x06驱动也能认。GT911那类Goodix芯片同样挂在I2C下内核里有CONFIG_TOUCHSCREEN_GOODIX具体compatible字符串要按你内核源码里的匹配表来别照抄别的平台。3.2 内核配置该开哪些项设备树写完了内核配置不够的话照样白搭。触摸相关的开关基本在Device Drivers - Input device support - Touchscreens这个菜单树下。常用配置项按你实际方案勾选CONFIG_INPUT_TOUCHSCREENyCONFIG_TOUCHSCREEN_XPT2046yCONFIG_TOUCHSCREEN_ADS7846y老内核或ADS7846方案CONFIG_TOUCHSCREEN_EDT_FT5X06yCONFIG_TOUCHSCREEN_GOODIXyGT9xx系列CONFIG_INPUT_EVDEVyCONFIG_INPUT_MOUSEDEVy可选把触摸模拟成鼠标这里有个容易被忽略的关联配置如果你的触摸芯片挂在内核I2C子系统下I2C控制器本身的驱动必须打开比如Zynq PS侧的I2C对应的是Cadence I2C控制器配置项通常叫CONFIG_I2C_CADENCE。很多人触摸节点没问题、芯片也在但总枚举不到设备最后发现是I2C控制器根本没编进内核。3.3 驱动上报事件的模型单点、多点、MT协议你不需要为这个项目从零写完整的内核驱动因为XPT2046、FT5x06这些主线驱动已经存在。但读驱动程序的时候最好知道它们内部是怎么上报数据的。单点触摸上报的核心就是四个调用input_report_key(input, BTN_TOUCH, 1); input_report_abs(input, ABS_X, x); input_report_abs(input, ABS_Y, y); input_sync(input);多点触摸则要遵循MT协议。内核里常见的是Type B也就是带slot的报法大致长这样input_mt_slot(input, 0); input_mt_report_slot_state(input, MT_TOOL_FINGER, true); input_report_abs(input, ABS_MT_POSITION_X, x); input_report_abs(input, ABS_MT_POSITION_Y, y); input_mt_report_slot_state(input, MT_TOOL_FINGER, false); input_sync(input);这里有个值得说的细节XPT2046这种电阻屏驱动很多用的是input_polled_dev轮询模式也就是每隔一段时间主动去读一次ADC而不是完全靠中断触发。轮询模式的优点是硬件链路简单缺点是CPU占用和功耗略高。如果你用中断加轮询混合的方式担心漏事件可以把input_poll_interval调短一点但别小于1ms不然一帧数据还没读完又触发下一次内核线程会一直占着CPU。4. 用户态校准与触摸屏的手感4.1 evtest是最快的判断工具驱动加载完先别急着连Qt或写应用直接用evtest看内核上报的事件。假设设备节点是/dev/input/event1执行evtest /dev/input/event1手指点在屏幕上你会看到类似这样的输出Event: time 1712345678.123456, type 3 (EV_ABS), code 0 (ABS_X), value 2304 Event: time 1712345678.123456, type 3 (EV_ABS), code 1 (ABS_Y), value 1600 Event: time 1712345678.123456, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1 Event: time 1712345678.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0看到这几行说明内核驱动到input子系统整条链路是通的。如果event节点存在但没有任何事件输出先查中断如果事件有但只有ABS_X没有ABS_Y查SPI/I2C读回来的数据长度对不对如果事件里的value永远不变多半是SPI时序的采样边沿错了。evtest是判断问题出在驱动之前还是驱动之后的分水岭这个工具一定要先跑。4.2 电阻屏不要跳过tslib校准对于XPT2046这类电阻屏tslib几乎是必装的因为原始ADC值和像素坐标之间不是简单的线性缩放。7寸屏可能只用到屏幕中间一块四个角的ADC采样值可能是(120,140)到(3850,3720)还带着旋转和镜像。tslib的作用就是把这套非线性关系转成像素坐标。# Debian/Ubuntu系列 apt install libts-bin export TSLIB_TSDEVICE/dev/input/event1 ts_calibrate ts_test_mt校准完成后会生成一个/etc/pointercal文件里面是7个浮点数系数。触摸屏不换、屏幕分辨率不改、驱动不重编的情况下这个文件可以一直用。有一点要提醒有些新系统把触摸事件的读取交给了libinputtslib直接读/dev/input/eventX可能读不到事件或者和libinput互相打架。如果你发现ts_test光标不动检查一下系统里是否同时有libinput在消费同一个event设备。4.3 应用层接法Qt和X11两条路在嵌入式Linux上接应用老项目常走tslib新项目基本都转向libinput。在Qt5/Qt6的嵌入式环境下如果用了linuxfb或eglfs插件触摸屏插件通常叫evdevtouch或tslib平台启动参数里指定一下就行。在X11桌面环境里用xinput命令查看设备并设置校准矩阵会更直接xinput list xinput set-prop pointer:FT5x06 libinput Calibration Matrix 1 0 0 0 1 0 0 0 1这里的9个数字是一个3x3矩阵对应旋转和缩放。很多人只关心能不能点我建议在项目初期就顺手写一个脚本启动时检测触摸设备、读取已有校准参数、再启动GUI。别问为什么等你同时接了一块电阻屏和一块电容屏就知道这套脚本能救命的次数有多频繁。5. 实测踩坑记录没反应、乱飞、漂移5.1 触摸没反应先查中断脚别急着改驱动最典型的故障是设备树挂上了event设备也有了但点了没反应。我的排查顺序永远是先看中断计数cat /proc/interrupts用笔点一下屏幕看触摸设备对应的中断计数有没有增加。如果计数不涨问题基本与Linux驱动无关集中在硬件链路PENIRQ有没有拉到FPGA引脚、排线有没有插好、引脚约束有没有外漏。如果计数狂涨甚至失控说明中断脚在不该触发的时候一直触发检查上拉电阻和中断类型配置IRQ_TYPE_EDGE_FALLING和IRQ_TYPE_LEVEL_LOW要看芯片手册决定很多国产兼容片对电平中断支持得并不好。我还遇到过一种隐蔽的情况中断每次都触发但内核线程读到的坐标一直是(0,0)。查到最后是FPGA里的SPI状态机在启动复位后没有初始化bit计数寄存器第一次读数据时少采了4位整帧数据错位。evtest里表现就是有事件但坐标恒定这时候不要怀疑设备树要用逻辑分析仪去看SCLK、MOSI、MISO三根线是否符合你预期的时序。5.2 坐标乱飞或翻转八成是通道或极性反了电阻屏最常见的乱飞原因有三个X/Y通道对调、X/Y反方向、SPI读回来的值没有做位宽掩码。用evtest可以快速判断手指沿屏幕水平方向慢慢滑动如果输出的ABS_X变化但方向不对或者变化的是ABS_Y那就需要翻转或交换坐标。设备树里的处理方式很直接touchscreen-inverted-x; touchscreen-inverted-y; touchscreen-swapped-x-y;这三个布尔属性在很多主线触摸驱动里都支持含义字面意思。改完设备树记得重新加载overlay或重启系统。如果你更依赖tslib也可以在pointercal上做调整但我倾向于把这类基本映射放在设备树层解决tslib只做非线性校准职责分开后面好维护。5.3 多点触摸失效MT协议Type A和Type B的坑电容屏的触摸乱和电阻屏不一样更常见的是多指同时按下时只剩一个点或者点住一个地方不动屏幕上却不断出现新触点。这往往不是硬件问题而是驱动上报的MT协议类型和应用层期待的不一致。Linux多点触摸有两种协议Type A用于非硬件关联的多指识别内核只负责上报一个个触摸点Type B使用slot机制内核给每个触点分配一个slot应用层能追踪同一根手指的滑动轨迹。绝大多数现代应用框架期待Type B。如果你的驱动上报的是Type A而框架按Type B解析就会出现触点无法关联、坐标漂移、断点。FT5x06驱动本身支持Type B但个别老版本内核的驱动默认走Type A需要在源码里把input_mt_init_slots的参数配成能力位正确的值。电阻屏不存在这个烦恼因为它物理上就只有一个触点。如果你在XPT2046方案上非要实现两点缩放手势那是FPGA和驱动都得改的大工程不是简单调配置能完成的项目规划时最好想清楚。5.4 漂移是电阻屏的宿命但可以缓解电阻屏用久了或者温度变化大坐标会慢慢漂移这是物理特性决定的没法根治。缓解手段有几个第一在FPGA侧加滑动平均滤波把连续几次采样取平均再更新寄存器第二在tslib里开启降采样和均值滤波插件第三保证触摸芯片的模拟参考电压干净不要在同一个电源轨上给电机或继电器供电。如果是I2C电容屏出现漂移事情通常更麻烦。比如GT911驱动里有一些出厂参数和中断模式配置是用固件写入的你自己改驱动未必能生效。我遇到过一次触摸点整体向左偏移十几个像素最后发现是屏幕贴合时玻璃微偏不是代码问题校准矩阵一转就解决了。所以碰到漂移先分清楚是面板物理量变还是采样值本身变了这两条路查起来天差地别。6. 软核处理器上的扩展玩法6.1 自定义寄存器设备如果你的Linux跑在Nios II或MicroBlaze上触摸控制器就是你自己定义的内存映射外设内核驱动的写法会完全不一样。比如你定义一个32位寄存器Bit[11:0]存XBit[23:12]存YBit[24]存是否按下Linux驱动只需要在这个外设对应的物理基址上ioremap然后注册一个中断处理函数在中断里读一次寄存器再上报input子系统。这种方案的调试难点已经从驱动转移到了FPGA侧寄存器是否按约定工作。我在软核平台上写驱动的习惯是先把寄存器地址、位宽、有效位写进一个头文件HDL和C代码共用同一个定义文件。这样两边改起来才不会出现C里读的是Bit25Verilog里写的其实是Bit24这种低级错误。6.2 用DMA搬运多点坐标单点电阻屏的采样率做到200Hz已经很高了DMA意义不大。但电容屏一旦开始做两点缩放、多点手势每个采样周期要搬好几个点的坐标CPU在中断里逐字节读取会吃掉大量算力。这时候可以考虑让FPGA侧的控制器把一串坐标先写入DDR缓冲区写完再发一个中断让Linux驱动去批量读取。设计思路是FPGA写一段固定大小的环形缓冲区用AXI接口连到DDRLinux驱动通过内存映射看到这块区域。每个样本记录格式固定比如4字节的X加4字节的Y再带一个标志位表示当前批次是否写完。驱动收到中断后把一批新数据一口气搬进input子系统。这个方案比普通中断驱动要稍微复杂但在高采样率触摸场景下是值得的。6.3 绕过input子系统直接给应用层还有一种特立独行的做法如果这是封闭的工控项目触摸数据只给一个自定义应用用没必要走Linux input子系统。你可以把FPGA控制器注册成一个misc字符设备应用层直接open/read/ioctl拿坐标。这样省掉了input子系统的抽象层也绕开了tslib/libinput的校准体系但代价是失去了标准化换应用或换屏都得改代码。我的观点是除非你对整个系统有绝对的控制权并且确实有低延迟、高吞吐的硬性要求否则还是走标准input子系统更划算——因为7寸触摸屏的瓶颈根本不在这层抽象上。最后说一个我一直保留的调试习惯不管问题多离谱先看原始数据再谈优化。触摸屏驱动这条链路从FPGA状态机里的一个采样位到应用层一次手势识别中间隔了五六层。直接在最高层猜问题大概率是在浪费一个下午从底层原始数据一层层往上确认往往十分钟就能把问题定位。7寸触摸屏只是个入口这套排查方法用顺了后面做MIPI触摸、多屏拼接、手势识别都会快很多。