拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA+Linux嵌入式开发:7寸GT911触摸屏驱动从设备树到调试全解析

做嵌入式开发这几年我接触过的显示方案不少但真正把FPGA、Linux和触摸屏驱动这几件事串起来做项目还是有点挑战的。很多做FPGA出身的朋友平时写惯了Verilog、调惯了时序一碰到Linux驱动就有点挠头反过来搞Linux应用开发的工程师看到FPGA那边的约束、引脚分配、时序参数也觉得像天书。黑金云课堂这套FPGA技术教程里7寸触摸屏驱动这一节恰好就是把两边的知识打通了。这篇文章我打算沿着项目实际推进的路线把从硬件接口分析、设备树配置到驱动代码实现、再到调试验证的完整过程拿出来聊聊既讲清楚每一步为什么这么做也把我踩过的坑、排除过的故障一并列出来给准备在FPGALinux平台上做显示交互的朋友当一份实战参考。1. 项目整体思路与方案选型1.1 硬件平台FPGAARM的异构架构为什么常见这类7寸触摸屏项目通常不是“纯FPGA”能搞定的。你想想看FPGA擅长的是并行数据流处理、高速接口扩展、时序逻辑控制但要跑Linux系统、管理文件系统、跑网络协议栈、做复杂的GUI界面它就不如ARM处理器顺手。所以市面上绝大多数FPGA视频开发板、显示交互方案走的是异构SoC架构——FPGA内部集成了一颗ARM硬核或者FPGA外挂一颗ARM处理器两者通过AXI总线、GPIO或者专用接口通信。我用过的比较典型的平台是Xilinx Zynq系列比如Zynq-7000芯片内部有双核Cortex-A9再加上一片可编程逻辑。这个架构的好处在于Linux跑在ARM核上触摸屏驱动、GUI应用都在这一侧而FPGA逻辑侧可以用来做图像采集、预处理、视频时序生成、MIPI或者RGB接口的适配。换句话说在Zynq平台上做7寸屏显示实际上是一个软硬件协同的系统工程FPGA负责生成RGB像素时序把画面推给屏ARM核负责跑Linux、跑触摸驱动、跑交互程序两者之间通过VDMA、AXI4-Stream之类的IP核把图像数据从DDR搬到显示控制器。这样一来触摸屏驱动的工作就落在Linux侧了但你必须对整个数据通路有概念否则出了问题都不知道该查哪边。1.2 触摸屏方案选型电容屏和电阻屏的取舍7寸触摸屏本身分两大类电阻屏和电容屏。这一点在项目一开始就得定下来因为后续的驱动开发路径完全不同。电阻屏结构简单、成本低、支持用指甲/手套操作但它需要压力触发多点触摸基本不支持而且表面容易被划伤电容屏支持多点触控响应灵敏表面是玻璃材质更耐用但在工业环境里戴厚手套操作会比较别扭成本也要高一点。从开发角度看二者的驱动差异非常大。老式的电阻触摸屏驱动走的是ADC采样加GPIO控制很多芯片是SPI接口内核里有现成的ads7846驱动参考代码原理是通过检测电压变化计算出触点坐标还要做校准把原始的ADC值映射到屏幕像素坐标。电容屏则普遍走I2C接口芯片内部自带触摸检测和坐标计算能力驱动要做的主要工作是初始化、配置、通过中断通知读取触摸数据。市面上常见的电容触摸控制IC有GT911、FT5x06、FT5x36、Goodix的GT系列、海栎创的CST系列等我这次选的是GT911因为它在国产7寸屏模组里出镜率很高资料也比较多很多屏厂直接集成好排线接口拿过来焊上就能用。1.3 屏幕接口与触摸接口是两套独立系统新手最容易搞混的一个点是屏幕显示是一套接口触摸是一套完全独立的接口。7寸屏通常用的是RGB888或者RGB565并行接口外加行场同步、像素时钟、数据使能这组信号是接在FPGA的IO引脚上的由FPGA逻辑负责产生时序。触摸功能则是另外一组引脚GT911这类芯片用I2C通信至少要接SDA、SCL两根数据线再加INT中断输出和RESET复位控制触摸数据就是通过这两根线跟ARM/Linux交换的。理解这个分离很重要。排错的时候显示画面异常比如花屏、闪烁、偏色那是RGB信号通路和FPGA时序的问题而触摸没反应、坐标乱跳、只能摸不能点那是I2C和驱动的问题。两条链路可以分开排查千万别一上来就去翻FPGA的时序约束文件结果发现触摸芯片根本没被Linux枚举到。2. 硬件接口分析与设备树配置2.1 RGB屏接口时序不要把时序参数理解得太神秘驱动触摸之前得先确保屏幕能亮。在Zynq平台上通常的做法是在FPGA侧例化一个Video Timing Controller核配置好分辨率和刷新率生成像素时钟、行同步、场同步、数据使能这几个信号再把RGB数据总线跟屏模组的对应引脚连起来。7寸屏模组常见的分辨率有1024x600和800x480我这次用的是1024x60060Hz刷新像素时钟大概在51.2MHz左右。这个数值不是随便拍的可以按公式自己算一下像素时钟约等于行像素总数乘以帧行总数乘以刷新率。1024x600屏行总数里要加上行消隐时间比如208帧行总数加消隐后大概是635行左右那就是(1024208)x(60035)x60约等于51.2MHz跟LCD控制器手册里给的推荐值基本吻合。FPGA侧VTC参数跟屏模组规格书里的时序表对齐以后显示通路就通了。但这里有个很关键的细节RGB数据线不是简单接上就完事还需要关注IO标准、电压域和引脚约束。7寸屏的RGB接口电压一般有3.3V和5V两种FPGA的Bank电压必须匹配不然信号电平不对轻则显示发暗、重则直接把FPGA或屏烧掉。我在做引脚约束时把RGB数据线全部约束在同一个Bank同时把LVCMOS33设为IO标准这步不能偷懒。2.2 GT911触摸芯片的I2C通信分析GT911是汇顶科技出的一款电容触摸控制IC支持5点或者10点触控内置电容检测矩阵自动完成触摸检测、坐标计算、滤波处理主控只需要通过I2C读取它的坐标数据就行。GT911的I2C地址是0x5D或者0x14具体由芯片的ADDR引脚电平决定当ADDR引脚拉高时是0x5D拉低时是0x14。这里有个反直觉的点GT911的上电之后有两种工作模式一种是正常模式芯片内部做触摸检测并等待主控读取另一种是配置模式芯片内部的寄存器可以被覆写用来配置灵敏度、触摸按键映射等参数。驱动里最重要的不是读坐标而是正确的复位时序。GT911手册明确要求上电后需要先拉低RESET保持至少1ms再拉高RESET然后等待芯片内部初始化完成至少等50ms之后再开始通过I2C读取。如果复位时序太急促芯片可能一直不响应I2C请求驱动里读不到任何数据。这个问题在实际中非常常见我遇到过不止一次一开始总怀疑I2C线路接错了后来用示波器看RESET时序才发现是上电后拉高复位线拉得太快芯片还没准备好。2.3 设备树节点把硬件连接信息告诉Linux在Zynq这类设备上跑Linux硬件连接信息是通过设备树Device Tree传递给内核的。触摸驱动是I2C客户端驱动所以你需要在设备树里找到I2C控制器的节点然后在它下面添加一个子节点描述这颗GT911芯片的挂接位置、中断引脚、复位引脚、供电等信息。设备树里没有直接控制硬件的代码它只是描述硬件拓扑的配置文件驱动会通过它拿到自己需要的资源。我先说我用的设备树片段核心节点大致是这个样子i2c0 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c0_default; gt911: gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts 18 IRQ_TYPE_EDGE_FALLING; irq-gpios gpio0 18 GPIO_ACTIVE_HIGH; reset-gpios gpio0 20 GPIO_ACTIVE_HIGH; touchscreen-size-x 1024; touchscreen-size-y 600; touchscreen-max-id 5; touchscreen-inverted-x 0; touchscreen-inverted-y 0; }; };这里有两个点值得多说一句。第一reg 0x5d这个地址必须与芯片的ADDR引脚实际接法对应如果硬件上ADDR引脚拉低了这个就要改成0x14否则驱动probe的时候根本匹配不上。第二interrupts里的中断号和irq-gpios里的GPIO号指的是同一个物理引脚但用的是两个不同的软件描述维度。GPIO号是告诉系统“这根引脚在哪个GPIO控制器下的第几号”中断号是告诉系统“这个中断请求线接到哪个中断控制器”。不同的芯片平台这两个编号体系不一样对Zynq来说GPIO0控制器的18号引脚对应的是哪个MIO需要查芯片手册确认不像树莓派那种通用Linux头文件那么直观。写错了中断就没法触发触摸数据永远不会被读取。2.4 设备树编译与加载的注意事项设备树写完不是直接生效的需要编译成dtb文件跟内核镜像一起打包。开发阶段最简单的方式是打开U-Boot在启动参数里指定fdt_addr或者让U-Boot自动读取指定分区的设备树然后覆盖boot分区里的dtb文件。我在开发板上实验时通常通过tftp或者SD卡直接更换dtb不用重新编译内核对迭代效率高非常多。调试设备树有一个非常实用的技巧在内核启动参数里加上printk.devkmsgon然后在U-Boot环境下用fdt print查看当前加载的设备树是否包含触摸节点。如果fdt print看不到节点说明加载的根本不是你修改过的设备树可能是U-Boot环境变量指定的dtb路径错了或者是设备树编译的时候include了错误的dtsi直接去改设备树是没用的。这种问题我碰到过两次每次都是先怀疑驱动代码最后发现是系统压根没加载新设备树。3. 驱动开发从零写GT911触摸驱动3.1 先理解Linux输入子系统再写驱动代码Linux内核把所有输入设备——键盘、鼠标、触摸屏、遥控器——都抽象成输入子系统Input Subsystem。驱动要做的事情本质上就是把硬件上报的数据变成标准化的input事件用户空间程序通过/dev/input/eventX文件读取这些事件即可。理解这个框架就不容易把触摸驱动想得太玄乎。GT911触摸芯片芯片本身已经做好了触摸检测和坐标计算Linux驱动的任务很清晰初始化I2C通信、配置芯片参数、在中断触发时通过I2C读取坐标数据、调用input子系统接口上报给用户空间。触摸屏坐标系和屏幕像素坐标系的关系需要在上报前处理好。GT911上报的X、Y坐标范围一般跟触摸屏的分辨率设置有关比如我在设备树里设置touchscreen-size-x 1024、touchscreen-size-y 600这样驱动上报的就是0~1023、0~599之间的值。如果触摸方向和屏幕显示方向不一致可以在驱动里做坐标变换也可以在设备树里直接声明映射关系但前提是驱动代码要支持这些设备树属性否则写了也白写。我见过很多移植驱动的朋友代码里写死了坐标范围导致屏换一个尺寸就要改代码这是非常糟糕的工程习惯。3.2 I2C驱动框架probe、remove、suspend/resumeLinux 驱动模型下I2C 驱动通过i2c_driver结构体注册。驱动代码的入口也就是这个结构体里的probe函数当 I2C 总线上出现一个设备且设备树里 compatible 属性和驱动里的id_table匹配上时内核就会调用probe。我写的驱动里probe函数最关键的事件就是复位芯片、等待芯片就绪、注册输入设备、注册中断处理函数。一个简化但逻辑完整的probe函数流程先是这样的获取 I2C 客户端结构体保存到一个私有数据结构里通过devm_gpiod_get获取复位 GPIO拉低、延时、拉高完成复位序列延时 50ms等待芯片内部初始化通过 I2C 读取芯片 ID 寄存器确认芯片通信正常这一步是调试时最有效的探测点初始化 input_dev设置EV_KEY、EV_ABS事件位通过input_set_abs_params设置 X/Y 轴范围和压力值范围注册输入设备通过devm_request_threaded_irq注册中断回调注意这里要用线程化中断因为 I2C 读取本身可能阻塞不能在硬中断上下文里做remove函数相对简单重点是注销输入设备、释放中断、关闭芯片和清理私有数据。但实际项目里remove很少被单独调用除非你写的是可卸载模块开发阶段为了快速迭代我通常把驱动编成模块需要更新的时候直接rmmod再insmod这样省去了整个内核重新编译的时间。3.3 中断处理与坐标上报最核心的数据通路触摸屏的坐标数据不是在驱动里轮询出来的而是靠中断通知的。GT911的INT引脚在芯片检测到触摸时会输出一个下降沿或低电平信号触发Linux里的中断回调。这里有个并发问题触摸屏是持续工作的中断频率可能很高而I2C读取本身需要时间如果在中断里直接做I2C读写容易丢失数据所以要区分硬中断和线程化中断。我在代码里用的是request_threaded_irq并传入IRQF_TRIGGER_FALLING | IRQF_ONESHOT标志。使用IRQF_ONESHOT可以保证在中断线程执行完毕之前不会重复触发同一个中断。这是防止中断风暴的标准做法尤其是在I2C读取延迟比较大的场景下尤为重要。中断线程里做的事情可以理解为三步通过I2C读取GT911的状态寄存器判断到底是“有点按”还是“抬起”如果有点按就读取触摸点坐标寄存器包括X坐标、Y坐标和压力值调用input_report_abs和input_sync上报事件这里我遇到过一个问题GT911触摸上报的数据格式不是一次I2C读就能全读出来的它有多个坐标寄存器需要按固定偏移连续读取。而且一个中断可能携带若干个触摸点的数据需要根据状态寄存器里的触摸点数循环读取。如果循环次数和寄存器偏移没对齐就会出现触摸点坐标错乱、漂移的现象。解决这个问题的关键就是仔细对照GT911的寄存器手册把各触摸点的坐标偏移量算准调试时打印原始寄存器值跟实际触摸位置一比马上就能发现规律。3.4 驱动代码骨架核心函数和数据结构我贴一段精简后的核心驱动代码骨架目的是把逻辑脉络讲清楚实际项目里还会有更细致的错误处理struct gt911_data { struct i2c_client *client; struct input_dev *input; struct gpio_desc *reset_gpio; struct gpio_desc *irq_gpio; struct mutex lock; u16 max_x; u16 max_y; }; static irqreturn_t gt911_irq_handler(int irq, void *dev_id) { struct gt911_data *ts dev_id; struct device *dev ts-client-dev; u8 buf[5]; int ret; u16 x, y; u8 status; ret i2c_smbus_read_byte_data(ts-client, GT911_REG_STATUS); if (ret 0) { dev_err(dev, failed to read status: %d\n, ret); return IRQ_HANDLED; } status ret 0x0f; if (status 0) { input_sync(ts-input); return IRQ_HANDLED; } /* 读取第一个触摸点的坐标buf依次为状态、xh、xl、yh、yl */ ret i2c_master_recv(ts-client, buf, sizeof(buf)); if (ret 0) { dev_err(dev, failed to read touch data: %d\n, ret); return IRQ_HANDLED; } x ((buf[2] 0x0f) 8) | buf[1]; y ((buf[4] 0x0f) 8) | buf[3]; input_report_key(ts-input, BTN_TOUCH, 1); input_report_abs(ts-input, ABS_X, x); input_report_abs(ts-input, ABS_Y, y); input_sync(ts-input); return IRQ_HANDLED; }这段代码里最容易被忽视的点是buf[2] 0x0f为什么坐标高位只用低4位因为GT911的X/Y坐标都设计为12位高字节只有低4位有效。如果不去掩码把高字节整个赋值过去坐标值会整体偏移一个倍数触摸点对应的实际位置就会出现肉眼可见的漂移。这类细节没有寄存器手册对照纯粹靠黑盒调是调不出来的。4. 调试、验证与常见问题排查4.1 从内核日志和设备树两条线确认驱动加载驱动写好之后第一步不是去碰触摸屏而是确认驱动有没有被内核正常加载。加载成功之后dmesg里会出现类似input: Goodix GT911 Touchscreen as /devices/platform/amba/amba:fpga0/i2c0/i2c-0/0-005d/input/input0这样的日志。如果probe函数里加了调试打印还会看到I2C通信成功、ID寄存器读取正常这些信息。如果dmesg里没有相关日志先检查驱动有没有真正进到probe里。在开发阶段我经常在probe函数的入口直接加一行dev_info打印这行日志能直接区分“驱动根本没匹配上”和“匹配上了但初始化失败”。匹配不上的原因九成是设备树里compatible字符串跟驱动id_table不一致或者I2C总线号不对这都可以通过ls /sys/bus/i2c/devices/来确认总线上有没有挂载到gt911设备节点。反过来如果probe里初始化失败比如I2C读ID失败那多半是硬件层面的问题复位时序不对、I2C上拉电阻缺失、地址不对、引脚约束错位。先解决这些再往下走不要先去调坐标和灵敏度。4.2 用hexdump和getevent验证触摸数据驱动加载成功不代表触摸功能完全可用需要通过用户空间工具验证事件链路。最直接的方式是查看触摸对应的输入设备节点是哪个然后用hexdump读它$ cat /proc/bus/input/devices $ hexdump /dev/input/event1正常情况下触摸屏幕时终端里会持续刷出类似0004 0003 000000d8 00000064这样的十六进制事件记录。这些字段分别对应事件类型、事件码、事件值你不需要完全读懂但能看到事件流在持续产生就说明驱动到底层硬件的数据链路是通的。更友好的方式是getevent工具$ getevent -lt /dev/input/event1它会把事件解析成可读性更好的格式显示类型为ABS_MT_POSITION_X或者ABS_X这些字段。这里有个经验点如果你用hexdump能刷出事件但GUI程序触摸没反应可能不是驱动问题而是你的应用读取的输入设备节点选错了。多块触控设备混在一起时/dev/input/eventX的枚举顺序不一定固定需要靠/dev/input/by-path/或者通过EVIOCGNAMEioctl读取设备名来区分。4.3 坐标方向不对、触摸乱跳、没反应的排查顺序触摸屏显示方向跟屏幕物理方向不一致这个几乎是必现的问题。排在前面的原因一般是屏幕本身的装配方向、触控面板的原点和RGB扫描顺序之间不匹配。简单处理方式是在设备树里配置touchscreen-inverted-x、touchscreen-inverted-y或者在驱动里做坐标变换。但这里有个关键坐标变换要在上报之前做而且最好跟显示控制器那边的水平/垂直扫描方向保持一致否则图像和触摸方向之间可能出现“上下左右交叉错乱”这种很隐蔽的问题。触摸点乱跳、断线通常是VREF、电容检测参数和屏的物理环境不匹配导致的。GT911有一个优点是有较成熟的配置寄存器可以调中断触发方式、滤波系数、触摸阈值等。驱动的调试阶段我习惯先把上报的原始数据打印出来观察触摸移动时坐标的变化规律。如果坐标只是轻微抖动一般是滤波参数问题如果出现坐标跳变到完全不对的位置那多半是坐标解析出了问题特别是高字节没做掩码、触摸点索引没对齐这种需要回到寄存器定义逐位核对。如果完全没触摸事件排查顺序我给一个自己常用的清单先确认设备树里interrupts配置正确中断号可以通过cat /proc/interrupts查看触发中断后计数是否增加再确认I2C通信有没有数据返回可以用i2c-tools里的i2cdetect扫描总线看0x5d地址是否存在最后用示波器量INT引脚确认芯片在有触摸时真的拉低了。按照这个顺序排查能过滤掉绝大多数表面现象直接找到根因。4.4 触摸屏与FPGA图像通路的配合问题触摸驱动调通了以后还有一个“整体联动”的问题触摸坐标最终要映射到GUI界面上而GUI画面是FPGA通过VDMA从DDR里取图像生成的这中间存在一个坐标系对应关系。很多项目里触摸屏坐标范围跟实际显示分辨率不是严格一致的比如屏是1024x600但触摸芯片原始范围是4095x4095驱动里没做范围映射就会导致触摸和显示位置偏移。用input_set_abs_params把ABS_X、ABS_Y的范围设置成和屏分辨率一致能在内核侧就解决大部分偏移。从Zynq平台的开发节奏来说我强烈建议把FPGA显示通路和触摸驱动分开验收。先用一个纯色测试画面跑通显示再用触摸测试工具验证坐标最后才把两个功能合在一起做联调。很多项目组一上来就烧一个大的完整工程结果显示和触摸同时出问题排查难度成倍上涨。分开验证一旦出问题马上能缩小范围到FPGA侧还是Linux侧省下的调试时间非常可观。5. 实操心得与扩展方向5.1 关于调试工具和操作方法上的几点心得做这类FPGALinux驱动开发实验室里至少要有示波器、逻辑分析仪、串口终端这几样工具缺一不可。示波器主要用来验证I2C时序和中断引脚波形逻辑分析仪可以同时抓多路信号排查时序对齐问题非常方便串口终端则是Linux日志输出的主通道。我调试GT911的时候发现中断引脚波形宽度很窄用示波器单次触发抓了好几次才抓到后来换了逻辑分析仪连续采样一下就定位了问题。软件层面i2cdetect、i2cget、i2cset这几个命令是排查触摸芯片的利器。我经常在驱动还没有完全写好时先用i2cget直接读芯片的ID寄存器确认I2C通信物理层是通的再开始写驱动。这一步能帮你把“通信问题”和“逻辑问题”隔离出来省下大量盲调时间。5.2 从7寸触摸屏驱动延展出去的几个方向7寸触摸屏驱动跑通以后后面可以做的事情很多。一个方向是在FPGA侧接入摄像头或图像处理IP核通过VDMA把视频流渲染到屏幕上实现实时显示和触摸交互这在家电面板、工业HMI、医疗设备这类场景里很常见。另一个方向是做多屏拼接、多触摸点协同需要把触摸驱动从单点改造成多点上报用TYPE_B协议上报协议处理多个触点。GT911本身支持5点或者10点触控驱动扩展起来并不难难的是应用层如何融合多点手势比如缩放、旋转、滑动。做嵌入式Linux底层的开发就是这样单独看一个触摸屏驱动不难但把它放在一个FPGALinux的完整工程里任何一个环节掉链子都会影响整体交付。我自己最大的体会是先把系统分层理解透FPGA那边管时序和像素Linux这边管外设和交互中间靠设备树和总线把两边接起来每个层的调试都独立验证最后联调的时候心里才有底。如果一开始就没有这个全局观很容易陷入“改一行FPGA代码又去改一下驱动再跑一遍看看”的反复循环中走了弯路还不自知。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门