【数字图传第一步】如何用esp32 构造一个usb uvc + acm(虚拟串口)
一、项目概述本文将介绍如何在ESP32-S3上实现一个USB复合设备同时支持UVCUSB Video Class将ESP32-S3模拟为USB摄像头ACMAbstract Control Model虚拟串口用于调试和数据通信1.1 应用场景无线图传接收端如FPV地面站USB视频采集卡调试设备通过虚拟串口查看日志1.2 硬件要求组件说明主控ESP32-S3支持USB OTGPSRAM建议8MB用于帧缓冲连接USB Type-C直接连接PC二、工程结构project/├── main/│ ├── main.cpp # 主程序入口│ ├── usb_descriptors.cpp # USB描述符│ ├── usb_descriptors.h # USB描述符头文件│ ├── board.cpp # USB PHY初始化│ ├── board.h│ ├── cdc.cpp # CDC串口处理│ ├── cdc.h│ ├── uvc.cpp # UVC视频传输│ ├── uvc.h│ ├── tusb_config.h # TinyUSB配置│ └── CMakeLists.txt三、核心配置详解3.1 TinyUSB配置文件 (tusb_config.h)// 选择MCU和操作系统#define CFG_TUSB_MCU OPT_MCU_ESP32S3#define CFG_TUSB_OS OPT_OS_FREERTOS// 启用设备模式#define CFG_TUD_ENABLED 1// 配置端点0大小#define CFG_TUD_ENDPOINT0_SIZE 64// 启用CDC和Video类#define CFG_TUD_CDC 1#define CFG_TUD_VIDEO 1// CDC缓冲区配置#define CFG_TUD_CDC_RX_BUFSIZE 64#define CFG_TUD_CDC_TX_BUFSIZE 64// UVC流端点缓冲区#define CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE 256#define CFG_TUD_VIDEO_STREAMING_BULK 1 // 使用Bulk传输3.2 USB PHY初始化 (board.cpp)void board_init() {static usb_phy_handle_t phy_hdl;usb_phy_config_t phy_conf {.controller USB_PHY_CTRL_OTG,.target USB_PHY_TARGET_INT,.otg_mode USB_OTG_MODE_DEVICE,.otg_speed USB_PHY_SPEED_UNDEFINED,};usb_new_phy(phy_conf, phy_hdl);}3.3 USB描述符配置 (usb_descriptors.cpp)3.3.1 设备描述符static tusb_desc_device_t const desc_device {.bLength sizeof(tusb_desc_device_t),.bDescriptorType TUSB_DESC_DEVICE,.bcdUSB 0x0200,// 使用IADInterface Association Descriptor.bDeviceClass TUSB_CLASS_MISC,.bDeviceSubClass MISC_SUBCLASS_COMMON,.bDeviceProtocol MISC_PROTOCOL_IAD,.bMaxPacketSize0 64,.idVendor 0xCafe,.idProduct 0x4000, // 动态计算.bNumConfigurations 1};3.3.2 接口分配enum {ITF_NUM_VIDEO_CONTROL 0, // 视频控制接口ITF_NUM_VIDEO_STREAMING, // 视频流接口ITF_NUM_CDC, // CDC控制接口ITF_NUM_CDC_DATA, // CDC数据接口ITF_NUM_TOTAL};// 端点地址分配#define EPNUM_VIDEO_IN 0x81 // 视频IN端点#define EPNUM_CDC_NOTIF 0x82 // CDC通知端点#define EPNUM_CDC_OUT 0x02 // CDC OUT端点#define EPNUM_CDC_IN 0x83 // CDC IN端点3.3.3 视频格式配置#define FRAME_WIDTH 640#define FRAME_HEIGHT 480#define FRAME_RATE 10// 使用MJPEG格式#define USE_MJPEG 1四、关键代码实现4.1 UVC视频传输 (uvc.cpp)cstatic uint8_t* uvc_jpeg_buffer nullptr; static size_t uvc_jpeg_size 0; static bool new_frame_ready false; static unsigned tx_busy 0; // 接收JPEG数据并推送到UVC void uvc_push_jpeg(const uint8_t* jpeg_data, size_t jpeg_size) { // 从PSRAM分配缓冲区避免占用内部RAM if (uvc_jpeg_buffer nullptr) { uvc_jpeg_buffer (uint8_t*)heap_caps_malloc(120000, MALLOC_CAP_SPIRAM); } if (!tx_busy uvc_jpeg_buffer jpeg_size 120000) { memcpy(uvc_jpeg_buffer, jpeg_data, jpeg_size); uvc_jpeg_size jpeg_size; new_frame_ready true; } } // 视频发送任务 void video_task(void* param) { while (1) { if (tud_video_n_streaming(0, 0) !tx_busy new_frame_ready) { tx_busy 1; new_frame_ready false; tud_video_n_frame_xfer(0, 0, (void*)uvc_jpeg_buffer, uvc_jpeg_size); } vTaskDelay(1); } } // 传输完成回调 void tud_video_frame_xfer_complete_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx) { tx_busy 0; }4.2 CDC串口处理 (cdc.cpp)cvoid cdc_task(void* params) { while (1) { if (tud_cdc_connected()) { while (tud_cdc_available()) { uint8_t buf[64]; uint32_t count tud_cdc_read(buf, sizeof(buf)); // 回显数据 tud_cdc_write(buf, count); } tud_cdc_write_flush(); } vTaskDelay(1); } } // CDC线路状态回调 void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) { // 处理DTR/RTS状态变化 }4.3 主程序入口 (main.cpp)cextern C void app_main() { // 1. 初始化USB PHY board_init(); // 2. 初始化NVS nvs_flash_init(); // 3. 初始化WiFi可选用于无线图传 wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(cfg); esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_start(); // 4. 创建USB设备任务使用静态内存分配 TaskHandle_t usb_handle xTaskCreateStatic( usb_device_task, usbd, USBD_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, usb_task_stack, usb_task_tcb ); // 5. 创建CDC任务 xTaskCreate(cdc_task, cdc, CDC_STACK_SIZE, NULL, 2, NULL); // 6. 创建UVC视频任务 xTaskCreate(video_task, video, VIDEO_STACK_SIZE, NULL, 3, NULL); // 7. 运行USB设备任务 while (1) { tud_task(); // 处理USB事务 tud_cdc_write_flush(); vTaskDelay(pdMS_TO_TICKS(2)); } } // USB设备主任务 static void usb_device_task(void* param) { tusb_rhport_init_t dev_init { .role TUSB_ROLE_DEVICE, .speed TUSB_SPEED_AUTO }; tusb_init(BOARD_TUD_RHPORT, dev_init); while (1) { tud_task(); vTaskDelay(pdMS_TO_TICKS(2)); } }五、CMakeLists.txt配置cmakeset(SRCS main.cpp usb_descriptors.cpp board.cpp cdc.cpp uvc.cpp # TinyUSB核心文件 tinyusb/src/tusb.c tinyusb/src/common/tusb_fifo.c tinyusb/src/device/usbd.c tinyusb/src/class/cdc/cdc_device.c tinyusb/src/class/video/video_device.c # DWC2驱动ESP32-S3使用 tinyusb/src/portable/synopsys/dwc2/dcd_dwc2.c ) idf_component_register( SRCS ${SRCS} INCLUDE_DIRS . INCLUDE_DIRS tinyusb/src REQUIRES usb esp_timer )依赖配置 (idf_component.yml)yamldependencies: idf: version: 4.1.0六、编译与烧录6.1 配置menuconfigbashidf.py menuconfig需要配置Component config → USB启用USB OTGComponent config → ESP32S3-specific确保PSRAM已启用6.2 编译bashidf.py build6.3 烧录bashidf.py -p /dev/ttyACM0 flash monitor七、PC端验证7.1 检查设备连接ESP32-S3到PC后应看到视频设备新的USB摄像头COM端口虚拟串口7.2 Linux验证bash# 查看USB设备 lsusb # 查看视频设备 v4l2-ctl --list-devices # 查看串口 ls /dev/ttyACM*7.3 Windows验证打开设备管理器 → 照相机端口(COM和LPT) → USB Serial Device八、常见问题与解决8.1 设备无法识别原因USB PHY未正确初始化解决确保board_init()在tusb_init()之前调用8.2 视频流卡顿原因帧率过高或缓冲区不足解决降低帧率如10fps增加CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE使用Bulk传输而非Isochronous8.3 内存不足原因UVC帧缓冲占用大量内存解决c// 使用PSRAM存储帧缓冲 uvc_jpeg_buffer (uint8_t*)heap_caps_malloc(size, MALLOC_CAP_SPIRAM);九、总结本文介绍了在ESP32-S3上实现USB UVC ACM复合设备的完整方案。关键点TinyUSB框架提供了USB协议栈的完整实现IAD描述符正确组织多接口复合设备PSRAM利用大容量帧缓冲避免内存瓶颈任务调度合理分配优先级保证视频流畅这套方案可以作为FPV图传接收端、USB采集卡等项目的基础。项目仓库[GitHub链接]如有参考文档TinyUSB官方文档ESP-IDF编程指南