ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FPGA SoC开发板Linux下7寸触摸屏驱动调试全流程解析

FPGA SoC开发板Linux下7寸触摸屏驱动调试全流程解析 前段时间拿到一块带7寸触摸屏的FPGA SoC开发板第一反应是“这有什么好调的不就是个I2C触摸芯片嘛”。真正接上电开始搞之后我才意识到事情没那么简单显示通道正常只说明RGB信号没问题触摸部分牵扯到FPGA可编程逻辑里的I2C控制器、Linux设备树、input子系统还有厂商触摸IC特有的寄存器时序任何一个环节对不上屏幕就是个只能看不能点的摆设。这篇内容就把这套链路完整捋一遍。我从硬件连接方式讲到设备树写法再到手写一个精简触摸驱动并完成evtest验证最后整理了实际调试中踩过的几个典型坑。适合手里有Zynq这类FPGA SoC开发板、想在Linux下用起触摸屏的嵌入式开发工程师也适合刚接触Linux驱动、想知道一个标准input设备到底是怎么挂到系统里的同学。整个方案的思路不绑定具体厂商你把芯片换成常见的FT5x06、GT1151也是一样的套路。1. 项目硬件链路与方案选型1.1 7寸触摸屏的硬件结构先搞清楚信号从哪来市面上的7寸屏通常由两部分组成一边是RGB/LVDS/MIPI接口的显示面板另一边是覆盖在面板上的触摸感应层。触摸层本身不是Linux设备它需要一颗触摸控制IC把电容变化或电阻电压转换成数字坐标再通过I2C或SPI上报给处理器。这块开发板上用的是一颗GT911典型国产电容触摸ICI2C接口成本低在7寸、10.1寸屏上非常常见。GT911的工作链路并不复杂芯片上电后需要复位时序然后等待主机通过I2C配置并读坐标引脚上会有一个INT中断信号告诉主机“有触摸发生了”。问题在FPGA SoC板卡上会多一层特殊性Zynq这类芯片有PS端ARM硬核和PL端FPGA可编程逻辑开发板为了布局方便往往会把7寸屏扩展接口里的触摸引脚直接引到PL侧的IO而不是接到PS端的MIO脚。这意味着什么如果你不做任何硬件映射ARM核根本感知不到这颗触摸IC的存在。常规做法是在FPGA内部例化一个AXI IIC控制器IP再通过AXI总线连接PS端让Linux看到的触摸芯片挂在一个由PL逻辑虚拟出来的I2C控制器下面。另一类板子会把触摸IO引到PS的EMIO引脚EMIO本质上还是PS的I2C控制器只是信号经过PL侧IO绕了一圈。两种方式设备树写法略有差别但驱动模型完全一样。1.2 驱动方案选型裸机轮询还是Linux输入子系统有的朋友可能会问FPGA里都能自研I2C时序了直接在RTL逻辑里把坐标读出来不就行了确实能但适用场景很窄。你一旦跑Linux应用层直接操作物理寄存器显然不现实更合理的做法是让系统看到标准的输入设备。裸机轮询的问题非常明显尤其是用FPGA SoC做产品的时候。第一坐标采集、滤波、多点跟踪、手势识别这些原本内核和用户态库已经做好的功能你都要自己从零写一遍维护成本极高。第二Qt、Weston、GTK这些图形框架默认从/dev/input/event*读输入事件你不按input子系统来图形应用根本拿不到触摸数据。第三多线程、中断、电源管理抽象得很好的大环境你不用非要自己裸轮询调试一次你就知道痛苦了。Linux的input子系统把所有输入设备统一抽象成“事件流”触摸屏驱动负责从芯片读坐标然后把坐标转换成标准事件core层负责管理设备evdev负责在/dev/input下生成设备节点。应用层不关心你的芯片是GT911还是FT5x06它只关心事件类型和坐标值。这种解耦结构让驱动开发聚焦在最底层的硬件读写上其他交给系统就好。顺便提一句如果你用的是红外触摸屏比如热词里出现的touchkit那通常是USB或者串口方案驱动对应usbtouchscreen或者hid_multitouch模型还是input device只不过总线换成了USB。驱动思路通用但寄存器操作差很多别套用I2C那套。2. 驱动框架与内核机制拆解2.1 Linux输入子系统设备、核心、事件层写触摸驱动之前得先理解input子系统三层结构设备驱动层Device Drivers、输入核心层Input Core、事件处理器层Event Handlers。设备驱动层就是我要写的那部分工作内容是初始化硬件、请求中断、读取坐标、调用input_report_abs上报数据。输入核心层负责维护一个全局的input_dev链表管理设备和handler之间的匹配关系。事件处理器层最常见的实现是evdev它会为每个input设备生成一个字符设备节点应用层open这个节点就能读到结构化事件。触摸屏最核心的增事件类型有两个EV_KEY用于按键状态比如BTN_TOUCH表示触摸按下或抬起EV_ABS用于绝对坐标单点触摸一般上报ABS_X/ABS_Y多点触摸上报ABS_MT_POSITION_X/ABS_MT_POSITION_Y。坐标上报后必须调一次input_sync表示这批数据是一个完整事件周期。漏掉sync是新手最容易犯的错误你会看到evtest里有x、y变化但没有一个完整的事件包被提交。事件类型 | 含义 | 上报方式 EV_KEY | 按键/触摸状态 | input_report_key(input, BTN_TOUCH, 1/0) EV_ABS | 绝对坐标 | input_report_abs(input, ABS_X, x) EV_SYN | 同步批次 | input_sync(input)2.2 I2C设备驱动模型probe是怎么被调起来的Linux的I2C子系统采用“控制器-客户端”模型。控制器是硬件上的I2C总线适配器在Zynq上对应I2C控制器驱动客户端就是触摸IC对应驱动模块。一个触摸驱动主要填充的结构体是i2c_driver需要指定匹配表然后实现probe、remove等回调。匹配过程大致是设备树解析后内核会为每个I2C子节点创建一个i2c_client其compatible属性会被用来和驱动的of_device_id做匹配。匹配成功后调用probe函数把i2c_client指针传给我们之后所有寄存器读写都通过这个client进行。static const struct of_device_id gt911_of_match[] { { .compatible goodix,gt911, }, { /* sentinel */ } }; static struct i2c_driver gt911_driver { .probe gt911_probe, .remove gt911_remove, .id_table gt911_id_table, .driver { .name gt911, .of_match_table gt911_of_match, }, }; module_i2c_driver(gt911_driver);probe函数里要做的事就三件复位并初始化触摸IC确认芯片通信正常分配并注册input_dev设备请求中断把物理坐标变化变成事件源。2.3 设备树告诉内核硬件长在哪、怎么连设备树对嵌入式Linux来说是硬件描述语言。触摸IC节点必须挂在它实际连接的I2C总线节点下面内核才能知道这个I2C地址上有一个触摸设备。我用的开发板把触摸INT引脚接到了PL侧的AXI GPIO上所以设备树里除了I2C节点还要配置中断和GPIO复位引脚。axi_iic_0 { status okay; clock-frequency 100000; gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent axi_gpio_0; interrupts 0 2; touchscreen-max-x 1024; touchscreen-max-y 600; }; };这里有几个容易踩坑的点。reg值是7位I2C地址GT911常见有两个地址0x5d和0x14由芯片的复位时序决定。设备树里的compatible必须和驱动of_device_id里的字符串一致否则内核找不到驱动。interrupt-parent和interrupts写法依赖于你硬件上把INT接在哪里如果触摸INT直接接PS的MIO那就要写对应的GIC中断号接PL GPIO通常先经过AXI GPIO再进PS中断控制器。这个属性配置错最常见的情况就是probe能跑但中断永远不来。3. 从零实战完整调通7寸触摸屏3.1 前置准备Vivado工程与Linux内核配置写驱动之前先保证硬件在外设层面可见。如果你从Vivado角度做需要在block design里添加一个AXI IIC控制器把SCL/SDA引脚引到PL侧IO触摸屏幕的INT引脚用一个AXI GPIO模块接收该GPIO的中断输出连接到PS的PL-PS中断端口。导出硬件工程后用PetaLinux或者自己编译内核时会生成对应设备树上面那一段节点往往已经替你建好了I2C部分。内核配置这一步很多人会忽略。确保.config里打开了I2C支持、INPUT事件设备、触摸屏支持CONFIG_I2Cy CONFIG_INPUTy CONFIG_INPUT_EVDEVy CONFIG_TOUCHSCREENy内核自带goodix驱动但这里为了讲清楚整个过程我选择挂一个自己写的模块。如果不想自己写直接用CONFIG_TOUCHSCREEN_GOODIX打开内核自带驱动也可以。不过学习角度建议手写一遍理解深很多。触摸IC如果直接接PS自带I2C就不需要额外PL逻辑但多数FPGA开发板的7寸屏扩展接口默认接PL所以AXI IIC这步很难绕过。3.2 启动系统确认触摸IC挂在总线上开发板启动、bit文件加载完成后先不要急着装驱动。用i2cdetect扫描一下总线看看I2C地址上有没有这颗芯片。Zynq上AXI IIC控制器对应的bus编号通常从0开始但也可能因为设备树枚举顺序变成1或者2先用ls /sys/bus/i2c/devices看清楚。ls /sys/bus/i2c/devices/ rootboard:~# i2cdetect -y 0 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- 5d -- -- -- -- -- --看到0x5d就说明I2C通路OK。如果扫不到先查硬件复位、电压、上拉电阻再查Vivado引出的引脚是否正确千万别急着写驱动。这一步就能拦住一半的“驱动写挂了”问题。3.3 手写GT911驱动核心代码与实现思路驱动整体结构不复杂probe里做初始化中断里读坐标然后上报input事件。下面代码是经过裁剪的关键路径为了便于阅读省略了部分错误处理、多点坐标解析和电源管理真实产品代码在此基础上补全即可。static int gt911_read_coords(struct i2c_client *client, u16 *x, u16 *y) { u8 reg[2] {0x81, 0x4E}; u8 buf[4]; struct i2c_msg msg[2]; int ret; /* 先写寄存器地址再读数据 */ msg[0].addr client-addr; msg[0].flags 0; msg[0].len 2; msg[0].buf reg; msg[1].addr client-addr; msg[1].flags I2C_M_RD; msg[1].len 4; msg[1].buf buf; ret i2c_transfer(client-adapter, msg, 2); if (ret ! 2) return -EIO; /* 状态位最高位为1表示有有效触点 */ if (!(buf[0] 0x80)) return -ENODATA; *x buf[1] | (buf[2] 8); *y buf[3] | (buf[4] 8); return 0; }GT911坐标寄存器从0x814E开始触碰后每个触点有几字节数据包括触点状态和x/y坐标。上面这段用i2c_transfer完成一次“先写后读”的组合事务比i2c_master_send加i2c_master_recv的写法更稳因为两个操作在同一个START-STOP周期内完成避免总线被其他设备插队。中断处理和上报部分static irqreturn_t gt911_irq_handler(int irq, void *dev_id) { struct gt911_dev *ts dev_id; u16 x, y; if (gt911_read_coords(ts-client, x, y) 0) { 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; }probe里要做的比较琐碎。获取中断号、初始化input设备、设置坐标范围、注册设备、申请中断。一个很关键的细节是必须用线程化中断因为I2C读操作可能会休眠不能在普通硬中断上下文里做。static int gt911_probe(struct i2c_client *client) { struct gt911_dev *ts; struct input_dev *input; int ret; ts devm_kzalloc(client-dev, sizeof(*ts), GFP_KERNEL); input devm_input_allocate_device(client-dev); if (!ts || !input) return -ENOMEM; ts-client client; ts-input input; i2c_set_clientdata(client, ts); input-name GT911 7inch Touch; input-id.bustype BUS_I2C; __set_bit(EV_KEY, input-evbit); __set_bit(EV_ABS, input-evbit); __set_bit(BTN_TOUCH, input-keybit); input_set_abs_params(input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(input, ABS_Y, 0, 600, 0, 0); ret input_register_device(input); if (ret) return ret; ret devm_request_threaded_irq(client-dev, client-irq, NULL, gt911_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gt911, ts); if (ret) return ret; dev_info(client-dev, gt911 probe ok\n); return 0; }中断触发方式这里写成下降沿但不同板子上的GT911默认寄存器配置可能有差异。有些固件配置为低电平有效有些是脉冲模式配置不对就会碰到下面第4章里的中断风暴问题。注册驱动后编译模块加载arm-linux-gnueabihf-gcc -c gt911.c # 或者放到内核目录里以模块方式编译 make ARCHarm dtbs zImage modules insmod gt911.ko3.4 应用层验证evtest与tslib校准驱动模块加载成功后先看内核有没有报错dmesg | tail -n 20 cat /proc/bus/input/devices如果正常会看到一个name为“GT911 7inch Touch”的input设备对应的event节点一般是event0或event1。接下来用evtest验证事件evtest /dev/input/event0手指点一下屏幕终端里会滚出大量如下内容Input device ID: bus 0x18 vendor 0x0 product 0x0 version 0x0 Events: EV_ABS ABS_X 512 EV_ABS ABS_Y 300 EV_SYN SYN_REPORT 0看到坐标变化就说明驱动基本通了。如果坐标值和物理位置对不上下一步做校准。虽然电容屏原则上出厂有映射但嵌入式里屏幕安装角度、驱动初始坐标范围设置不对都会导致触摸点偏移。嵌入式Linux下最常用的还是tslib。export TSLIB_TSDEVICE/dev/input/event0 export TSLIB_CALIBFILE/etc/pointercal ts_calibrate校准程序会依次显示五个十字点依次点击后生成pointercal校准文件。Qt嵌入式程序里通过环境变量TSLIB_CALIBFILE或命令行参数指定触摸设备也能直接使用。4. 踩坑记录与调试手法实录4.1 中断不触发或者一触发就中断风暴中断是最容易出问题的地方。先说“不触发”。我有一次在设备树里把interrupt-parent写成了总线的父节点结果驱动probe成功但client-irq是无效的触摸屏怎么点都没反应。排查方法是看/sys/class/gpio或/proc/interrupts确认中断号对应GPIO上有没有电平变化再用工具把GPIO强制拉低拉高验证链路最后检查设备树的中断属性写法。再说“中断风暴”表现为一加载驱动CPU占用率飙到接近100%dmesg刷屏。大概率是清中断不及时。GT911这类触摸IC在主机读走坐标后会自动释放INT引脚但在你没读之前INT会一直保持有效。我用上升沿触发时每次中断读完数据第二次变更就检测不到了改成下降沿又遇到高电平一直有效导致连续触发。最终解决方式是把中断触发类型调成低电平配合线程化IRQF_TRIGGER_LOW并在中断处理里合理读取状态寄存器点一下只报一次有效事件。4.2 坐标错位、镜像翻转和分辨率范围显示画面正常但触摸对不上的现象太常见了。排查分两步先看坐标范围再看方向。GT911输出的是0到最大坐标的原始值最大坐标通常由触摸屏的传感通道决定可能是1024x600也可能是2048x1200。我用evtest打点发现四个角落的坐标和屏幕逻辑分辨率差很多于是把input_set_abs_params里的最大值改成触摸IC输出的实际上限偏移马上就小了很多。方向问题在横竖屏切换时特别突出。屏幕竖着用驱动却按横屏上报那就需要把X/Y对调甚至反向。常规做法是在驱动里加坐标变换很多产品会用设备树的touchscreen-inverted-x、touchscreen-inverted-y、touchscreen-swapped-x-y属性内核触摸屏框架解析后统一处理比自己改驱动方便也不容易出错。记住一个原则先确保原始坐标最大范围正确再谈翻转两个问题混在一起解决效率很低。4.3 I2C通而不稳扫描有设备但一读数据就超时这类问题在接了长排线的7寸屏上特别多。I2C电平本来就依赖上拉电阻开发板上触摸屏排线一长线间电容增大信号边沿变慢。扫描设备偶尔成功真正连续读坐标时频繁NACK或EIO。解决思路按顺序从硬件到软件排查。先看I2C速率100kHz比400kHz稳很多初期调试不妨先用慢速。再用示波器看SCL/SDA波形确认上升沿是否过缓如果慢重点检查上拉电阻是否合理排线是否松动。软件侧给i2c_transfer加重试机制遇到EIO就延时几毫秒再重试实测下来能救回不少偶发失败。但重试只是兜底别天真地以为能让所有硬件问题消失。4.4 多点触摸协议的选择为什么单点上报不够用按上面代码注册EV_KEY加EV_ABS只能上报单点。现在很多Linux图形应用虽然单点也能用但涉及缩放手势或者多点书写就会出问题。原因是多点触摸在内核里有专门的MT协议分为协议A和协议B。协议A每次上报当前所有触点状态系统自己判断触点连续性协议B依赖内核识别用户空间触摸点跟踪通过Type B slot机制维护触点ID。想真正支持多点驱动需要增加ABS_MT_SLOT、ABS_MT_POSITION_X、ABS_MT_POSITION_Y上报时先input_mt_slot确认触点编号再input_mt_report_slot_state上报触点状态最后input_sync同步。GT911每个触点坐标存放在独立的寄存器列表里先算出当前触摸点数再循环上报所有触点即可。协议B代码量会多不少但这是触摸驱动“能用”和“好用”的分水岭。另外补充一点ts_calibrate对电容多点触摸意义没那么大它主要用于电阻屏单点线性校准。电容屏坐标不准更多是翻转和坐标范围设置问题别把校准当成万能药我见过有人调了一整天tslib最后排查出来是驱动把x和y填反了ts_calibrate再折腾也救不回来。最后再分享一个调试习惯驱动调通后别急着接应用层先用evtest把原始事件打出来观察几分钟重点看有没有断点、跳点、坐标毛刺。刚上电时系统还没稳定驱动里连续读会有几次I2C失败别慌这是触摸IC电源和复位时序还没完全稳定的表现加个简短延时后继续跑即可。真正常见的随机异常大多出现在坐标反跳和触点在多触点之间漂移这类问题在裸板上用evtest就能复现比带GUI跑方便太多。调试嵌入式驱动耐心和测量工具永远比堆代码更有用。
返回列表