想接着跟客户演示“我们这套板子能不能当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 DRD(Dual 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然后挂载configfs:
mount -t configfs none /sys/kernel/config3.2 创建UVC gadget的标准流程
我建议把整个创建过程写成脚本,方便反复执行。下面这个脚本我实测过,注释也标清楚了每个文件的含义:
#!/bin/bash GADGET_NAME="uvc_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/PID,0x1d6b是Linux官方VID,0x0104可自定义 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 # 设置帧间隔列表,单位是100ns,333333表示30fps echo 333333 > functions/uvc.0/streaming/mjpeg/m/720p/dwFrameIntervalUVC帧率换算逻辑:帧间隔单位是100ns,333333就是1/30秒,对应30fps;666666就是15fps,100000就是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输出mjpeg:
ffmpeg -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 scale=1280:720 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0帧率方面,建议加上-r 30强制输出帧率,防止视频文件帧率和UVC配置不一致导致画面时快时慢:
ffmpeg -re -stream_loop -1 -i demo.mp4 \ -vf scale=1280:720 \ -r 30 \ -pix_fmt yuyv422 \ -f v4l2 /dev/video0实操心得:UVC的帧率匹配尽量用摄像头驱动侧配置为准。我遇到过配置了60fps但视频源只有25fps,结果主机端亮度偏高、画面跳帧。后来直接把UVC描述符改成30fps,一切正常。
4.4 补充:用GStreamer怎么走
如果习惯了GStreamer,管道可以这样写:
gst-launch-1.0 filesrc location=demo.mp4 ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! video/x-raw,format=YUY2,width=1280,height=720,framerate=30/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/video0,UVC节点是/dev/video3:
ffmpeg -s 1920x1080 -pix_fmt nv12 -i /dev/video0 \ -vf scale=1280: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自带RGA(Raster Graphic Acceleration)模块,专门做2D图形操作,比如格式转换、旋转、缩放、裁剪。使用RGA可以把颜色转换从CPU卸载掉,几乎不占用CPU资源。
使用FFmpeg的h264_rkmpp或rockchip系列编解码器和RGA滤镜可以这样用:
ffmpeg -c:v h264_rkmpp -i /dev/video0 \ -vf "scale_rkrga=1280:720,format=yuyv422" \ -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,整条通路跑1080P@30fps没有压力。
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_rkrga=1280:720,format=yuyv422 \ -f v4l2 /dev/video3注意加了-rtsp_transport tcp,RTSP默认用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-count=1手动抓一帧试试。 - 检查格式是否匹配:
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 scale=1280: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里找线索,基本都能解决。