使用 esp-iot-solution 的 usb_device_uvc 组件将 ESP32 打造为 USB UVC 摄像头设备
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
usb_device_uvc是 esp-iot-solution 仓库中面向 ESP32-S2 / ESP32-S3 / ESP32-P4 的 USB UVC(USB Video Class)设备驱动组件,它让 MCU 能够以标准摄像头设备的身份出现在 USB 主机(PC、平板等)上,并通过回调函数将相机或任意图像源包装成符合 UVC 规范的设备。读完本文,你将掌握如何添加该组件、理解四个核心回调函数与uvc_device_config()/uvc_device_init()的完整用法、配置分辨率与帧率等 Kconfig 选项,并能参照仓库示例将 ESP32-S3-EYE 变成即插即用的 USB 网络摄像头。
组件概览与能力边界
usb_device_uvc组件位于 components/usb/usb_device_uvc,其核心实现为 usb_device_uvc.c,对外 API 声明在 include/usb_device_uvc.h。组件底层基于 TinyUSB 协议栈,由 tusb 目录下的tusb_config.h、usb_descriptors.c与uvc_frame_config.h提供描述符和帧配置支持。
根据 usb_device_uvc.rst 与 README.md,该组件支持:
- 通过 UVC 流接口进行视频流传输(Stream 接口);
- 同时支持 Isochronous(等时)与 Bulk(批量)两种传输模式;
- 支持多种分辨率和帧率(默认支持 QVGA/HVGA/VGA/SVGA/HD/FHD 多档,并允许自定义多帧尺寸列表);
- 在开启
CONFIG_UVC_SUPPORT_TWO_CAM后支持双摄像头同时出流。
需要说明的是:组件当前不提供deinitAPI 的公开承诺(文档明确指出"该组件仅提供一个 API 用于配置 UVC 设备。由于驱动基于 TinyUSB 堆栈,因此未提供 deinit API"),不过在较新版本源码中已额外实现了uvc_device_deinit()(见 usb_device_uvc.c),可用于停止任务、拆除 USB 栈与释放 PHY,读者可按需使用。
将组件添加到项目
与仓库中其他组件一致,usb_device_uvc通过 ESP-IDF 组件管理器进行依赖管理。在项目根目录执行:
idf.py add-dependency "espressif/usb_device_uvc=*"该命令会把组件写入项目的idf_component.yml,在后续 CMake 配置步骤中自动从组件仓库下载,无需手动拷贝源码。
用户参考:配置结构与四个回调函数
组件对外只要求用户完成两件事:填充uvc_device_config_t配置结构并调用uvc_device_config()注册,然后调用uvc_device_init()启动设备。由于图像数据完全由用户提供,组件本身并不关心画面来源——它可以是真实摄像头,也可以是从 Flash/内存中读取的 JPEG 图片(测试用例就是这么做的)。
帧缓冲区结构 uvc_fb_t
uvc_fb_t(定义于 usb_device_uvc.h)描述一帧待发送的数据:
| 字段 | 类型 | 含义 |
|---|---|---|
buf | uint8_t * | 帧数据指针 |
len | size_t | 帧数据字节数 |
width | size_t | 图像宽度(像素) |
height | size_t | 图像高度(像素) |
format | uvc_format_t | 帧格式(JPEG 或 H264) |
timestamp | struct timeval | 帧的时间戳(自启动起) |
uvc_format_t枚举支持UVC_FORMAT_JPEG与UVC_FORMAT_H264两种格式(见 usb_device_uvc.h)。
四个回调函数
组件通过四个回调把"什么时候启动相机、什么时候取帧、什么时候还帧、什么时候停止"完全交给用户实现(回调类型定义见 usb_device_uvc.h):
#include "usb_device_uvc.h" static esp_err_t camera_start_cb(uvc_format_t format, int width, int height, int rate, void *cb_ctx) { // 用户可以在这里初始化相机 // 相机应根据给定的格式、宽度、高度和帧率进行初始化 return ESP_OK; } static void camera_stop_cb(void *cb_ctx) { // 用户代码(停止相机采集) return; } static uvc_fb_t* camera_fb_get_cb(void *cb_ctx) { // 用户代码以返回图像帧缓冲区 // 相机应准备下一帧,并返回帧缓冲区 return uvc_fb; } static void camera_fb_return_cb(uvc_fb_t *fb, void *cb_ctx) { // 在帧缓冲区被复制到传输缓冲区后返回 // 用户代码以回收帧缓冲区 return; }各回调的语义与触发时机如下:
start_cb:主机(Host)以特定格式/分辨率/帧率打开 UVC 设备时被调用,返回值非ESP_OK会被组件视为相机初始化失败,从而向主机返回 UVC 错误(见 usb_device_uvc.c 中tud_video_commit_cb的实现);stop_cb:主机关闭设备、USB 总线挂起(tud_suspend_cb)或重新提交流参数(re-commit)时被调用,用于停止相机采集,避免传感器重复初始化;fb_get_cb:视频任务按帧间隔向用户索要下一帧,返回NULL表示取帧失败(组件会丢弃该帧并继续);fb_return_cb:帧数据已被复制进组件内部传输缓冲区后调用,用户在此处归还/释放帧缓冲区(如调用esp_camera_fb_return)。
配置结构 uvc_device_config_t 与启动流程
// 缓冲区用于存储要发送到主机的数据 const size_t buff_size = 30 * 1024; uint8_t *uvc_buffer = (uint8_t *)heap_caps_malloc(buff_size, MALLOC_CAP_DEFAULT); assert(uvc_buffer != NULL); uvc_device_config_t config = { .uvc_buffer = uvc_buffer, .uvc_buffer_size = 40 * 1024, .start_cb = camera_start_cb, .fb_get_cb = camera_fb_get_cb, .fb_return_cb = camera_fb_return_cb, .stop_cb = camera_stop_cb, .cb_ctx = NULL, }; ESP_ERROR_CHECK(uvc_device_config(0, &config)); ESP_ERROR_CHECK(uvc_device_init());uvc_device_config_t完整字段(见 usb_device_uvc.h):
| 字段 | 类型 | 说明 |
|---|---|---|
uvc_buffer | uint8_t * | UVC 传输缓冲区,存放待发送帧数据 |
uvc_buffer_size | uint32_t | 传输缓冲区大小,必须大于一帧的大小,否则大帧会被直接丢弃 |
start_cb | uvc_input_start_cb_t | 主机以特定格式/分辨率打开设备时的回调 |
fb_get_cb | uvc_input_fb_get_cb_t | 主机请求新帧时的回调 |
fb_return_cb | uvc_input_fb_return_cb_t | 帧缓冲区不再使用时的回调 |
stop_cb | uvc_input_stop_cb_t | 主机关闭设备时的回调 |
cb_ctx | void * | 回调上下文,用户自定义数据,原样传给所有回调 |
uvc_device_config(int index, uvc_device_config_t *config)的index是 UVC 设备编号([0,1],0对应 Cam1,1对应 Cam2,仅在开启双摄像头时可用)。从源码看,该函数会做严格的参数校验:index越界、config为 NULL、四个回调缺失、缓冲区为空或大小为 0 都会返回ESP_ERR_INVALID_ARG(见 usb_device_uvc.c)。
uvc_device_init()则完成真正的工作:
- 根据 Kconfig 中 Cam1/Cam2 的格式选项确定
UVC_FORMAT_JPEG或UVC_FORMAT_H264; - 创建事件组,初始化 USB PHY(
usb_phy_init(),配置为 OTG 设备模式); - 调用
tusb_init()启动 TinyUSB 协议栈; - 创建 TinyUSB 任务与 UVC 视频任务(任务优先级、绑核均可通过 Kconfig 配置),随后设备即可被主机枚举为 UVC 摄像头。
源码级工作原理:从取帧到 USB 传输
从 usb_device_uvc.c 的实现可以梳理出整条数据通路:
- 参数协商:主机发起 UVC 流参数协商(Probe/Commit)时,TinyUSB 调用
tud_video_commit_cb,组件打印bFrameIndex与dwFrameInterval,将帧间隔(100ns 单位)换算为毫秒存入interval_ms,并按帧索引从UVC_FRAMES_INFO查表得到宽/高/帧率,调用start_cb让用户初始化相机; - 帧节奏控制:视频任务(
video_task)通过tud_video_n_streaming(0, 0)判断当前是否处于流传输状态;未传输时重置内部状态并等待;传输时按interval_ms精确节流,实现目标帧率; - 取帧与拷贝:到达帧间隔后调用
fb_get_cb取帧,若pic->len > uvc_buffer_size则记录frame size is too big, dropping frame并归还帧、丢弃本帧;否则memcpy到传输缓冲区后立即调用fb_return_cb归还用户帧缓冲; - 异步发送:调用
tud_video_n_frame_xfer()发起 USB 传输,并通过任务通知(xTaskNotifyGive)在tud_video_frame_xfer_complete_cb中获知上一帧发送完成,从而保证同一时刻只有一个在途传输,避免缓冲区竞争。
可见组件对"缓冲不足"的处理是主动丢帧而非阻塞,因此uvc_buffer_size的取值直接影响高分辨率下的实际帧率表现:若缓冲区小于最大帧尺寸,超大帧将被持续丢弃,表现为花屏或掉帧。
Kconfig 配置详解
组件的所有可调参数集中在 Kconfig 中,通过idf.py menuconfig进入USB Device UVC菜单配置。需要说明的是:这些配置在 menuconfig 中体现为CONFIG_UVC_CAM1_*/CONFIG_UVC_CAM2_*系列符号,由 Kconfig 中的变量(如UVC_CAM1_FRAMESIZE)经选择后生成。
USB 身份信息
| 配置项 | 默认值 | 说明 |
|---|---|---|
TUSB_VID | 0x303A | 厂商 ID(Espressif 的 USB VID) |
TUSB_PID | 0x8000 | 产品 ID |
TUSB_MANUFACTURER | Espressif | 厂商字符串 |
TUSB_PRODUCT | ESP UVC Device | 产品字符串 |
TUSB_SERIAL_NUM | 12345678 | 序列号字符串 |
这些值会写入 USB 设备/字符串描述符(见 usb_descriptors.c),决定主机设备管理器中显示的名称。
传输模式与 USB 速度
| 配置项 | 默认值 | 说明 |
|---|---|---|
UVC_SUPPORT_TWO_CAM | n | 是否支持双摄像头(Cam2 全部配置依赖此项) |
TINYUSB_RHPORT | HS(P4/S31)/ FS(其余) | USB PHY 控制器速度:HS 对应高速(USB OTG 2.0 PHY),仅 ESP32-P4 / ESP32-S31 支持 |
UVC_CAM1_XFER_MODE/UVC_CAM2_XFER_MODE | Isochronous | Cam1/Cam2 的传输模式(Isochronous 或 Bulk) |
传输模式的选择有实际性能与兼容性影响。以仓库示例 examples/usb/device/usb_webcam/README.md 给出的实测参考:Isochronous 模式吞吐约 512KB/s、兼容 Windows/Linux/macOS;Bulk 模式吞吐约 1216KB/s、兼容 Windows/macOS,但部分 Linux 平台可能遇到兼容性问题。此外 README.md 特别提示:若开启UVC_SUPPORT_TWO_CAM且需要多摄像头切换,请将两个摄像头的传输模式都设为 Isochronous。
分辨率、帧率与多帧列表
Cam1 的默认分辨率(UVC_CAM1_FRAMESIZE)与配套的默认帧率、宽高如下:
| 分辨率选项 | 宽×高 | 默认帧率 |
|---|---|---|
| QVGA | 320×240 | 30 fps |
| HVGA | 480×320 | 30 fps |
| VGA | 640×480 | 15 fps |
| SVGA | 800×600 | 15 fps |
| HD(默认) | 1280×720 | 15 fps |
| FHD | 1920×1080 | 15 fps |
Cam2(UVC_CAM2_*)拥有完全相同的选项结构,仅默认分辨率为 HD。另有两个重要开关:
UVC_CAM1_MULTI_FRAMESIZE/UVC_CAM2_MULTI_FRAMESIZE(默认y):启用后向主机暴露额外的多帧尺寸列表;UVC_MULTI_FRAME_CONFIG菜单下的FRAME_SIZE_1/2/3:自定义三个附加帧尺寸,默认分别为 640×480@15、480×320@30、320×240@30。
这些帧配置最终汇聚到 uvc_frame_config.h 中的UVC_FRAMES_INFO[][4]静态表(每路摄像头一张 4 项的表:1 个默认帧 + 3 个附加帧),USB 描述符会依据该表向主机声明可协商的帧格式。示例日志中打印的Frame(1)=1280*720@15fps、Frame(2)=640*480@15fps、Frame(3)=480*320@30fps正是这张表的实际内容(见 usb_webcam_main.c)。
任务优先级与核绑定
| 配置项 | 默认值 | 范围 | 说明 |
|---|---|---|---|
UVC_TINYUSB_TASK_PRIORITY | 5 | 1–15 | TinyUSB 任务优先级 |
UVC_TINYUSB_TASK_CORE | -1 | -1–1 | TinyUSB 任务绑核(-1 为不绑定) |
UVC_CAM1_TASK_PRIORITY | 4 | 1–15 | Cam1 视频任务优先级 |
UVC_CAM1_TASK_CORE | -1 | -1–1 | Cam1 视频任务绑核 |
UVC_CAM2_TASK_PRIORITY/UVC_CAM2_TASK_CORE | 4 / -1 | 同上 | Cam2 任务配置(依赖双摄像头开关) |
在多核芯片上,将 TinyUSB 任务与视频任务分配到不同核心有助于避免帧传输抖动。
双摄像头模式
当CONFIG_UVC_SUPPORT_TWO_CAM开启后,组件通过CONFIG_UVC_CAM_NUM = 2扩展内部状态,为两路摄像头各维护一份uvc_device_config_t、格式、帧间隔与视频任务(video_task/video_task2,见 usb_device_uvc.c)。此时需要分别调用两次uvc_device_config():
uvc_device_config(0, &config_cam1); uvc_device_config(1, &config_cam2); uvc_device_init();uvc_device_init()会校验两路都已完成配置,否则返回ESP_ERR_INVALID_STATE。USB 描述符中也会相应出现两组 Video Control / Video Streaming 接口(字符串描述符中的 "UVC CAM1" / "UVC CAM2"),主机可同时打开两个摄像头设备。双摄像头实现在 examples/usb/device/usb_dual_uvc_device 中有完整参考。
运行示例:将 ESP32-S3-EYE 变成 USB 摄像头
仓库提供了可直接运行的示例 examples/usb/device/usb_webcam,它把 ESP32-S3-EYE 开发板包装成一个标准的 USB WebCam 设备(仅支持 MJPEG 格式,IDF v5.0 及以上版本支持 LCD 动画显示)。
硬件要求
- 任意带摄像头与 USB 接口的 ESP32-S2 / ESP32-S3 开发板(示例默认 ESP32-S3-EYE,也可通过 menuconfig 中
USB WebCam config → Camera Pin Configuration → Select Camera Pinout切换板型,或选择Custom Camera Pinout逐引脚配置); - USB 连接:GPIO19 接 D-,GPIO20 接 D+;
- 摄像头需支持 JPEG 硬件压缩(如 OV2640、OV3660 等,可参考 esp32-camera 支持列表)。
构建与烧录
. $HOME/esp/esp-idf/export.sh # 设置 ESP-IDF 环境变量 idf.py set-target esp32s3 # 或 esp32s2 idf.py build flash monitor示例运行后会打印格式/帧列表,并在主机打开设备时输出bFrameIndex、dwFrameInterval与相机初始化日志(参考 usb_webcam/README.md 的 Example Output)。此时在 PC 的摄像头应用(如 OBS、浏览器 WebRTC 或系统相机)中即可看到 ESP32 的画面。
示例中的关键实现
示例的四个回调展示了与esp_camera组件的标准配合方式(见 usb_webcam_main.c):
camera_start_cb根据主机请求的宽高映射到esp_camera的framesize_t与 JPEG 质量参数,再调用camera_init()初始化传感器;初始化完成后还会设置vflip等图像方向参数;camera_fb_get_cb通过esp_camera_fb_get()获取相机帧,将其包装为uvc_fb_t返回,并检查帧长是否超过UVC_MAX_FRAMESIZE_SIZE(S3 上为 75KB);camera_fb_return_cb在组件拷贝完数据后调用esp_camera_fb_return()归还帧缓冲;camera_stop_cb在主机关闭设备时收到通知(示例中用于关闭 S3-EYE 的 LCD 眼睛动画)。
组件级测试验证
组件自带测试用例 test_apps/main/usb_device_uvc_test.c,它不使用真实摄像头,而是把内嵌在固件中的 JPEG 图片(esp_1280_720.jpg,通过_binary_esp_1280_720_jpg_start符号引用)作为帧来源,在camera_fb_get_cb中按当前配置的宽高填充uvc_fb_t并返回。该测试同样走完整的uvc_device_config→uvc_device_init流程,验证了组件"数据源无关"的设计——只要回调能提供合法的 JPEG/H264 帧,任何来源都可以成为 UVC 设备,这对调试与 CI 自动化验证非常有价值。
常见问题与注意事项
- 帧缓冲尺寸:
uvc_buffer_size必须大于最大一帧的字节数。示例中 S3 使用 75KB、S2 使用 60KB(见 usb_webcam_main.c)。帧过大时组件会打印frame size is too big, dropping frame并丢弃该帧; - 内存分配:传输缓冲区建议通过
heap_caps_malloc/malloc从可用堆分配;真实相机示例将帧缓冲放在 PSRAM(CAMERA_FB_IN_PSRAM),以缓解片内内存压力; - Bulk 模式的 Linux 兼容性:部分 Linux 平台对 Bulk 模式的 UVC 摄像头支持不完善,若无法枚举请切回 Isochronous 模式;
- 双摄像头切换:启用
UVC_SUPPORT_TWO_CAM后如需要多摄像头切换,两个摄像头的传输模式都应保持 Isochronous; - 回调必须完整:
uvc_device_config会校验四个回调与缓冲区均非空,缺一不可; - deinit 语义:早期文档说明基于 TinyUSB 的驱动不提供
deinit;当前源码已实现uvc_device_deinit(),可完成任务回收、tusb_teardown()与 PHY 释放,具体以所用版本头文件声明为准。
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考