尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ZYNQ下Linux触摸屏驱动实战:从设备树到input子系统

ZYNQ下Linux触摸屏驱动实战:从设备树到input子系统 1. 这块屏到底难在哪先把整体架构捋清楚如果你和我一样之前一直泡在FPGA的世界里天天跟Verilog、时序约束、仿真波形打交道那第一次接到“把7寸触摸屏在Linux下跑起来”这个任务时多半会愣一下。不是说点不亮而是“点亮”和“跑起来”之间隔着一整套陌生的软件栈。先说结论这个任务的核心不是FPGA本身而是Linux下的输入子系统Input Subsystem、设备树Device Tree以及你手里那块触摸屏对应的通信协议。FPGA在这里的角色更像是一个“画面输出设备”加“IO扩展中枢”——屏幕的RGB时序由PL侧产生触摸芯片的数据则通过SPI或I2C总线传给ARM核再由Linux内核里的驱动转换成标准的输入事件最终被Qt、GTK或者任何图形程序消费掉。我用的是ZYNQ平台PS端跑LinuxPL端写了一个简单的LCD时序控制器来驱动7寸RGB屏。触摸部分则根据屏幕型号分了两种方案电阻屏走SPI接口的ADS7846/XPT2046电容屏走I2C接口的GT911。这两种芯片非常典型市面上大部分7寸模组要么是前者要么是后者把这两条路都走通基本就能应对绝大多数项目。适合看这篇文章的主要是两类人一是像我这样从纯FPGA往SoC/Linux方向跨界的工程师想找个完整的驱动案例上手二是做嵌入式Linux开发但之前只弄过按键、LED这类简单字符设备想试试触摸屏这种带中断、带异步上报、还得调坐标映射的输入设备。如果你只想在板子上“能用”那直接启用内核自带的ads7846或goodix驱动就行。但如果你想搞明白每个环节为什么这么设计出了问题知道去哪查那还是得自己把驱动从头到尾写一遍。这篇文章就是按后者来写的。2. 不谈原理写不好驱动触摸屏的硬件细节补课2.1 7寸RGB屏的接口定义和FPGA侧时序先搞清楚你手里的屏幕是什么接口。常见的7寸屏比如800x480或1024x600分辨率的RGB屏接口上通常包含两组信号一组是显示用的RGB数据线和同步信号另一组就是触摸屏的引线。显示部分一般有24根RGB数据线RGB888、VSYNC、HSYNC、DE、PCLK再加背光控制和电源。这部分由FPGA的PL侧负责我用Verilog写了一个简单的时序控制器按屏幕数据手册里的参数产生行场同步和像素时钟。具体参数各家屏幕略有差异但原理都一样本质上就是三个计数器一个数像素行内一个数行帧内一个数帧。像素时钟跟屏幕分辨率、刷新率挂钩比如800x48060Hz典型像素时钟在33MHz左右。这个在FPGA里用PLL锁相环分频产生不是什么难事。真正容易翻车的是触摸部分的接口定义。很多7寸模组的触摸排线是独立的有的是4根线电阻屏有的是4根或6根线电容屏I2C加中断和复位。拿到屏幕第一步别急着接线先找模组厂家要一份接口定义表确认触摸芯片型号。我之前就遇到过排线上写着“TSC”但实际芯片是GT911的情况硬件接错了排查起来特别痛苦。2.2 电阻触摸XPT2046/ADS7846的工作过程电阻屏的原理其实很朴素就是靠压力让上下两层导电膜接触形成一个分压电阻网络。你按的位置不同分压值就不同ADC采出来就是对应的X或Y坐标。所以电阻屏驱动芯片的核心就是一个带触摸检测功能的ADCXPT2046、ADS7846、TSC2046这些芯片功能基本兼容都是12位ADCSPI接口一个PENIRQ引脚用来上报“有触摸”。用SPI读XPT2046的时候主机要发送一个控制字节用来选择转换通道和模式。比如读X坐标通常发0xD012位、差分模式然后连续读两个字节把12位有效数据拼出来。Y坐标同理换一个控制字节。注意这里有个细节XPT2046的SPI是“发一个字节、收一个字节”交替进行的读坐标需要先发命令再读数据CS片选要保持住期间时钟不能断。很多第一次写驱动的人在这里翻车以为像读普通SPI Flash那样连续读就行。还有一个是触摸检测和坐标采样的配合。PENIRQ引脚在无触摸时是高电平有触摸时拉低。所以驱动里可以用这个引脚做外部中断触发中断来了之后启动一次坐标采样。但要注意刚检测到触摸时ADC模拟部分还没稳定最好延时一小段时间再采样或者连续采几次取平均否则读出来的坐标抖动会非常大。2.3 电容触摸GT911的配置流程和点报格式电容屏和电阻屏完全是两个思路。电容屏是通过检测手指触碰时电容变化来定位的触摸感应本身就集成在玻璃面板里外面那颗IC负责扫描、计算坐标然后通过I2C把坐标数据报给主机。以GT911为例这颗芯片在7寸屏上非常常见。I2C地址可以通过外部引脚配置常见的是0x5D或0x147位地址对应到8位传输地址就是左移一位。这个地址在驱动里必须匹配你板子上的实际配置不然i2cdetect扫描不到设备后面全白搭。GT911上电之后有个初始化流程需要操作INT和RST两个引脚。典型时序是上电后先将INT拉低RST拉低保持一段时间然后RST拉高延时后再将INT拉高此时芯片才进入正常工作模式。这个时序是硬性的因为GT911要在复位过程中根据INT引脚的电平来决定I2C地址和通信模式。如果初始化时序不对芯片可能不响应I2C命令或者地址和预期不符。正常工作后GT911会把触摸点数据缓存在内部寄存器里。驱动要做的就是读0x814E这个寄存器拿到当前有效触摸点数然后从0x8150开始读取每个触摸点的数据块。每个点6个字节其中包含12位的X坐标、12位的Y坐标以及触摸面积和触摸ID。这里要特别注意GT911的寄存器地址是16位的Linux标准的i2c_smbus接口只支持单字节命令读不了0x8150这种高地址必须自己用i2c_transfer拼一个两字节地址前缀再读数据。这个坑几乎人人都踩提前说一声能省你半天时间。3. 驱动开发实战手写一个可用的触摸驱动3.1 驱动骨架SPI/I2C设备驱动注册不管屏幕是电阻屏还是电容屏Linux驱动的框架套路是一样的先注册一个SPI或I2C设备驱动在probe函数里做初始化然后注册输入设备最后处理中断、上报坐标。以SPI接口的XPT2046为例核心结构体是spi_driverstatic const struct of_device_id xpt2046_of_match[] { { .compatible mycompany,xpt2046, }, { } }; static struct spi_driver xpt2046_driver { .driver { .name xpt2046, .of_match_table xpt2046_of_match, }, .probe xpt2046_probe, .remove xpt2046_remove, }; module_spi_driver(xpt2046_driver);对应的设备树节点这样写spi0 { status okay; xpt20460 { compatible mycompany,xpt2046; reg 0; spi-max-frequency 2000000; interrupt-parent gpio0; interrupts 20 IRQ_TYPE_EDGE_FALLING; }; };这里有个细节Linux的SPI框架要求片选号通过reg属性指定而且SPI设备的地址是“挂在哪条片选上”的编号不是I2C那种器件地址。spi-max-frequency别给太高XPT2046虽然号称时钟能跑几MHz但实际采样时给太高容易采到毛刺2MHz左右比较稳。I2C的GT911也差不多i2c_driver 设备树节点i2c1 { status okay; gt9115d { compatible mycompany,gt911; reg 0x5d; interrupt-parent gpio0; interrupts 23 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 22 GPIO_ACTIVE_LOW; interrupt-gpios gpio0 23 GPIO_ACTIVE_LOW; }; };i2c_driver的注册代码和SPI版本结构类似区别在于i2c_client的访问方式。这里要记得在probe里用devm_request_threaded_irq申请中断并且用request_threaded_irq的线程化机制来处理后续的I2C读写因为I2C传输是可能睡眠的不能在普通中断上下文里直接调用。3.2 中断处理与坐标上报input子系统核心APILinux把键盘、鼠标、触摸屏这类设备统一抽象成输入设备驱动要做的事情就三件分配input_dev、设置能力位、上报事件。分配和注册的流程是固定的struct input_dev *input devm_input_allocate_device(spi-dev); if (!input) return -ENOMEM; input-name xpt2046 Touchscreen; input-id.bustype BUS_SPI; __set_bit(EV_ABS, input-evbit); input_set_abs_params(input, ABS_X, 0, 4095, 0, 0); input_set_abs_params(input, ABS_Y, 0, 4095, 0, 0); input_set_abs_params(input, ABS_PRESSURE, 0, 255, 0, 0); ret input_register_device(input);注意input_set_abs_params的第三个参数电阻屏12位ADC采样的原始范围是0~4095所以这里设置成4095。后面做坐标映射时再把原始值换算成屏幕分辨率。中断处理这里有两种写法。XPT2046因为在PENIRQ触发时芯片并不缓存坐标数据需要主机主动发起SPI读操作去采样所以比较适合在线程化中断里直接读。GT911则不同芯片内部已经按一定频率扫描出坐标并缓存了驱动只要在中断触发后去寄存器里把坐标取出来即可。以GT911为例中断处理的核心逻辑static irqreturn_t gt911_irq_handler(int irq, void *dev_id) { struct gt911_dev *ts dev_id; u8 buf[6]; int count, i; count gt911_read_reg(ts-client, 0x814E) 0x0F; if (count 0 || count 5) return IRQ_HANDLED; for (i 0; i count; i) { if (gt911_i2c_read(ts-client, 0x8150 i * 6, buf, 6) ! 2) continue; if (buf[0] 0x80) { int x ((buf[2] 0x0F) 8) | buf[1]; int y ((buf[4] 0x0F) 8) | buf[3]; input_report_abs(ts-input, ABS_X, x); input_report_abs(ts-input, ABS_Y, y); input_report_abs(ts-input, ABS_PRESSURE, buf[5]); } } input_sync(ts-input); return IRQ_HANDLED; }这里有个关键点GT911是16位寄存器地址所以gt911_read_reg不能直接用i2c_smbus_read_byte_data得自己封装带两字节地址的i2c_transfer。坐标数据是12位的由高4位和低8位拼接而成。判断触摸是否按下看状态字节的最高位。3.3 坐标映射、旋转、防抖这些绕不开的细节驱动把原始坐标上报出去并不代表工作就结束了。你很快会发现手指在屏幕上横着滑光标却是竖着走的或者点击左上角光标落在右上角。这就是坐标映射和方向问题。坐标映射分两层第一层把ADC原始值换算成屏幕分辨率第二层根据安装方向做翻转或旋转。换算公式就是线性比例比如电阻屏的X轴原始范围是0~4095屏幕宽度是800那么x (x_raw * 800) / 4095; y (y_raw * 480) / 4095;听起来很简单但实际项目里屏幕模组的安装方向五花八门有的朝上有的朝下有的电线朝左。在通用驱动里内核的touchscreen库提供了几个设备树属性来解决这个问题比如touchscreen-inverted-x、touchscreen-inverted-y、touchscreen-swapped-x-y。建议自己写的驱动也读取这几个属性方便后人调整方向而不需要改代码。防抖滤波则是另一件必须做的事尤其是电阻屏。由于ADC采样噪声和接触不稳定单次采样的坐标经常会跳变几格甚至几十个像素。处理办法一般是在驱动里做一个简单的滑动平均比如连续采5次去掉最大值最小值后取平均。电容屏因为芯片内部已经做了滤波驱动里一般不额外处理但注册输入设备时可以把ABS_PRESSURE的fuzz参数设大一点让内核层面帮忙过滤一下抖动。4. 调通全过程的坑与排查方法4.1 中断来了但读不到坐标SPI时序配置错了这是电阻屏驱动最经典的故障现象用手点屏幕cat /proc/interrupts能看到中断号在增长但坐标数据读出来全是0xFF或者乱码。用示波器抓SPI引脚波形看着也对命令发出去了MISO上也有数据但解析出来不对。问题多半出在SPI模式上。XPT2046要求的是SPI Mode 0也就是CPOL0、CPHA0。Linux的spi_device结构体里通过mode成员配置你如果设备树里spi-max-frequency写太高或者驱动里mode设置不对特别是CPHA配置反了采样时序就会错位读出来永远是错的数据。我当时排查这个问题的过程也挺曲折。一开始以为是FPGA里的SPI控制器有问题后来用逻辑分析仪抓波形对比芯片手册上的时序图发现命令字节确实发出去了但是MISO上返回的数据比预期晚了一个时钟周期。就是因为CPHA配置不对。把mode改成SPI_MODE_0之后一次通过读出来的坐标非常稳定。还有一个隐藏的坑XPT2046在刚上电或者长时间没有触摸时模拟部分会进入休眠省电状态。中断触发后立刻发起SPI读操作有可能因为内部还没来得及唤醒而读到错误数据。稳妥的做法是在probe函数里先做几次“空的转换”把ADC唤醒之后再用或者每次中断后加一小段延时再采样。4.2 触摸点跟屏幕显示错位、方向反转这个现象在GT911电容屏上更容易遇到。显示明明是正常的触摸也响应但点屏幕的左边鼠标出现在右边点上面光标出现在下面。这种情况基本可以断定是坐标系方向不一致。Linux里有一个坐标系约定输入设备的坐标原点在屏幕左上角X向右递增Y向下递增。但触摸屏模组安装后物理坐标的初始方向未必和屏幕显示方向对齐可能旋转180度也可能X和Y互换了。排查步骤很简单先用evtest看事件上报的原始值然后做一个系统的坐标检测依次点击屏幕的四个角和中心点看原始坐标值的递增方向跟屏幕方向的关系。比如我点左上角原始值显示(X4000, Y4000)点右下角显示(X100, Y100)那说明两个轴都反了。这就在设备树里加上touchscreen-inverted-x和touchscreen-inverted-y。还有一种更隐蔽的情况X轴反了而且X和Y也互换了。比如屏幕横屏安装但触摸芯片的物理X轴恰好对应屏幕的显示Y轴。这个用设备树属性同样能处理kernel的touchscreen_parse_properties函数已经把磁带翻转涉x-y互换的解析做进去了。4.3 点击没反应问题可能出在设备树和事件节点如果中断也不触发事件节点也没生成那问题可能不在驱动代码本身而是设备树里中断资源没配好。最常见的是interrupt-parent指向了错误的GPIO控制器。ZYNQ平台上PS侧的MIO和通过EMIO扩展的PL引脚GPIO控制器的实例编号不同中断号也不同。设备树里写错一个数字中断子系统匹配不到对应的GPIOprobe函数就会失败。排查思路是这样先看内核日志dmesg里有没有我们这个驱动的报错信息。再看/sys/bus/i2c/devices/或/sys/bus/spi/devices/下有没有对应的设备文件夹。如果有设备但没中断多半是interrupts属性里的GPIO编号跟实际接线不符。另外一个小技巧是在probe函数和中断处理函数里都加上dev_info或dev_dbg打印。模块加载的时候看probe有没有被执行中断触发的时候看handler有没有被调用。这样能把问题快速定位到“驱动没加载”还是“中断没触发”还是“中断触发了但数据不对”这三个阶段之一。4.4 电容屏第一次上电没反应复位时序是关键GT911这种芯片和传统的I2C传感器还不一样它在上电后需要主机配合初始化时序芯片才会正常响应I2C命令。如果你的驱动probe里直接就去读寄存器大概率读回来的全是0xFFi2cdetect也扫描不到设备。正确的上电流程应该是先给屏幕供上电延时一段时间等电源稳定然后拉低INT引脚再拉低RST引脚保持至少10ms然后把RST拉高再延时10ms最后把INT拉高。完成这个时序之后GT911才会切换到正常工作模式I2C地址也才会生效。这里有一个特别容易踩的坑GT911有两个可选I2C地址通过复位时INT引脚的电平状态来决定。INT拉低复位地址是0x5DINT拉高复位地址是0x14。很多初始化代码先拉高INT再复位结果地址就变成0x14了和你设备树里写的0x5D对不上自然怎么都探测不到。我自己的做法是在复位流程里把INT和RST都明确地拉低确保地址稳定在预设值。5. 实测调通的完整流程从设备树到事件上报5.1 触摸和显示联调先上设备树下面以我手头的一块ZYNQ板子为例记录一次完整的调通过程。屏幕是7寸800x480的RGB电容屏触摸芯片是GT911I2C挂在PS端的I2C1上中断引脚接到PL侧的GPIO。第一步先在设备树里把I2C节点和GT911节点配上。需要注意ZYNQ的I2C控制器和设备树节点的对应关系要查清楚I2C1对应的是i2c1里面的时钟频率配置也要和实际硬件匹配。i2c1 { status okay; clock-frequency 100000; gt9115d { compatible mycompany,gt911; reg 0x5d; interrupt-parent gpio0; interrupts 23 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 22 GPIO_ACTIVE_LOW; interrupt-gpios gpio0 23 GPIO_ACTIVE_LOW; touchscreen-size-x 800; touchscreen-size-y 480; }; };设备树编译、烧录、启动之后先别急着加载驱动用i2cdetect确认芯片在总线上能被扫描到。扫描结果里能看到5d这个地址的设备说明芯片上电成功初始化时序对了地址也对了。5.2 驱动模块加载与事件节点验证驱动编译成内核模块insmod加载然后看dmesg输出。正常情况probe会被调用打印出我们加的调试信息然后注册输入设备生成/dev/input/event0节点。用hexdump验证数据是接下来最直接的一步hexdump /dev/input/event0手指点在屏幕上移动终端里应该会不停地刷出事件数据。每个输入事件是struct input_event结构体16字节里面有类型、编码、数值。看到EV_ABS和ABS_X/ABS_Y事件说明驱动从底层到上层已经打通了。如果hexdump有输出但evtest显示的坐标范围不对那就需要在驱动里重新检查坐标换算。我之前踩过这种坑GT911内部报告的坐标范围其实是通过配置寄存器设置的默认可能是1024x600或者别的值和屏幕的800x480不一致。这时候要么在驱动里做换算要么在初始化GT911时往它的配置寄存器里写入正确的坐标上限让它输出的原始值本来就落在0~800和0~480范围。5.3 用evtest做最终验证等hexdump确认事件在流动再用evtest做UI层面的验证。evtest会显示每次事件的详细信息包括类型、编码、值还能看到ABS_X的range是多少。我通常会在屏幕上画一个十字线然后依次点击中心和四个角看上报的坐标是否符合预期。如果中心点上报的值大约是(400, 240)四个角的数值也都对得上那驱动这一层的坐标映射就算是完全没问题了。最后一步接上Qt或者GTK应用用滑动手势测试流畅度。这一步主要感受触摸事件的连续性如果光标移动一顿一顿的多半是中断频繁且每次只上报了一次坐标可以在中断处理里把芯片缓存的所有点都读出来再上报或者提高芯片内部扫描频率。GT911这个芯片本身就有扫描缓存功能即使中断上报频率不高坐标数据也应该是连续的。6. 几个能直接用的调试命令和真实心得调试触摸驱动有一些命令是高频使用的我整理一下方便你直接抄走i2cdetect -y -r 1扫描I2C总线1上的设备地址GT911没扫描到先检查复位时序和地址配置。cat /proc/interrupts | grep gpio确认触摸中断有没有触发。如果不增长检查设备树的中断引脚配置或者用GPIO调试接口确认引脚状态。evtest /dev/input/event0查看事件上报的详细内容坐标范围对不对、压力值有没有一眼就能看出来。hexdump /dev/input/event0轻量验证适合在没有evtest的板子上快速确认事件是否在流动。dmesg | grep gt911驱动自身的打印信息probe成功没、中断申请成功没都有记录。还有一些日常文档里不太会写的经验我觉得比命令本身更值钱。第一驱动代码里一定要加probe成功、中断触发、坐标读取三个关键节点的调试打印。交付之前可以关掉但开发阶段不要嫌log多。触摸驱动不像纯逻辑代码你没法单步调试打印就是唯一的眼睛。第二示波器或者逻辑分析仪是必需品但别一上来就抓时序。先确认软件层面的寄存器读写正常没有再确认中断有没有触发最后才上示波器看波形。除非你怀疑硬件连接有问题否则直接抓波形往往事倍功半。第三FPGALinux的调试和纯FPGA调试有个很大的心理差异。FPGA里出了问题第一反应是看RTL逻辑哪里写错了Linux系统里出了问题第一反应应该变成了看设备树、看内核日志、看中断有没有触发。这个思维转换非常重要。我见过不少FPGA工程师转做Linux驱动时还在用仿真的思维去排查驱动问题结果陷入死胡同。驱动开发的核心是“看系统实际状态”不是“推演逻辑”。第四如果发现坐标有固定偏移比如触摸点整体偏右下但方向和大小都对那大概率不是算法问题而是屏幕的触摸有效区域和显示区域的物理偏差。这种问题在批量生产的模组上比较常见可以在用户空间做一个校准四角采样后生成校准参数比在驱动里硬编码更灵活。最后再说一个容易忽略的点VCC供电。7寸屏幕的触摸芯片供电一般是3.3V或者2.8V如果供电电压不足或者纹波大触摸坐标会出现随机跳变而且是在整个屏幕上无规律跳看起来特别像软件滤波没做好。排查这种问题用示波器看触摸芯片供电引脚的纹波比调代码快得多。我就遇到过一块屏幕触摸不稳折腾两天滤波算法都没用最后发现是背光供电和触摸供电共用一个LDO背光一亮就拉低触摸电压造成干扰。把这个供电拆开之后问题彻底消失。触摸驱动的开发说到底是把物理世界的“按”和数字世界的“坐标”连接起来。摸清原理、看懂时序、会查日志剩下的就是耐心和细心。
返回列表