ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RK3588变身USB摄像头:UVC-gadget从内核配置到视频流输出全指南

RK3588变身USB摄像头:UVC-gadget从内核配置到视频流输出全指南 想接着跟客户演示“我们这套板子能不能当USB摄像头用”结果手头没有现成的USB摄像头模组或者你正在做RK3588的机器视觉项目需要在Windows电脑上把板子输出的视频流当成一个标准摄像头打开不想多买采集卡也不想折腾网络推流。这个场景Linux下UVC-gadget就能直接搞定。简单说就是让RK3588变成一台“USB摄像头”视频文件、RTSP流、甚至摄像头实时画面都可以作为视频源主机端插上USB线就能识别免驱动Windows、Linux、macOS基本通吃。这篇文章我就把完整的实操链路拆开讲从内核配置、设备树、configfs挂载到把视频文件当作视频源灌进UVC设备再到RK3588特有的MPP/RGA硬件加速玩法全程都是拿RK3588开发板实际跑过的东西。适合正在做嵌入式平台视频产品、或用RK3588做视觉设备原型验证的工程师收藏。1. UVC-gadget到底是什么为什么RK3588特别适合干这件事1.1 从USB gadget框架说起Linux内核里有一套USB Gadget框架作用是让设备比如RK3588开发板作为USB从机模拟成鼠标、键盘、网卡、串口、摄像头等设备。所谓UVC全称USB Video Class是USB组织定义的标准摄像头设备类。只要设备实现了UVC协议主机端Windows/macOS/Linux就不用装私有驱动系统自带UVC驱动就能识别成一个Camera设备应用层直接调DirectShow、AVFoundation或者V4L2接口就能拿到视频数据。在RK3588上做这件事关键点在于它的USB控制器和OTG角色切换。RK3588有一个Type-C口内部连接的是USB 3.0 DRDDual Role Device控制器可以动态地在Host模式和Device模式之间切换。很多开发板出厂默认把Type-C口配成了Host模式方便接U盘、鼠标但是我们要把RK3588作为虚拟摄像头必须让这个控制器工作在Device模式并且把USB线插到这个Type-C口上。1.2 为什么不是直接用v4l2loopback很多同学知道Linux下有v4l2loopback这个内核模块可以把FFmpeg的输出导出一个虚拟V4L2设备然后被浏览器、OBS等软件调用。这个方案在单机环境下很好用但它是纯本地的只能让同一台Linux机器上的应用看到这个“虚拟摄像头”。如果目的只是“RK3588本机跑一个程序把它当成输入源”v4l2loopback确实够用。但如果你想把视频流传给另一台电脑、手机、平板或者给一个不装任何软件的机器使用那就没有比UVC-gadget更好的选择了——一根USB线主机端识别为摄像头零依赖零网络配置。UVC-gadget相当于在USB协议栈层面实现了摄像头设备枚举而不是在系统内部伪造一个V4L2设备。一个是“往外输出标准USB信号”一个是“内部自娱自乐”两者定位完全不同。1.3 这个玩法能解决哪些实际问题我做过的几种典型应用基本都能体现UVC-gadget的价值产品演示板子里面循环播放一段产品演示视频通过USB线连到展厅电脑电脑上打开相机App屏幕里就在播放视频给客户看效果不需要额外采集卡。摄像头替代板子接了真正的MIPI摄像头做ISP处理后输出给上位机这时候UVC-gadget就相当于通路把RK3588变成一个高端USB摄像头。视频源注入在RK3588上跑一个AI识别程序识别结果叠加到视频画面里然后作为UVC摄像头输出立刻就能在PC端看到带推理框的实时画面。协议学习做USB摄像头协议栈验证用可控的视频数据去测试主机端上位机软件的兼容性。2. 环境准备RK3588开发板、内核选项和设备树2.1 硬件选型与接线注意事项我手头用的是瑞芯微官方RK3588 EVB板以及其他几个厂家的RK3588核心板测试下来原理相同。硬件上需要注意确保USB线支持数据传输不要用“只充电不传数据”的线。最好插到RK3588原生的Type-C口而不是通过USB HUB扩展出来的口。建议给板子单独供电不要靠电脑USB口供电。RK3588的功耗不低虚拟摄像头传输数据时电流会波动靠USB Host口供电容易掉盘或者设备反复枚举。在接线前先把板子的USB控制器确认清楚。RK3588有两路USB 3.0 Host控制器和一路USB 3.0 DRD控制器DRD控制器通常接在Type-C0口上。使用lsusb -t可以查看当前USB拓扑但更重要的是要先确认当前OTG口处于什么角色cat /sys/class/udc/*/role如果输出是host说明当前处于主机模式需要切到device。2.2 内核配置项检查大部分RK3588的Ubuntu/Debian镜像内核已经开启了usb_f_uvc但部分精简内核或SDK工程默认没开。可以通过以下方式确认zcat /proc/config.gz | grep CONFIG_USB_CONFIGFS zcat /proc/config.gz | grep CONFIG_USB_F_UVC如果看不到配置就在内核源码目录执行make menuconfig按以下路径打开Device Drivers - USB support - USB Gadget Support - * USB Gadget Functions * UVC Gadget另外还要确保开启了ConfigFS支持USB Gadget Support - * USB Gadget functions configurable through configfs编译选项对应CONFIG_USB_CONFIGFS、CONFIG_USB_CONFIGFS_F_UVC。内核版本建议5.10以上RK3588的SDK内核基本都在5.10或更高。2.3 设备树中的OTG模式配置RK3588设备树里Type-C0对应的USB控制器节点通常是usbdrd3_0: usbdrd3_0 { compatible rockchip,rk3588-dwc3; dr_mode otg; ... }; usbdrd_dwc3_0: usbdrd_dwc3_0 { compatible snps,dwc3; dr_mode otg; extcon u2phy0_otg; ... };设备树里dr_mode支持host、peripheral、otg三种值。要实现动态切换配置为otg模式并配合extcon来感知Type-C插入状态。实际调试中我习惯在设备树里先把dr_mode配成otg这样在开发阶段可以用sysfs随时切换角色不用反复编译设备树。如果产品场景明确只需要Device模式直接设peripheral省事。修改设备树后重新编译内核和设备树写入启动分区重启后用dmesg | grep dwc3确认控制器遍历成功。2.4 快速切换OTG角色的操作启用OTG模式后角色切换可以通过写sysfs实现# 查看当前角色 cat /sys/class/udc/usb0/role # 强制切换为device echo device /sys/class/udc/usb0/role不同内核版本对role节点的支持有差异有的平台需要写/sys/bus/platform/drivers/dwc3/.../role或者使用gadget接口。如果SDK里有usb_role_switch也可以用libcomposite自带的角色管理。切换后再次检查cat /sys/class/udc/usb0/role # 期望输出 device注意部分开发板Type-C口默认接了CC逻辑芯片插入USB线时会自动切换到设备模式无需手动写sysfs。但为了调试稳定建议还是确认状态。3. 用configfs把UVC-gadget“拼装”出来3.1 configfs的分层思想Linux USB Gadget的ConfigFS接口把USB设备的描述符拆成了目录结构通过创建目录、写属性文件来构造一个USB设备。整个过程就像搭积木创建一个gadget添加配置配置里添加function。在RK3588的Ubuntu系统上先把必要模块加载进来modprobe libcomposite modprobe usb_f_uvc然后挂载configfsmount -t configfs none /sys/kernel/config3.2 创建UVC gadget的标准流程我建议把整个创建过程写成脚本方便反复执行。下面这个脚本我实测过注释也标清楚了每个文件的含义#!/bin/bash GADGET_NAMEuvc_gadget GADGET_PATH/sys/kernel/config/usb_gadget/$GADGET_NAME # 如果之前创建过先清理 if [ -d $GADGET_PATH ]; then echo $GADGET_PATH/UDC rm -rf $GADGET_PATH fi mkdir -p $GADGET_PATH cd $GADGET_PATH # 设置VID/PID0x1d6b是Linux官方VID0x0104可自定义 echo 0x1d6b idVendor echo 0x0104 idProduct echo 0x0100 bcdDevice echo 0x0200 bcdUSB # 设置厂商和产品字符串 mkdir -p strings/0x409 echo Rockchip strings/0x409/manufacturer echo RK3588 Virtual Camera strings/0x409/product echo 123456789 strings/0x409/serialnumber # 创建配置 mkdir -p configs/c.1 mkdir -p configs/c.1/strings/0x409 echo UVC Config configs/c.1/strings/0x409/configuration # 创建UVC function注意function名固定为uvc.0 mkdir -p functions/uvc.0创建完成后functions/uvc.0目录下会出现streaming、control等子目录接下来就要往streaming里填具体的视频格式了。3.3 视频格式描述符填充UVC设备要告诉主机“我能输出什么格式、什么分辨率、多少帧率”这些信息就写在streaming目录下。对于常见的YUY2格式和MJPEG格式可以这样建目录# YUY2格式支持640x480和1280x720 mkdir -p functions/uvc.0/streaming/mjpeg/m/720p mkdir -p functions/uvc.0/streaming/mjpeg/m/1080p mkdir -p functions/uvc.0/streaming/uncompressed/yuy2/360p # 写入各分辨率参数 echo 1280 functions/uvc.0/streaming/mjpeg/m/720p/wWidth echo 720 functions/uvc.0/streaming/mjpeg/m/720p/wHeight echo 333333 functions/uvc.0/streaming/mjpeg/m/720p/dwDefaultFrameInterval echo 30 functions/uvc.0/streaming/mjpeg/m/720p/bDefaultFrameIndex # 设置帧间隔列表单位是100ns333333表示30fps echo 333333 functions/uvc.0/streaming/mjpeg/m/720p/dwFrameIntervalUVC帧率换算逻辑帧间隔单位是100ns333333就是1/30秒对应30fps666666就是15fps100000就是10fps。这个不要搞错写错的话主机端帧率会显示得很奇怪。配置完后把function绑定到配置ln -s functions/uvc.0 configs/c.1/最后绑定UDC让USB设备真正“上线”echo usb0 /sys/kernel/config/usb_gadget/$GADGET_NAME/UDC这里的usb0是指/sys/class/udc/usb0这个UDC控制器名。提示绑定UDC之前一定要确保OTG处于device模式否则会报No such device或Device or resource busy。3.4 主机端识别验证绑定成功后主机端应该有反应。在Windows下会弹出声“找到新硬件”设备管理器里能看到一个“USB 摄像头”或者“UVC Camera”。在另一台Linux电脑上可以用lsusb看到Bus 001 Device 003: ID 1d6b:0104 Rockchip RK3588 Virtual Camera到这一步UVC设备枚举已经OK了但还没有视频数据流主机端打开摄像头会黑屏或报错。接下来就要解决“视频源怎么灌进去”。4. 把本地视频文件变成虚拟摄像头的完整链路4.1 几种视频源注入方案的对比UVC-gadget在主机端显示为一个/dev/videoX设备也可能是/dev/video0、/dev/video1取决于系统分配。往这个设备节点写视频帧主机端就能看到画面。具体方案有几种方案实现方式优点缺点FFmpeg直接输出使用v4l2sink输出到UVC节点简单直接格式转换由FFmpeg完成大分辨率高码率时性能一般v4l2loopback中转先出到loopback再复制到UVC节点便于叠加软件特效多一次拷贝延迟增加GStreamer管道走v4l2src/v4l2sink插件管道灵活容易集成AI推理调试门槛高自研C/C程序libv4l2直接写帧性能最佳可控性最强需要写代码我实际项目里最常用的是FFmpeg直接输出因为简单、跨格式能力强适合快速验证。如果做产品化再改成C程序直接调用V4L2接口写帧。4.2 用FFmpeg把本地视频“喂”给UVC节点先确认UVC gadget对应的video节点编号v4l2-ctl --list-devices # 输出里找到 RK3588 Virtual Camera 对应的 /dev/videoX假设UVC设备是/dev/video0本地视频文件是demo.mp4那么核心命令是ffmpeg -re -stream_loop -1 -i demo.mp4 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0这个命令的意思是循环读取demo.mp4-stream_loop -1然后按原始帧率节奏输出-re像素格式转成yuyv422通过V4L2接口写入/dev/video0。这里最关键的坑是像素格式必须和UVC描述符一致。如果你在configfs里配置的是MJPEG格式就要让FFmpeg输出mjpegffmpeg -re -stream_loop -1 -i demo.mp4 \ -vcodec mjpeg -q:v 5 \ -f v4l2 /dev/video0如果不一致V4L2驱动会直接报Invalid argument或者主机端显示画面颜色错乱、花屏。4.3 分辨率与帧率匹配问题ffmpeg输出分辨率必须落在UVC配置的分辨率列表里否则也会报错或者被主机端拒绝。比如configfs里只配置了720p那就先做缩放ffmpeg -re -stream_loop -1 -i demo.mp4 \ -vf scale1280:720 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0帧率方面建议加上-r 30强制输出帧率防止视频文件帧率和UVC配置不一致导致画面时快时慢ffmpeg -re -stream_loop -1 -i demo.mp4 \ -vf scale1280:720 \ -r 30 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0实操心得UVC的帧率匹配尽量用摄像头驱动侧配置为准。我遇到过配置了60fps但视频源只有25fps结果主机端亮度偏高、画面跳帧。后来直接把UVC描述符改成30fps一切正常。4.4 补充用GStreamer怎么走如果习惯了GStreamer管道可以这样写gst-launch-1.0 filesrc locationdemo.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! video/x-raw,formatYUY2,width1280,height720,framerate30/1 ! v4l2sink device/dev/video0GStreamer的优点是中间可以随意插入各种滤镜、文字叠加、画中画对做演示场景很方便。但GStreamer的插件版本差异较大建议先用FFmpeg验证通路再上GStreamer搞花活。5. 高级玩法接入真实摄像头、RTSP流和RK3588硬件加速5.1 把MIPI摄像头画面导出为USB摄像头RK3588最常见的场景是接MIPI摄像头通过ISP处理后输出视频帧。如果想把这个实时画面通过UVC输出重点在于打通V4L2设备到UVC设备的数据通路。RK3588的MIPI摄像头在系统中通常注册为/dev/video0或/dev/videoX取决于驱动的media graph。可以用media-ctl -p查看拓扑。假设摄像头节点是/dev/video0UVC节点是/dev/video3ffmpeg -s 1920x1080 -pix_fmt nv12 -i /dev/video0 \ -vf scale1280:720 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video3这里我特意提一下NV12转YUY2。RK3588的ISP默认输出可能是NV12或NV16而UVC描述符里如果配置的是UNCOMPRESSED/YUY2就必须做颜色空间转换。这一步在CPU上跑很费资源4K转720P都可能会卡所以要借助RK3588的RGA硬件加速模块。5.2 RGA硬件加速做格式转换和缩放RK3588自带RGARaster Graphic Acceleration模块专门做2D图形操作比如格式转换、旋转、缩放、裁剪。使用RGA可以把颜色转换从CPU卸载掉几乎不占用CPU资源。使用FFmpeg的h264_rkmpp或rockchip系列编解码器和RGA滤镜可以这样用ffmpeg -c:v h264_rkmpp -i /dev/video0 \ -vf scale_rkrga1280:720,formatyuyv422 \ -f v4l2 /dev/video3scale_rkrga是FFmpeg的Rockchip RGA滤镜需要FFmpeg带--enable-rkrga编译或者在RK3588的SDK里直接用瑞芯微提供的FFmpeg版本。如果自己写C程序也可以直接用librga库做转换。RGA的一个典型调用流程打开/dev/rga文件描述符初始化rga_info_t设置源图像格式、目标图像格式然后ioctl(fd, RGA_BLIT_SYNC, info)。RGA支持的颜色格式转换中NV12-YUY2是常见组合性能很强1080P的转换耗时通常不到1ms整条通路跑1080P30fps没有压力。5.3 把RTSP流变成虚拟摄像头很多项目里视频源不在本地而是来自网络摄像头或视频服务器的RTSP流。RK3588接网线或者WiFi把RTSP拉回来再通过UVC输出就等价于把一个IP摄像头变成了“USB摄像头”。这个场景我用的是FFmpeg加rkmpp硬解码ffmpeg -rtsp_transport tcp -i rtsp://192.168.1.100:554/live \ -c:v h264_rkmpp \ -vf scale_rkrga1280:720,formatyuyv422 \ -f v4l2 /dev/video3注意加了-rtsp_transport tcpRTSP默认用UDP传输在某些网络环境容易丢包卡顿TCP更稳。如果视频流本身是H.265编码把h264_rkmpp换成hevc_rkmpp。实操注意RTSP拉流后输出到UVC链路有多个缓冲环节延迟会累积。如果追求低延迟建议RTSP服务端把GOP设小一点UVC输出端关闭额外缓存。实测1080P RTSP转UVC端到端延迟能做到200ms左右基本够用。5.4 AI识别画面叠加后输出顺带提一个进阶场景。RK3588跑YOLOv8或RKNN模型做AI识别识别出的框通过RGA叠加到视频帧上再通过UVC输出立刻就能在PC端看到“带智能分析结果的真USB摄像头”。实现上就是用RKNN推理拿到目标框坐标然后用RGA画框画完的帧送给UVC设备。这个方案我用来做AI盒子产品的原型验证相当实用。整个管线复杂度主要在内存缓冲区的生命周期管理需要注意RKNN输出的图像缓冲和RGA输入输出缓冲都要做DMA内存映射否则拷贝开销会吃掉硬件加速带来的收益。6. 常见问题与排查技巧实录6.1 主机端识别不到UVC设备检查OTG角色cat /sys/class/udc/usb0/role必须是device。确认UDC是否绑定cat /sys/kernel/config/usb_gadget/uvc_gadget/UDC应该有udc名字。检查USB线是否支持数据传输。查看内核日志dmesg | grep -i uvc、dmesg | grep -i dwc3。如果UDC绑定报Device or resource busy很可能有其他gadget已经占用了控制器先清掉再绑。常见于系统里预装了一些g_serial、g_ether之类的gadget。6.2 枚举成功但打开摄像头黑屏确认ffmpeg是不是真在往UVC节点写数据v4l2-ctl -d /dev/videoX --stream-mmap --stream-count1手动抓一帧试试。检查格式是否匹配v4l2-ctl -d /dev/videoX --list-formats-ext对比UVC描述符里的格式。检查分辨率主机会按dwMaxVideoFrameSize分配缓冲区如果写入的帧超过这个值会直接失败。6.3 颜色偏色或绿屏典型的颜色空间不匹配问题。主机端摄像头的默认颜色编码和UVC输出不一致比如UVC配置是NV12但描述符写成了YUY2。另外要注意RGB与YUV色域范围Full range和Limited range也会导致画面发灰或发白。RK3588的ISP输出有可能是BT.601或者BT.709的色域UVC标准里默认可能走BT.601需要加colorprimaries之类的属性或者用RGA做色域转换。6.4 掉帧、卡顿、延迟高CPU被软件转换占满。优先用RGA和RKMPP做硬件加速。UVC的bInterval配置。USB 2.0高速模式下UVC的等时传输间隔设置不当会导致吞吐量上不去。检查USB枚举速度lsusb -t看是不是连在USB 3.0速率下如果降级到USB 1.1高分辨率会非常卡。6.5 踩过的其他坑文件句柄泄漏循环写帧的情况下如果代码每次写帧都打开/dev/videoX而不关闭一段时间后文件描述符耗尽系统其他业务会异常。buffer数量V4L2采集端推荐至少申请4个缓冲区否则帧间隔容易出现抖动。连续UVC配置切换如果动态修改UVC描述符比如从720P切到1080P必须先unbind UDC再重新bind否则主机端可能识别到错误的分辨率信息。电源干扰RK3588满载运行时如果USB供电不稳UVC设备会反复枚举这时候务必用独立电源。7. 命令速查与实用配置清单我整理了一个从零开始到跑通的最小命令序列方便复制# 1. 确认OTG角色 cat /sys/class/udc/usb0/role # 2. 加载模块并挂载configfs modprobe libcomposite modprobe usb_f_uvc mount -t configfs none /sys/kernel/config # 3. 创建gadget参见第三章脚本 # 4. 绑定UDC echo usb0 /sys/kernel/config/usb_gadget/uvc_gadget/UDC # 5. 查看UVC video节点 v4l2-ctl --list-devices # 6. 用本地视频输出到UVC ffmpeg -re -stream_loop -1 -i demo.mp4 \ -vf scale1280:720 \ -r 30 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0FFmpeg输出前的每一行参数都值得反复琢磨-re控制读取速度-stream_loop -1控制循环-vf scale保证分辨率匹配-pix_fmt保证颜色格式匹配-f v4l2指定输出方式。少一个都可能出问题。8. 写在最后的几点体会UVC-gadget在RK3588上跑通不难难的是把一个看似简单的USB摄像头做成稳定、低延迟、不挑主机的产品。从我实际测试的经验看几个方向特别值得继续深入一是结合RK3588的VPU做硬解码再输出CPU占用能压到5%以下二是把UVC描述符里的参数做成动态可配置通过上位机软件实时调整分辨率帧率三是配合RK3588的多路视频输入做成多路UVC摄像头同时输出的复杂系统。如果只想要一个快速demo照着上面的步骤走一遍半小时内就能让电脑看到一个会播视频的“USB摄像头”。但如果你准备把这套方案放进正式产品强烈建议再打磨三件事USB线材质量与屏蔽、电源纹波控制、以及设备枚举失败后的自动恢复机制。这个玩法本身是Linux USB Gadget生态里一个极具代表性的应用内核代码和文档也都比较成熟有问题多在/sys/kernel/config和dmesg里找线索基本都能解决。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进