1. 从一块RK3588开发板说起:触摸屏点亮之前要理清的事
RK3588这颗芯片这两年在嵌入式圈子里热度一直没降过。8核CPU(4×Cortex-A76 + 4×Cortex-A55)、Mali-G610 GPU、6TOPS算力的NPU,再加上能同时驱动多路4K显示的VPU,参数摆在那里,做边缘计算、工业HMI、智能终端的人都绕不开它。但真正上手的人都知道,从拿到板子到触摸屏能正常响应,中间隔着的坑比想象中多得多。
我前后在三四块不同的RK3588板子上做过触摸屏适配,从最开始的MIPI DSI屏到后来的HDMI+USB触摸方案,再到eDP接口的工业屏,每一块屏的点亮过程都不太一样。这篇内容就是把这些经验整理出来,围绕触摸屏开发这条主线,把设备树配置、驱动加载、触摸校准、和NPU侧AI推理的联动这几个关键环节讲清楚。不管你是刚拿到鲁班猫5这类RK3588开发板的新手,还是已经在做PX4飞控地面站或者YOLOv8边缘部署的老手,触摸屏这一环都是绕不过去的基础设施。
先说清楚一个前提:RK3588的显示子系统分两条线,一条是VOP(Video Output Processor,视频输出处理器),负责把帧缓冲送到物理接口;另一条是显示接口控制器,比如MIPI DSI、HDMI、eDP、DP这些。触摸屏的“显示”和“触摸”其实是两个独立子系统——显示走VOP+接口控制器,触摸走I2C或USB。很多人第一次调试时把两者混在一起排查,结果方向就偏了。我见过有人屏幕能显示但触摸没反应,就去改VOP配置,改了半天没用,因为触摸根本不在那条链路上。
所以这篇内容的组织逻辑是:先把硬件链路和软件栈的分层关系理清楚,再进入设备树的具体配置,然后是触摸驱动的加载与调试,接着讲触摸校准和坐标映射,最后把触摸屏和RK3588的NPU推理结合起来,做一个带触摸交互的AI demo。整个流程走完,你对RK3588触摸屏开发的理解就不会停留在“照着教程改改设备树”的层面了。
2. RK3588显示与触摸子系统的分层拆解
2.1 显示链路:从VOP到物理接口的信号走向
RK3588的VOP设计比较特殊,它有两个VOP模块,VOP Big和VOP Little。VOP Big支持更高的分辨率和更多的图层叠加,VOP Little则偏向低功耗场景。每个VOP可以绑定到不同的显示接口上,比如VOP Big接HDMI,VOP Little接MIPI DSI。这个绑定关系在设备树里通过rockchip,vop相关的属性来指定。
信号从VOP出来之后,会经过一个叫“显示接口控制器”的模块。以MIPI DSI为例,VOP输出的并行RGB信号会送到DSI控制器,DSI控制器再把它串化成MIPI DSI的差分信号发给屏幕。HDMI则是VOP输出到HDMI TX控制器,再通过PHY发出TMDS信号。eDP类似,走eDP控制器和PHY。
这里有个容易忽略的点:RK3588的MIPI DSI有两个控制器,DSI0和DSI1,但它们能绑定的VOP是有限制的。具体哪个DSI能接哪个VOP,要看芯片手册里的显示子系统框图。我在一块板子上想把DSI0绑到VOP Big上,结果怎么改都不亮,后来查手册才发现DSI0只能绑VOP Little。这种硬约束在设备树里不会报错,但屏幕就是不亮,排查起来很费时间。
2.2 触摸链路:I2C与USB两条路径的取舍
触摸屏的触摸部分通常有两种接口:I2C和USB。I2C触摸屏一般是电容屏的触摸控制器,比如Goodix GT9xx系列、FocalTech FT5x06系列,它们通过I2C总线和RK3588通信,同时还需要一根中断线和一根复位线。USB触摸屏则常见于HDMI触摸显示器,触摸控制器通过USB HID协议上报坐标,系统识别为标准输入设备。
两种方案各有优劣。I2C方案更紧凑,适合嵌入式一体机,但需要配置设备树、加载对应驱动、处理中断和复位GPIO。USB方案即插即用,系统自带HID驱动就能识别,但占用一个USB口,而且有些USB触摸屏的坐标范围需要额外校准。我在工业HMI项目里更倾向I2C方案,因为可以省下一个USB口给其他外设,而且I2C触摸的延迟通常更低。
选择哪种方案,在硬件设计阶段就要定下来。如果屏幕是MIPI DSI接口的,触摸大概率是I2C;如果是HDMI接口的显示器,触摸大概率是USB。也有例外,比如有些eDP屏的触摸走I2C,有些走USB,具体看屏幕规格书。
2.3 软件栈分层:从内核驱动到应用层事件
触摸屏的软件栈从下到上大致分四层。最底层是硬件,触摸控制器芯片。往上是内核驱动,I2C触摸屏对应的是drivers/input/touchscreen/目录下的驱动,比如goodix.c、edt-ft5x06.c。USB触摸屏走的是usbhid驱动。驱动层之上是输入子系统(Input Subsystem),它把触摸事件抽象成input_event结构,通过/dev/input/eventX设备节点暴露给用户空间。
用户空间这一层,常见的做法是用libinput或者直接读/dev/input/eventX。如果跑的是Qt或者LVGL这类GUI框架,它们内部会对接输入子系统。如果自己写应用,可以用evdev接口读取触摸事件。再往上就是应用逻辑,比如根据触摸坐标触发某个AI推理任务。
这个分层理解清楚了,调试的时候就能快速定位问题在哪一层。屏幕不亮是显示链路的问题,触摸没反应是触摸链路的问题,触摸有反应但坐标不对是校准或坐标映射的问题,坐标对了但应用没响应是用户空间的问题。一层一层排查,比盲目改配置高效得多。
3. 设备树配置:MIPI DSI屏幕与I2C触摸的完整落地
3.1 显示部分:DSI控制器与屏幕时序参数
设备树里显示部分的配置主要分两块:DSI控制器和屏幕面板。DSI控制器的节点通常在rk3588s.dtsi里已经定义好了,我们需要在板级设备树里把它使能,并配置时钟、电源、引脚等。屏幕面板则是一个独立的节点,需要描述屏幕的时序参数、分辨率、接口类型等。
时序参数是屏幕能否正常点亮的关键。以一块常见的10.1寸MIPI DSI屏为例,它的时序参数大概是这样:水平活跃像素1280,水平前肩40,水平后肩40,水平同步宽度10;垂直活跃像素800,垂直前肩10,垂直后肩10,垂直同步宽度4。这些参数必须和屏幕规格书完全一致,差一个数就可能不亮或者显示异常。
panel: panel@0 { compatible = "simple-panel"; reg = <0>; backlight = <&backlight>; power-supply = <&vcc3v3_lcd>; enable-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio1 RK_PA1 GPIO_ACTIVE_LOW>; display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <68000000>; hactive = <1280>; vactive = <800>; hfront-porch = <40>; hback-porch = <40>; hsync-len = <10>; vfront-porch = <10>; vback-porch = <10>; vsync-len = <4>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; };clock-frequency这个参数需要算一下。它是像素时钟,等于(hactive + hfront-porch + hback-porch + hsync-len)×(vactive + vfront-porch + vback-porch + vsync-len)× 刷新率。以1280×800@60Hz为例,(1280+40+40+10)×(800+10+10+4)× 60 ≈ 1370 × 824 × 60 ≈ 67.7MHz,取整就是68MHz。这个值如果算错了,屏幕可能显示但刷新率不对,或者干脆不亮。
3.2 触摸部分:I2C节点与中断GPIO配置
I2C触摸的配置分两块:I2C控制器使能和触摸控制器节点。I2C控制器在RK3588上一般用I2C1、I2C3、I2C5等,具体用哪个看硬件原理图。使能I2C控制器需要配置引脚复用和时钟。
&i2c1 { status = "okay"; clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&i2c1m2_xfer>; gt911: touchscreen@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&gpio1>; interrupts = <RK_PB0 IRQ_TYPE_EDGE_FALLING>; reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>; irq-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>; touchscreen-size-x = <1280>; touchscreen-size-y = <800>; status = "okay"; }; };这里有几个细节值得注意。reg是I2C从机地址,GT911的地址可以是0x5d或0x14,取决于上电时INT引脚的电平。如果地址不对,驱动加载了也读不到数据。interrupts配置的是触摸中断,GT911在检测到触摸时会拉低INT引脚,驱动通过中断触发读取坐标。reset-gpios和irq-gpios的GPIO编号要和原理图一致,搞错了驱动初始化就会失败。
touchscreen-size-x和touchscreen-size-y是触摸区域的分辨率,一般和屏幕分辨率一致。但如果触摸区域和显示区域有偏移,这两个值可能需要微调。我遇到过一块屏,触摸区域比显示区域小一圈,后来把这两个值改小才对齐。
3.3 背光与电源:容易被忽略的使能顺序
背光和电源的配置经常被忽略,但它们直接影响屏幕能不能亮。背光通常是一个PWM控制的LED驱动,设备树里需要配置PWM控制器和背光节点。电源则是一个GPIO控制的稳压器,需要在屏幕节点里引用。
backlight: backlight { compatible = "pwm-backlight"; pwms = <&pwm1 0 25000 0>; brightness-levels = <0 4 8 16 32 64 128 255>; default-brightness-level = <6>; power-supply = <&vcc12v_dcin>; }; vcc3v3_lcd: vcc3v3-lcd { compatible = "regulator-fixed"; regulator-name = "vcc3v3_lcd"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; gpio = <&gpio1 RK_PA2 GPIO_ACTIVE_HIGH>; enable-active-high; regulator-always-on; };使能顺序很重要。一般是先给屏幕上电,再释放复位,然后配置DSI控制器,最后开背光。如果顺序反了,屏幕可能初始化失败。我在一块板子上遇到过背光先亮但屏幕不显示的情况,后来把背光使能放到DSI初始化之后就好了。这个顺序在设备树里通过enable-gpios和backlight的引用关系来间接控制,但有时候需要在内核驱动里调整。
4. 触摸驱动加载与调试:从dmesg到evtest的完整链路
4.1 驱动加载确认:dmesg里的关键信息
设备树改完之后,重新编译内核和设备树,烧录到板子上。开机后第一件事是看dmesg,确认驱动有没有加载成功。对于GT911,正常的日志大概是这样:
[ 2.345678] goodix_ts 1-005d: goodix_ts_probe: start [ 2.456789] goodix_ts 1-005d: GTP version: 0x1060 [ 2.567890] goodix_ts 1-005d: GTP config version: 0x5a [ 2.678901] input: goodix_ts as /devices/platform/feac0000.i2c/i2c-1/1-005d/input/input2如果看到probe failed或者i2c transfer failed,说明I2C通信有问题。常见原因有三个:I2C地址不对、GPIO配置错误、上电时序不对。我一般先用i2cdetect -y 1扫一下I2C总线,看能不能扫到0x5d这个地址。扫不到就查硬件,扫到了但驱动加载失败就查设备树。
USB触摸屏的日志更简单,插上之后dmesg会显示usb 1-1: new full-speed USB device和input: ... as /devices/...。如果没反应,先lsusb看设备有没有枚举成功。
4.2 事件节点确认:/dev/input/eventX的对应关系
驱动加载成功后,会在/dev/input/下生成一个eventX节点。用cat /proc/bus/input/devices可以查看每个event节点对应的设备。找到名字里带goodix或者touchscreen的那个,记下它的event编号。
cat /proc/bus/input/devices | grep -A 5 -i touch输出里会有Handlers=event2这样的信息,说明触摸对应的是/dev/input/event2。接下来用evtest工具测试触摸事件。
evtest /dev/input/event2然后在屏幕上触摸,终端里应该会打印出EV_ABS事件,包含ABS_X和ABS_Y的坐标值。如果没有任何输出,说明触摸中断没触发或者驱动没上报事件。如果坐标值一直是0或者固定值,说明I2C读数据有问题。
4.3 常见故障排查:从“没反应”到“坐标乱跳”
触摸没反应是最常见的问题。排查顺序我一般是这样:先确认I2C通信正常(i2cdetect),再确认中断GPIO有变化(用gpio工具或者示波器看INT引脚),然后确认复位GPIO在上电时有没有正确拉低再拉高。GT911的复位时序要求复位拉低至少10ms,然后拉高,之后等待一段时间才能通信。如果复位时间不够,芯片可能没初始化。
坐标乱跳是另一个常见问题。原因可能是触摸区域和显示区域不匹配,或者触摸屏的坐标轴方向反了。GT911驱动里可以通过touchscreen-inverted-x和touchscreen-inverted-y属性来翻转坐标轴。如果触摸区域有偏移,可以调整touchscreen-size-x和touchscreen-size-y,或者在用户空间用libinput的校准工具重新映射。
还有一种情况是触摸有反应但只能点一个点,多点触控失效。这通常是驱动配置里touchscreen-max-points没设置对,或者I2C速率太高导致数据丢失。把I2C速率从400kHz降到100kHz试试,有时候能解决。
5. 触摸校准与坐标映射:让手指和光标对齐
5.1 为什么需要校准:触摸坐标系与显示坐标系的差异
触摸屏的触摸坐标系和显示坐标系是两套独立的系统。触摸控制器上报的是它自己感知到的坐标范围,比如0到4095,而显示分辨率可能是1280×800。如果两者没有正确映射,就会出现手指点在屏幕左上角但光标跑到右下角的情况。
内核驱动里通过touchscreen-size-x和touchscreen-size-y来做初步映射,但有些屏幕的触摸区域和显示区域不是完全重合的,比如触摸区域比显示区域大一圈或者小一圈,这时候就需要更精细的校准。
5.2 内核态校准:设备树属性的微调
内核态的校准主要靠设备树里的几个属性。touchscreen-size-x和touchscreen-size-y定义触摸区域的分辨率,touchscreen-inverted-x和touchscreen-inverted-y控制坐标轴方向,touchscreen-swapped-x-y控制X和Y轴是否交换。
我遇到过一块屏,触摸的X轴和Y轴是反的,手指横向滑动光标纵向移动。加上touchscreen-swapped-x-y之后就正常了。还有一块屏,触摸区域比显示区域小,手指点到屏幕边缘时光标到不了边缘,把touchscreen-size-x从1280改成1200之后,边缘就能点到了。
这些属性的调整需要反复测试。我的做法是先在屏幕上画四个角,用evtest看每个角对应的坐标值,然后根据坐标值反推需要调整哪个属性。
5.3 用户态校准:libinput与自定义映射
如果内核态校准不够精细,可以在用户态做进一步映射。libinput提供了一个校准工具,可以设置触摸区域的变换矩阵。
libinput calibrate touchscreen这个命令会引导你在屏幕上点几个点,然后自动计算校准矩阵。校准结果会保存在/etc/libinput/local-overrides.quirks里。
如果不用libinput,也可以自己写一个映射程序。思路是读取/dev/input/eventX的原始坐标,然后用一个线性变换把它映射到屏幕坐标。线性变换的参数可以通过最小二乘法拟合得到,采集几个已知点的原始坐标和屏幕坐标,解方程就行。
import numpy as np # 采集的原始坐标和对应的屏幕坐标 raw_points = np.array([[100, 200], [1000, 200], [100, 700], [1000, 700]]) screen_points = np.array([[0, 0], [1280, 0], [0, 800], [1280, 800]]) # 求解仿射变换矩阵 A = np.hstack([raw_points, np.ones((4, 1))]) matrix, _, _, _ = np.linalg.lstsq(A, screen_points, rcond=None) print(matrix)这个矩阵可以用在用户空间程序里,把原始坐标转换成屏幕坐标。这种方法比内核态校准更灵活,但会增加一点延迟。
6. 触摸屏与RK3588 NPU联动:做一个带触摸交互的AI Demo
6.1 场景设计:触摸选择目标区域触发YOLOv8推理
光把触摸屏点亮还不够,RK3588的NPU才是这颗芯片的杀手锏。我设计了一个小demo:屏幕上显示摄像头画面,用户用手指在画面上画一个矩形框,程序把框内的图像裁剪出来,送给NPU跑YOLOv8推理,然后把检测结果画在屏幕上。
这个demo涉及几个环节:摄像头采集、触摸事件读取、图像裁剪、NPU推理、结果显示。触摸屏在这里的作用是提供交互入口,让用户可以选择感兴趣的区域,而不是对整帧图像做推理。这样做的好处是减少NPU的计算量,提高响应速度。
6.2 触摸事件读取与图像坐标映射
触摸事件通过evdev接口读取。Python里可以用evdev库,C里可以直接读/dev/input/eventX。读取到的坐标是屏幕坐标,需要映射到摄像头画面的坐标。如果屏幕分辨率和摄像头分辨率不一致,需要做一个缩放。
import evdev device = evdev.InputDevice('/dev/input/event2') screen_width, screen_height = 1280, 800 camera_width, camera_height = 1920, 1080 for event in device.read_loop(): if event.type == evdev.ecodes.EV_ABS: if event.code == evdev.ecodes.ABS_X: x = event.value * camera_width // screen_width elif event.code == evdev.ecodes.ABS_Y: y = event.value * camera_height // screen_height这段代码把屏幕坐标转换成摄像头坐标。实际使用中还需要处理触摸按下、移动、抬起的事件,记录矩形的起点和终点。
6.3 NPU推理调用:RKNN API的触摸触发流程
RK3588的NPU通过RKNN API调用。流程是:加载RKNN模型,初始化运行时,设置输入,运行推理,获取输出。YOLOv8的RKNN模型需要先转换,把PyTorch模型转成ONNX,再用RKNN Toolkit转成RKNN格式。
from rknnlite.api import RKNNLite rknn = RKNNLite() rknn.load_rknn('yolov8n.rknn') rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0) # 触摸触发后裁剪图像 cropped = frame[y1:y2, x1:x2] outputs = rknn.inference(inputs=[cropped])推理结果需要做后处理,解析出边界框、类别、置信度,然后画到屏幕上。整个流程的延迟主要取决于模型大小和输入分辨率。YOLOv8n在RK3588上跑640×640输入,单帧推理大概20到30毫秒,加上前后处理,整体能在50毫秒内完成,触摸交互的体验比较流畅。
6.4 实测性能与优化:从30fps到60fps的调优记录
实测下来,摄像头采集30fps,NPU推理30fps,显示刷新60fps,整体是流畅的。但如果同时跑多个模型或者输入分辨率提高到1080p,帧率会掉到15fps左右。优化方向有几个:降低输入分辨率、使用NPU的多核并行、把前后处理放到GPU上做。
我把输入分辨率从640×640降到416×416,帧率从30fps提到了45fps,检测精度下降不多。另外,RK3588的NPU有三个核心,可以用RKNNLite.NPU_CORE_0_1_2让三个核心并行推理,吞吐量能提升两倍多。不过多核并行需要模型支持,有些模型拆分后会有精度损失。
触摸屏的响应延迟也要考虑。I2C触摸的延迟一般在10毫秒以内,USB触摸在20毫秒左右。如果应用对延迟敏感,I2C方案更合适。另外,触摸事件的读取最好放在独立线程里,避免阻塞主循环。
7. 几个让我踩过坑的细节
7.1 设备树引脚复用冲突:一个GPIO引发的血案
RK3588的引脚复用很灵活,但也很容易冲突。我在一块板子上把触摸的复位GPIO配成了GPIO1_A1,结果屏幕怎么都不亮。后来查原理图发现,GPIO1_A1同时被用作MIPI DSI的某个控制信号,两个功能冲突了。设备树编译不会报错,但运行时两个功能都不正常。
排查这种问题,我一般用cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinmux看每个引脚的复用状态。如果发现某个引脚被两个节点同时引用,就要改其中一个的引脚编号。选引脚的时候尽量避开那些被显示接口、以太网、PCIe占用的引脚,具体哪些引脚被占用了,看芯片手册的引脚复用表。
7.2 I2C地址冲突:GT911的0x5d和0x14
GT911的I2C地址由复位时的INT引脚电平决定。INT拉低时地址是0x5d,INT拉高时地址是0x14。如果设备树里写的地址和实际地址不一致,驱动加载会失败。我遇到过一块屏,设备树里写的是0x5d,但实际地址是0x14,因为硬件设计时INT引脚默认拉高了。
解决办法有两个:一是改设备树里的reg属性,二是改硬件把INT引脚默认拉低。软件改更简单,但要注意驱动里可能对地址有硬编码。GT911的驱动里一般会同时尝试两个地址,但有些版本的驱动只试一个。
7.3 触摸中断丢失:从边沿触发到电平触发
触摸中断配置成边沿触发时,如果中断处理函数执行时间太长,可能会丢失后续中断。我遇到过快速滑动时触摸断断续续的情况,后来把中断类型从IRQ_TYPE_EDGE_FALLING改成IRQ_TYPE_LEVEL_LOW,问题就解决了。电平触发的好处是只要INT引脚还是低电平,中断就会持续触发,不会丢失。
不过电平触发也有缺点,如果INT引脚一直拉低,中断会一直触发,CPU占用率会上去。GT911的正常工作方式是触摸时拉低,读取数据后拉高,所以电平触发是安全的。但如果I2C通信失败,INT可能一直拉低,这时候需要在驱动里加超时处理。
7.4 屏幕闪烁与VPU负载的关系
屏幕闪烁这个问题困扰了我很久。单独跑显示没问题,一旦同时跑NPU推理和视频解码,屏幕就开始闪。后来用top看CPU和VPU的负载,发现VPU解码4K视频时占用率很高,和VOP抢带宽。RK3588的内存带宽是有限的,VPU、GPU、CPU、VOP都要用,带宽不够时VOP就拿不到足够的数据,导致屏幕闪烁。
解决办法有几个:降低视频解码分辨率、把显示刷新率从60Hz降到30Hz、或者用VOP Big接主屏,VOP Little接副屏,把负载分散开。我最后是把视频解码从4K降到1080p,屏幕就不闪了。如果必须跑4K,可以考虑用硬件叠加层,让VOP直接读VPU的输出,减少内存拷贝。
8. 从点亮到产品化:还需要考虑的事
触摸屏点亮只是第一步,真正做产品还有很多细节要处理。比如触摸屏的EMC性能,工业环境下触摸屏容易受干扰,导致误触或者不响应。解决方法是加滤波电容、缩短I2C走线、用屏蔽线。还有触摸屏的防水防尘,电容屏在潮湿环境下容易误触,需要做防水算法或者用防水触摸控制器。
另外,量产时每块屏的触摸参数可能有细微差异,需要做批量校准。我的做法是在工厂测试环节加一个触摸校准步骤,把每块屏的校准参数写到EEPROM或者设备树里。这样用户拿到手就不需要再校准了。
RK3588的触摸屏开发,从设备树配置到驱动调试,再到和NPU联动,整个链路走下来,对嵌入式系统的理解会深很多。我一开始也是照着教程一步步来,踩了不少坑才慢慢摸清楚每个环节的原理。希望这些经验能帮你少走点弯路。