ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA上跑Linux调通7寸触摸屏驱动:从设备树到坐标校准全流程

FPGA上跑Linux调通7寸触摸屏驱动:从设备树到坐标校准全流程 做FPGA的工程师常年跟Verilog和时序图打交道突然有一天要在Zynq上跑Linux还得去调一个7寸触摸屏驱动第一反应往往是这活不是嵌入式软件兄弟干的吗但实际做过一次就知道FPGALinux的组合里屏幕能不能亮、触摸能不能点准最后全落在设备树和驱动上而这两样东西又和你的PL端逻辑紧密耦合。黑金这块板子我调了挺久从没反应到一摸一个准中间踩过的坑值得拿出来聊聊。1. 项目背景与方案选型1.1 为什么要在FPGA里跑Linux做触摸屏先说一个最直观的问题触摸屏驱动这事儿和FPGA有什么关系很多刚入门的朋友以为FPGA就是写Verilog点灯、做图像处理、写状态机碰到Linux就懵了。但实际上以Xilinx Zynq为代表的SoC平台本身就集成了ARM核PL端和PS端之间通过AXI总线挂着你完全可以在ARM上跑Linux再通过设备树和驱动去控制PS端的I2C控制器从而读取触摸屏的坐标数据。我用的开发板是黑金的AX7020芯片是Zynq-7020。这个平台上跑Linux做驱动开发最大的好处是生态成熟内核源码、交叉编译工具链、Petalinux工具链都能直接用触摸屏驱动不用自己去扒寄存器手册而是走Linux标准的input子系统通过/dev/input/eventX节点把触摸事件交给上层应用。整个开发流程是“硬件连接 - 设备树描述 - 驱动加载 - 事件上报”这个链路在FPGA平台跑通了后面再去做图像采集、DMA传输、MIPI转接之类的功能思路就完全打开了。1.2 7寸触摸屏驱动的主流方案市面上7寸屏配触摸方式很多但整理下来核心就三大类电容式I2C接口触摸屏、电阻式SPI/ADC触摸屏、红外框式触摸屏。我这次做的是最常见的电容触摸后端挂在I2C总线上芯片型号很多GT811、GT9147、FT5x06系列都很常见接口就四根线SDA、SCL、INT中断、RST复位。选型上看I2C电容屏优先级最高。原因很简单Linux内核里已经有相当完整的input子系统框架你只要写好一个I2C客户端驱动在探测函数里拿到触摸芯片的坐标数据通过input_report_abs和input_sync上报系统立刻就能识别成一个触摸设备。相比之下红外触摸屏比如touchkit这种走的往往是串口或者USB虽然内核里也有对应的Input驱动但很多时候要对着协议手册一个个字节去解析工作量明显大不少。如果只是想把一个7寸屏用起来I2C电容方案是性价比最高的一条路。2. 硬件接口分析与开发板准备2.1 7寸屏的接口组成RGB信号和I2C触摸通道这里得先说清楚一件事7寸RGB屏显示和触摸其实是两套完全独立的通路。显示部分的RGB信号、时钟、行场同步、使能信号是从FPGA的PL端IO出来的一般Vivado里会做一个LCD控制器IP再配合时序参数把画面输出到屏幕上。触摸部分则完全靠触摸芯片自己工作通过I2C读到坐标。我手里的屏是黑金配套的7寸电容触摸屏颜色格式RGB888分辨率1024x600。屏幕的接口是一个40Pin的FPC排线里面有RGB数据线、背光控制还有一组I2C和电源用来给触摸芯片供电和传输数据。这组I2C触摸引脚在硬件设计上是接到了PS端的I2C控制器上也就是MIO引脚。这个设计对我来说是天大的好事因为直接用PS端I2C驱动在内核里就能挂上不用额外写PL端的I2C控制器去和ARM互通。2.2 中断和复位引脚怎么安排I2C之外还有两个引脚必须搞清楚INT和RST。INT是触摸芯片的中断输出当手指触摸屏幕时芯片通过INT引脚拉低告诉处理器“有新数据可以读了”。这个引脚我接到了PS端的MIO中断上在设备树里用interrupts属性描述。为什么不用轮询因为轮询I2C会白白消耗CPU而且Linux内核对于input设备的中断上报处理非常成熟中断驱动能保证触摸事件低延迟。RST用于复位芯片上电时拉低再拉高确保触摸芯片进入正常工作状态。这里注意一个坑复位引脚的极性要看芯片手册有的是低有效复位有的是高有效复位接反了芯片干脆没反应。2.3 开发环境准备Petalinux还是Ubuntu环境上两种路子我都试过。一种是直接用Vivado里的Petalinux工具它能根据你的硬件配置生成完整的Linux镜像设备树、内核、根文件系统都给你打好包很适合量产和快速验证。另一种是在板子上跑Ubuntu根文件系统然后单独编译内核模块插入这种方式更适合驱动调试因为改一个模块不用重新打包整个镜像。我最终用的是“Petalinux做基础镜像 单独编译驱动模块”的混合模式。先在Vivado里把PS端的I2C控制器和中断配置好导出硬件描述文件XSA然后用Petalinux petalinux-config命令配置内核把I2C支持编进去再把设备树写对最后在内核源码树里单独编译触摸驱动模块。这套流程熟练之后整个调试周期能控制在半小时以内效率很高。3. Linux设备树配置与内核裁剪3.1 设备树里I2C节点怎么改设备树是Linux下描述硬件的标准方式在Zynq平台里尤其重要因为PL端的逻辑怎么连、PS端的外设挂在哪全得靠它告诉内核。我用的内核版本是4.14设备树文件在Petalinux工程的project-spec/meta-user/recipes-bsp/device-tree/files/目录下。首先找到PS端I2C0或I2C1的节点。以I2C1为例默认设备树里类似这样i2c1 { status okay; clock-frequency 100000; pinctrl-0 pinctrl_i2c1_default; pinctrl-names default; gt811: gt8115d { compatible goodix,gt811; reg 0x5d; interrupt-parent intc; interrupts 0 29 4; reset-gpios gpio0 54 GPIO_ACTIVE_LOW; touchscreen-inverted-x 0; touchscreen-inverted-y 0; }; };这里几个关键点reg 0x5d 是触摸芯片在I2C总线上的地址不同芯片地址不同GT811常见的有0x5d和0xBA两种需要注意读写位的组合。interrupt-parent和interrupts指定了中断控制器和irq号。Zynq-7020的中断号是从29开始映射的这里要注意和硬件连接对应起来。reset-gpios可写可不写如果你在硬件上已经把复位脚拉死了驱动里就不用管但正规的设计都会留一个GPIO控制复位。3.2 驱动编进内核还是编成模块这里给初学者一个明确的建议调试阶段一定要编成模块也就是CONFIG_TOUCHSCREEN_GT811m。原因很简单编成模块之后你可以单独修改驱动源码交叉编译出一个.ko文件通过scp传到板子上insmod加载。如果出了问题rmmod卸载再重新加载就行整个过程十几秒。要是编进内核改一次驱动就要重新编译整个内核再做镜像折腾一次饭都凉了。等到驱动完全稳定再做一次petalinux-package --boot把模块打进根文件系统或者干脆改成y编进内核这样开机自动加载不用手动insmod。3.3 中断和轮询模式怎么选触摸驱动有两种工作模式一种是靠INT引脚中断触发读取另一种是内核定时轮询。我强烈建议用中断模式。原因在于轮询模式有两个问题第一轮询间隔设置短了CPU占用率高嵌入式平台本来资源就有限第二轮询间隔长了触摸事件延迟大操作体感很差。中断模式则完美避开这两个问题芯片检测到触摸就拉引脚触发内核的threaded irq去读取数据没有触摸时CPU完全不干预。当然中断模式要处理一个问题触摸芯片在按下和抬起时都会产生中断有的芯片还会在持续触摸时每隔一段时间刷新坐标所以中断处理函数里不能光读一次就完事要设计一个类似“工作队列”的机制把读取和上报交给专门的线程去做避免在中断上下文里做耗时操作。4. 触摸驱动核心实现细节4.1 探测函数和I2C通信流程驱动入口和大部分Linux设备驱动一样通过module_i2c_driver宏注册。i2c_driver结构体里最重要的是id_table它用compatible字符串和设备的设备树节点匹配。匹配成功后驱动框架会回调probe函数。probe函数里做的事说白了就是三件初始化I2C通信确认芯片真的在线。向input子系统注册一个触摸设备规定它能上报哪些事件类型。注册中断让触摸芯片的INT引脚能触发我们自己的处理函数。I2C通信这块Linux内核已经封装好了i2c_transfer和i2c_smbus_read_i2c_block_data等接口你要做的只是封装一层register_read和register_write把芯片自带的寄存器地址和数据传进去。GT811这种芯片的寄存器布局是有规律的坐标数据一般在特定偏移处连续存放比如0x04附近的寄存器存放X坐标0x06附近存放Y坐标每次真正有触摸时还能从一个状态寄存器里读到当前触摸点数。这里给一个读取坐标的核心流程参考static int gt811_read_touch(struct i2c_client *client, struct gt811_data *ts) { u8 buf[4]; int ret; ret i2c_smbus_read_i2c_block_data(client, 0x04, 4, buf); if (ret 0) return ret; ts-x (buf[0] 0x0f) 8 | buf[1]; ts-y (buf[2] 0x0f) 8 | buf[3]; return 0; }这只是示例实际芯片的寄存器偏移和坐标位宽要看数据手册但套路是一样的读状态寄存器判断有没有触摸读坐标寄存器拿坐标读压力值寄存器拿压力。4.2 和input子系统对接一个触摸屏驱动最终要让应用层能感知到触摸必须注册input设备并且上报合理的事件。在probe函数里这样操作ts-input input_allocate_device(); if (!ts-input) return -ENOMEM; ts-input-name GT811 Touchscreen; ts-input-id.bustype BUS_I2C; ts-input-dev.parent client-dev; __set_bit(EV_KEY, ts-input-evbit); __set_bit(BTN_TOUCH, ts-input-keybit); __set_bit(EV_ABS, ts-input-evbit); input_set_abs_params(ts-input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(ts-input, ABS_Y, 0, 599, 0, 0); ret input_register_device(ts-input);坐标范围要根据屏幕分辨率来设置。这块7寸屏是1024x600但触摸芯片输出的坐标往往不是直接等于像素坐标可能是一个0到4095或0到1023的ADC值需要做一定比例的缩放。若是输出的最大值过大可以在驱动里做一次线性换算再上报这点后面讲坐标异常时再展开。4.3 上报时机与多点触摸处理触摸事件上报时最容易犯的错误是漏掉同步事件。每次上报一组坐标之后必须调用input_sync告诉内核“这一次触摸快照结束了可以分发上去了”。否则上层应用拿到的数据会错乱轻则显示乱跳重则完全没反应。如果是多点触摸要用MT协议。在《Linux内核文档》的input/event-codes.txt里有明确说明Type B协议用slot区分每个触点。以GT811这类最多支持5点的芯片为例你要在设备树或者驱动里声明一次能支持多少个触点然后分配对应的slot上报时依次填好感兴区、压力和坐标最后input_mt_sync上报。Type A和Type B的差别一句话说就是Type A每次上报全量触点状态Type B则可以增量上报触点的新状态。对于现代内核强烈推荐Type B性能更好也更符合芯片的实际工作方式。5. 备选方案红外touchkit触摸屏驱动的适配要点不是所有场景都用I2C电容屏。有些工控设备、一体机用的是红外框式触摸屏通过检测手指遮挡红外线来定位常见的就有touchkit系列。这类触摸屏驱动如果你用的是标准内核很多在drivers/input/touchscreen/usbtouchscreen.c里直接支持USB接口的插上就能识别内核会把它注册成一个通用触摸屏设备原理是把触摸区域划分成网格红外对管被遮挡后计算坐标。我单独把它拿出来说是因为它有一个和电容屏完全不同的特性红外触摸屏不能检测压力只能靠遮挡面积估算有没有触摸。所以在驱动适配时上报BTN_TOUCH的触发条件不是压力值而是触摸点是否有效。另外红外屏往往支持鼠标模拟模式也就是坐标上报通过相对位移或者绝对坐标的方式走鼠标事件这种模式下应用层拿到的不是触摸事件而是鼠标事件在Qt或GTK里适配思路完全不同。如果你的项目里碰到的是touchkit这类屏先去内核配置里打开CONFIG_TOUCHSCREEN_USB大多数情况下就能免驱工作。如果不行再检查USB设备的vendor id和product id有没有被Linux的usb touchscreen驱动识别不识别就只能在驱动的usbtouch_devices数组里追加自己的设备号重新编译内核模块。这个工作量比I2C电容屏小很多但也意味着你能定制的东西也少。它和FPGA的关系更多是“Linux系统层面的外设接入”不涉及PL端逻辑除非你的USB或者串口控制器放在了PL端。6. 编译加载与触摸验证全流程6.1 驱动模块的Makefile写驱动模块Makefile是最容易出错的环节因为涉及交叉编译环境变量。我用的是黑金提供的交叉编译器arm-linux-gnueabihf-gcc配合内核源码树编译一个最简Makefile长这样obj-m : gt811_drv.o KDIR : /home/user/KERNEL/linux-xlnx ARCH ? arm CROSS_COMPILE ? arm-linux-gnueabihf- all: $(MAKE) -C $(KDIR) M$(PWD) modules ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) clean: $(MAKE) -C $(KDIR) M$(PWD) clean注意KDIR一定要指向你已经配置过的、和板上运行内核版本一致的内核源码目录。很多人在这一步报错就是因为内核源码版本和开发板上运行的内核版本不一致导致insmod时出现version magic不匹配。6.2 板端加载模块驱动编译出gt811_drv.ko后通过scp传到开发板然后按顺序操作insmod gt811_drv.ko dmesg | tail cat /proc/bus/input/devices ls /dev/input/event*如果probe成功dmesg里能看到类似“gt811: I2C connection established”这样的日志然后输入子系统里多出一个触摸设备节点通常叫event2或event3。关于加载顺序还有一个细节如果你把设备树里的status设为“disabled”内核不会挂载这个I2C设备insmod驱动也不会触发probe反过来如果设备树节点存在但驱动没有编译系统启动后会报“Failed to probe”之类的错误。排查时要先确认设备树有没有生效用ls /sys/bus/i2c/devices/查看I2C总线上有没有挂上你预期的地址这个比反复看dmesg高效得多。6.3 用evtest验证触摸事件模块加载成功后最简单的验证工具是evtest。它读取input设备的绝对坐标和BTN_TOUCH事件还带一个简单的GUI界面显示坐标轨迹。执行evtest /dev/input/event2手指在屏幕上滑动时应该能看到ABS_X、ABS_Y和BTN_TOUCH的数值一直在变化。这里提到的坐标如果是0到4095的原始ADC值但识别的是1024x600分辨率说明驱动里没有做坐标缩放后面还要处理。如果是0到1023但屏幕显示出来手指位置和光标位置不对称说明方向和偏移有出入。如果要用在Qt界面里还需要tslib库做坐标校准和滤波。tslib是一个触摸屏校准和处理的库编译时要带上模块./autogen.sh ./configure --hostarm-linux-gnueabihf --prefix/usr/local/tslib make make install把生成的文件拷贝到板子在开发板上用export TSLIB_TSDEVICE/dev/input/event2指定触摸节点然后就能跑ts_test进行五点校准。校准结果会生成ts_calibrate文件告诉系统怎么把触摸坐标映射到屏幕分辨率上。7. 常见问题与调试陷阱7.1 I2C通信失败的排查我在这个项目里遇到最多的错误就是probe失败dmesg里报“i2c_transfer failed: -6”或“-5”。-6是NACK说明I2C总线上根本没有这个地址的从设备响应。先检查地址对不对读写位有没有问题。有的芯片7位地址写成0x5d但如果我们误写成了8位地址0xBA肯定收不到ACK。-5是EIO一般发生在总线时序错误或硬件连线松动时。用i2cdetect -y 1扫一下I2C总线看看哪个地址能响应是最快的定位方法。还有一个隐蔽坑I2C总线被其他设备占用或者总线被拉死。这时候用示波器去量SDA和SCL波形是最直观的如果波形对但主机端仍然报错那很有可能触摸芯片的VCC供电不对或者reset引脚没有释放。7.2 中断风暴和事件上报异常中断模式下如果中断触发方式配置不对比如应该配置成下降沿触发却配置成了低电平触发就极容易造成中断风暴CPU被打满系统卡死。调试时在设备树里用interrupts 0 29 2下降沿还是0 29 4高电平要格外小心。另外触摸芯片在按下时可能连续产生多次中断而且第一次中断时坐标数据可能还没准备好。正确做法是在中断处理函数里加一个延时或者状态机的设计读到有效坐标后再上报。我一开始没做这个处理结果每次触摸都会飘出一个错误坐标屏幕上光标会突然跳一下后来直接在驱动里加了“数据准备好”的判断问题才解决。7.3 坐标不准或方向不对坐标不对通常分两类第一类是X、Y方向反了或镜像了。判断办法很简单手指向左滑看到光标向右跑那就是X方向镜像手指向上滑看到光标向下跑那就是Y方向镜像。解决办法是在驱动里修改上报的坐标值比如上报的屏幕宽度减去原始X坐标再进行一次换算这种属于最简单的一类问题。第二类是缩放关系不对。触摸输出范围是0到4095屏幕显示范围只有1024x600直接上报原始值会导致光标只在一个区域里动。解决办法是先把范围缩放到屏幕分辨率再做偏移修正。这里推荐把所有修正集中到驱动的一个函数里比如convert_coordinate()方便调试时调整公式。用tslib做校验也是一个选择但不建议依赖tslib去修正驱动层的坐标错误因为有些应用层直接读event节点不经过tslib。7.4 内核版本兼容性问题最后提一个经验不同内核版本之间input上报和I2C驱动的接口API变化不大但设备树的解析方式和中断框架有一些差异。比如旧内核的irq_of_parse_and_map和新内核的设备树IRQ解析变化可能导致中断号不对。遇到这种问题先查阅对应内核版本的Documentation/input目录看看你的输入设备驱动模型有没有变化再决定是抄哪个版本的代码。我编译驱动模块时也遇到过几次“undefined symbol”的报错多半就是因为内核源码树和板子实际运行的内核源码不一致重新编译内核镜像或者用完全一致的源码树就能解决。写到最后的一点经验触摸屏驱动这个东西说难不难说简单也不简单。它的难点不在代码量而在于整个链条的每一环都可能出问题——硬件连线、设备树、中断号、坐标缩放、input子系统任何一个地方出错表现都是“屏幕没反应”或者“触摸乱跳”。我在调这块7寸屏的时候最受用的一句话就是先确认硬件再查设备树最后才怀疑驱动代码。如果你也是第一次在FPGA开发板上跑Linux调触摸屏建议按照“i2cdetect扫地址 - 加载模块看dmesg - evtest看事件 - 校准坐标”这个顺序来。只要能走到最后一步触摸屏驱动就算拿下了后面的多点触摸、手势识别、应用层集成全都是水到渠成的事。
返回列表