ARTICLE DETAIL

资讯详情

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

FPGA平台Linux触摸屏驱动开发实战:从设备树到坐标校准

FPGA平台Linux触摸屏驱动开发实战:从设备树到坐标校准 做FPGA的人多少都碰过这类需求板子上跑了个Linux系统外接一块7寸触摸屏结果屏幕亮了触摸却没反应。更常见的是厂商给的驱动只支持某个特定内核版本一换内核就编译不过最后只能灰溜溜地在应用层读设备节点还时不时被坐标乱跳折磨。这篇文章就围绕FPGA平台下的Linux触摸屏驱动开发把我实际调试过程中踩过的坑、验证过的方案、最终能稳定跑通的步骤一起梳理出来。这个内容适合两类人一类是刚接触FPGALinux的嵌入式开发想搞清楚“屏幕亮了之后怎么让触摸也能用”另一类是已经在用Linux但被设备树、input子系统、触摸校准折腾过的工程师希望通过一篇完整的实操记录把整个链路串起来。不涉及晦涩的硬件描述语言设计重点是驱动层面的东西但也需要你对Linux基本操作、设备树、内核编译有一定了解。1. 为什么触摸屏驱动在FPGA平台上会被单独拎出来讲很多人第一反应是触摸屏驱动不是Linux内核自带的吗为什么FPGA平台还要专门写教程这个问题确实问到点子上了因为FPGALinux的平台和普通ARM开发板不一样。1.1 从“裸机点屏”到“Linux驱动”的思维转变FPGA工程师的习惯是在裸机或者轻量级RTOS下操作外设点亮一块屏通常是拉引脚时序、发初始化寄存器、刷新显存这几个步骤。但上了Linux这套工作模式就完全不一样了。Linux把所有外设都抽象成设备触摸屏的控制器在Linux看来就是一个I2C或SPI/USB设备你要做的是告诉内核“这个设备在哪、怎么访问它、数据上报后怎么处理”剩下的交给内核机制去完成。在FPGA平台里触摸屏控制器的硬件连接往往是通过GPIO模拟I2C、或者FPGA内部逻辑模拟的I2C控制器接到ARM端。怎么让Linux内核在启动时正确识别这个设备这正好是设备树Device Tree干的事情。你要在设备树里描述这个I2C控制器的地址、中断引脚、复位引脚这样内核的i2c-core才能枚举到你的触摸控制器。如果你之前在裸机里用过触摸屏最大的坑就是“裸机里你按寄存器操作Linux里你不能随便直接操作寄存器”一旦你往某个寄存器写值没通过内核API轻则驱动崩溃重则整个系统挂掉。1.2 选型思考自己写驱动还是直接用内核自带的刚开始做这个项目时我第一反应是找厂商要驱动源码。但现实很骨感厂商提供的驱动大部分是针对某个具体内核版本的补丁包放在老内核上能编译放在新内核上满屏报错。而且FPGA平台经常要裁剪内核有些驱动依赖的子系统被裁掉了加载时直接报unknown symbol。所以我自己总结了一套选型逻辑如果触摸屏控制IC是主流型号比如FT5x06、GT911、GT9147、HX8527这类优先考虑内核主线里有没有现成的驱动有就直接用只需要改设备树如果没有现成的但控制IC的数据手册和初始化序列是公开的就自己写一个标准的input设备驱动如果厂商给了源码也不要直接编译先看它依赖哪些内核接口再确认你的内核版本是否支持这些接口。这样的话不管平台怎么变你手上的技术方案都是可迁移的不至于被厂商绑定死。2. 硬件链路与驱动框架选择驱动开发不能只盯着软件硬件接错了也是白搭。我在调试过程中反复确认了硬件连接、上电时序和控制IC的I2C地址才最终把驱动跑通。这一节把触摸屏驱动的硬件链路和Linux侧的框架串起来讲。2.1 7寸屏硬件接口与上电时序7寸屏常见的是RGB接口并行24bit或18bit加一个电容触摸面板。RGB接口负责图像显示触摸面板则通过一个控制IC连接主机。控制IC常见的有FT5x06Goodix的GT911也很多、海信的HX8527等它们大多走I2C接口有些还支持SPI但I2C是主流。上电时序是个容易翻车的细节。很多屏的触摸控制IC要求“先给VCC再给I2C上拉、复位脚释放”时序不对会导致I2C设备无法枚举。你在FPGA平台上做硬件时如果复位脚和中断脚都由FPGA引脚控制务必确认Linux启动时这些引脚处于正确的默认状态。我调试时遇到过一种情况设备树里明明写对了I2C地址但i2cdetect就是扫不到设备后来发现是复位脚一直没有拉高IC一直处于复位状态。把复位脚通过设备树的gpio-hog或者驱动代码正确拉高后设备瞬间就能扫到了。在实际项目中7寸屏的触摸面板经常会出现坐标反向的问题特别是横竖屏切换的场景。这个不一定是驱动写错了很可能是面板安装方向和触摸IC坐标轴的默认方向不一致后面会讲校准方法。2.2 Linux input子系统和触摸屏驱动的挂载点Linux内核里输入设备统一走input子系统。你写触摸屏驱动核心就两件事注册一个i2c_driver并在probe回调里注册一个input_dev然后当中断触发时把触摸坐标通过input_report_abs上报给内核。结构上大概是这样的触摸屏IC - I2C控制器FPGA逻辑或者ARM内部I2C - i2c_client设备树里描述的从设备 - 触摸屏驱动drivers/input/touchscreen/xxx.c - input_dev - event设备节点 /dev/input/eventX你在用户态做的所有触摸操作最终都是通过读/dev/input/eventX文件来获取的。就算你不写一行应用代码用hexdump /dev/input/eventX都能看到原始触摸事件这点对调试非常有用。2.3 中断方式还是轮询方式触摸屏驱动可以工作在中断模式也可以工作在轮询模式。7寸屏的触摸控制器一般都会有一个INT引脚触摸发生时INT脚拉低或拉高通知主机。FPGA平台上这个INT引脚会接到ARM的GPIO上设备树里配置为中断触发。中断模式的好处是CPU占用低、响应快坏处是一旦INT信号不稳定会出现误触或漏触。轮询模式简单粗暴定时器每隔一段时间读一次触摸IC的寄存器没触摸就跳过有触摸就上报。缺点是CPU占用高、触摸响应有延迟但我遇到过信号质量差的板子中断模式频繁误触发最后只能用轮询兜底。所以这个取舍完全取决于你的硬件设计。3. 设备树配置与驱动代码实现在讲解实际代码之前先说清楚Linux设备模型里设备树扮演的角色。你可以把设备树理解成一张“硬件清单”内核启动时通过它知道系统里挂了哪些设备、设备在哪个总线地址上、需要什么资源。你在Linux下写触摸屏驱动第一步就是让内核“看见”这个触摸控制器。3.1 设备树里需要描述的信息设备树节点通常挂在某个I2C总线上核心属性包括compatible用来匹配驱动、regI2C设备地址、interrupt-parent和interrupts中断引脚、reset-gpios和touchscreen-size-x/y等。以一个GT911触摸控制器为例设备树节点大致长这样i2c0 { status okay; clock-frequency 100000; gt911: gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts 20 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 21 GPIO_ACTIVE_LOW; irq-gpios gpio0 20 GPIO_ACTIVE_LOW; touchscreen-size-x 1024; touchscreen-size-y 600; }; };这里有个容易踩的坑I2C地址不是固定的GT911根据引脚电平配置不同地址可能是0x5d或0x14驱动里一般通过读取配置寄存器或者直接尝试两个地址来适配。如果你在设备树里写死了一个地址而硬件上实际是另一个就会出现设备扫描不到的现象。排查时用i2cdetect去扫一下真实地址比反复改设备树快得多。3.2 驱动代码结构从probe到事件上报驱动代码的结构其实很固定核心就是probe函数里完成设备初始化和input设备注册然后等着中断发生。给一个精简的骨架#include linux/i2c.h #include linux/input.h #include linux/interrupt.h #include linux/of.h #include linux/of_gpio.h #include linux/gpio/consumer.h static irqreturn_t gt911_irq_handler(int irq, void *dev_id) { struct gt911_data *ts dev_id; u8 buf[8]; int x, y, id; /* 读取触摸状态和坐标寄存器 */ i2c_master_recv(ts-client, ts-xy_buf, 8); if (!(ts-xy_buf[0] 0x80)) goto out; id (ts-xy_buf[0] 0x0f); x ((ts-xy_buf[1] 0x0f) 8) | ts-xy_buf[2]; y ((ts-xy_buf[3] 0x0f) 8) | ts-xy_buf[4]; 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); out: return IRQ_HANDLED; } static int gt911_probe(struct i2c_client *client) { struct gt911_data *ts; struct input_dev *input; int err; ts devm_kzalloc(client-dev, sizeof(*ts), GFP_KERNEL); input devm_input_allocate_device(client-dev); input-name gt911_touch; input-id.bustype BUS_I2C; __set_bit(EV_KEY, input-evbit); __set_bit(BTN_TOUCH, input-keybit); __set_bit(EV_ABS, input-evbit); input_set_abs_params(input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(input, ABS_Y, 0, 599, 0, 0); err input_register_device(input); if (err) return err; ts-client client; ts-input input; i2c_set_clientdata(client, ts); err request_threaded_irq(client-irq, NULL, gt911_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gt911, ts); if (err) { dev_err(client-dev, request irq failed: %d\n, err); return err; } return 0; }需要特别注意request_threaded_irq的使用。触摸屏中断处理如果直接在主中断里做I2C读写很可能因为I2C传输耗时太长影响系统实时性。request_threaded_irq配合IRQF_ONESHOT可以把I2C读取放在线程上下文里这样中断响应快也不会阻塞其他高优先级中断。3.3 编译与加载驱动驱动编译分两种方式一种是编进内核一种是编成模块。调试阶段强烈建议编成模块毕竟每改一次代码就烧一次内核太浪费时间。如果你的驱动放在内核源码树下比如drivers/input/touchscreen/gt911.c那就需要修改Kconfig和Makefile。但自己调试时更快的做法是直接把驱动源码放在内核外编译用内核提供的Makefile框架obj-m : gt911.o KVERSION : $(shell uname -r) KDIR : /lib/modules/$(KVERSION)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean然后在板子上加载insmod gt911.ko加载后立刻看内核日志dmesg | tail -50如果probe成功会看到类似“input: gt911_touch as /devices/.../input/input0”的日志。如果失败检查I2C地址对不对、中断是否注册成功、设备树节点有没有被正确解析。4. 触摸坐标校准与内核事件验证驱动跑起来之后你会发现触摸能动了但点不准。这几乎是所有触摸屏项目的必经环节所以单独拿一节来讲坐标校准和数据验证。4.1 为什么触摸屏坐标会“不准”触摸屏控制IC上报的坐标是一个ADC采样值通常范围是0到最大分辨率之间。但这个坐标和屏幕像素坐标之间并不是天然一一对应的影响因素有几个方面触摸IC的坐标原点可能在左上角也可能在右上角取决于面板走线方向屏幕的显示分辨率是800x480、1024x600或1280x800而触摸IC的最大坐标值不一定是这个分辨率液晶屏在组装时可能有细微偏移旋转导致触摸坐标和显示像素之间有缩放、旋转和平移关系。所以内核驱动里你做的input_set_abs_params中的最大值设置必须和你的LCD分辨率对应。如果触摸IC最大坐标是4095而你设置成了1023就会出现“触摸物体手指移到屏幕中间光标却跑到四分之一处”的诡异现象。4.2 内核层校准与应用层校准tslib/libinput校准分两个层面。内核层面能做的就是把input_set_abs_params里的最大值设对以及必要时在驱动里做坐标变换。比如屏是横屏显示、触摸IC默认竖屏坐标那就要做一次交换XY轴的映射。应用层面的校准常见做法是tslib或libinputtslib是传统的触摸屏校准库它的核心思路是通过5点或9点校准算出一个从触摸坐标到屏幕坐标的变换矩阵然后把变换后的坐标上报给上层应用libinput是桌面系统常用的输入处理库它会自动识别触摸设备并进行坐标校准但它主要面向桌面环境在嵌入式里如果你是Qt应用也可以直接用libinput但配置起来比tslib麻烦。我的习惯是嵌入式Qt项目直接用tslib因为它的环境变量配置简单在Qt里只要设置QT_QPA_FB_TSLIB1就能生效。4.3 手动验证坐标上报的正确姿势写完驱动后不管是做校准前还是校准后一定要先手动验证坐标上报是否正常。常用工具有hexdump、evtest。先用hexdump看裸数据hexdump /dev/input/event1触摸屏幕会看到类似这样的输出0000000 0000 0000 0000 0000 0000 0000 0000 0000按一下再按一下每次触摸都会产生一组event包含时间戳、type、code、value。这个阶段不解析具体数值只看有没有事件产生。然后装evtest它会把事件解析成人能看懂的内容evtest /dev/input/event1触摸屏幕观察ABS_X和ABS_Y的数值变化。如果X坐标随着手指左右移动而有规则地变化说明触摸驱动基本工作正常。如果数值乱跳或者没有变化就要回到中断、I2C读取、设备树这几个环节排查。5. 调试过程中遇到的坑与排查思路这节把调试过程中踩过的坑和现场排查思路整理出来方便你以后遇到类似问题直接对照定位。5.1 常见问题速查表现象可能原因排查/解决i2cdetect扫不到触摸ICI2C地址错误、复位脚未拉高、I2C时序不对检查硬件连接确认IC实际地址用示波器看I2C波形检查复位脚电平和时序触摸无反应dmesg没有中断日志中断引脚配置错误、中断未注册成功检查设备树interrupts属性确认GPIO号对应正确用gpio_test手动测试引脚电平变化能上报事件但坐标乱跳触摸IC供电不稳、I2C读取有干扰、未做滤波检查硬件电源纹波尝试轮询模式或软件滤波确认I2C时钟频率不要过高坐标反向或旋转触摸面板安装方向与坐标轴不一致在驱动代码里做坐标变换用tslib校准在设备树里设置swap-x/y参数驱动加载报unknown symbol内核缺少对应子系统或模块符号检查内核配置是否包含input子系统、I2C子系统确认依赖模块先加载多点触摸只能识别一点驱动未注册ABS_MT系列事件补全type B协议相关配置确认控制IC支持多点并开启相关寄存器5.2 中断触发不稳怎么办触摸屏中断引脚在硬件上如果没做好滤波很容易受电磁干扰造成误触发。最直接的解决办法是看一下设备树里的中断触发类型IRQ_TYPE_EDGE_FALLING还是IRQ_TYPE_LEVEL_LOW要跟IC手册对应起来。我实际遇到过一个问题GT911在初始化配置后中断引脚默认输出高电平触摸时拉低。但我设备树里配了IRQF_TRIGGER_FALLING结果触摸一次会连续触发多次中断。原因是在中断处理完成后如果触摸状态寄存器没有清除干净引脚电平持续低边沿触发就会再次挂起。解决办法是用IRQF_TRIGGER_LOW配合IRQF_ONESHOT或者在中断处理里读取状态寄存器后主动清一次标志位。另一个思路是中断延时确认。在中断触发后延时20到50ms再读坐标能滤掉一部分干扰但代价是触摸延迟增加。这个要根据你的项目对实时性的要求来取舍。5.3 触摸丢点、滑动不连贯丢点几乎是所有电容触摸屏驱动都要面对的问题最典型的场景是快速滑动时轨迹中间突然断掉。这背后有几个可能的原因I2C读取速度不够导致事件上报频率跟不上触摸扫描频率触摸IC内部扫描周期本身较长比如有些IC固定20ms上报一次你再快也没用驱动只处理了单点上报没有处理多点数据导致滑动时某一路坐标丢失。针对I2C读取不够快可以尝试提高I2C时钟频率。GT911官方支持最高400kHz但有些FPGA逻辑模拟的I2C控制器跑的并不稳定上到400kHz会在长线缆场景下出错。我的建议是先用100kHz跑通确认逻辑正确后再提速。如果是多点的滑动务必确认驱动里实现了type B协议也就是要上报ABS_MT_SLOT、ABS_MT_POSITION_X、ABS_MT_POSITION_Y并用input_mt_report_slot_state来报告触摸状态。只上报ABS_X/ABS_Y的单点驱动在多指滑动时丢点非常严重。5.4 应用层拿不到触摸事件有时候驱动工作正常dmesg也没问题但Qt或者其他应用就是收不到触摸事件。这个问题往往不是驱动问题而是系统没有安装好“触摸事件到应用事件的桥”。常见做法是使用libinput或tslib把内核对input子系统上报的事件转化为应用能识别的触摸操作。如果你用tslib还要确认/dev/input/eventX的设备节点路径正确并在环境变量里配置好export TSLIB_TSDEVICE/dev/input/event1 export TSLIB_CALIBFILE/etc/pointercal export QT_QPA_FB_TSLIB1如果应用还是收不到用evtest验证一下该设备节点能否正常上报事件。能上报但应用没反应那就是应用层配置问题不能上报就回头查驱动和设备树。6. 性能优化与后续扩展思路驱动基本工作正常之后你会发现触摸响应体验还有优化空间。这里分享几个我实际用过的优化手段以及往更复杂场景扩展时的思路。6.1 减少触摸上报延迟的实用手段触摸延迟来源主要有三块硬件扫描周期、I2C读取时间、内核事件上报和应用处理延迟。硬件扫描周期改不了但后面两块可以优化。把I2C中断处理改成threaded irq之后如果还觉得延迟高可以尝试把坐标读取和input上报分开中断里只标记need_read由一个高优先级内核线程去读坐标这样能避免I2C传输阻塞其他中断。减少I2C读取次数。很多触摸IC在一次读取里可以同时返回单点/多点坐标如果你只需要单点就只读必要的几个寄存器不要每次把所有寄存器都读一遍。在应用层面使用Qt的QPA插件时开启evdevtouch插件比普通fb插件响应更快因为它直接走libinput/evdev的路径省掉了一层转换。6.2 和多点触控/手势识别的接轨如果产品不只是点按还要支持缩放、滑动、长按之类的手势那就需要完整支持多点触控。Linux内核的input子系统已经把多点触控协议分成了type A和type B现代触摸IC基本都是type B也就是带slot机制。驱动里需要上报ABS_MT_SLOT、ABS_MT_TRACKING_ID、ABS_MT_POSITION_X/Y等事件。手势识别可以放在应用层也可以放内核层但我的经验是放应用层更灵活。内核层做手势识别一旦识别错了很难在运行中调整参数应用层可以随时改阈值和算法。Qt、LVGL、GTK都有自己的手势处理机制直接用就行。6.3 从单屏到多屏、从触摸到压感FPGA平台的显示接口通常很灵活有时候一块板子要接多个屏幕。多屏情况下每个触摸屏的input设备都会独立注册应用层通过设备节点名称来区分。设备树里给每个触摸节点起一个可识别的label属性应用层用libinput list-devices就能清晰分辨。带压感的触摸屏比如主动笔驱动会比普通电容触摸复杂一些需要额外上报ABS_PRESSURE而且还涉及笔侧键、悬停等事件。如果你以后要接这种屏驱动框架还是同一套只是寄存器解析和事件上报逻辑会成倍增加。建议先把手上的普通7寸触摸屏驱动彻底吃透再去啃压感笔的部分。总结下来的一点个人体会从裸机点屏到Linux触摸屏驱动最大的门槛其实不是C语言也不是驱动框架而是思维方式的转变从直接操作寄存器变成“通过内核机制间接控制硬件”。FPGA平台的灵活性既是优势也是包袱因为硬件引脚、I2C控制器可能都不是标准ARM芯片上的那种固定外设设备树的编写更容易出错。但我仍然建议你坚持用Linux标准驱动框架来做哪怕第一次调试花的时间长一点。原因很简单标准框架意味着社区内有大量现成的资料和驱动可参考哪怕今天不是GT911、明天换成了FT5x06你只要掌握这套方法换成另外一个触摸IC也就是改改设备树、改改寄存器解析的事。踩坑不可怕怕的是每次都在同一个地方栽跟头。把这篇文章里的排查路径走一遍你的触摸屏驱动基本就能稳稳跑起来了。
返回列表