ESP IoT Solution 使用原生 TinyUSB 开发 USB 设备:工程搭建、配置宏与 UVC 实战指南
2026/9/20 5:38:52 网站建设 项目流程

ESP IoT Solution 使用原生 TinyUSB 开发 USB 设备:工程搭建、配置宏与 UVC 实战指南

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

本篇技术指南以 ESP IoT Solution 仓库的 TinyUSB 开发指南 为骨架,系统讲解如何在 ESP-IDF 工程中直接使用原生 TinyUSB 协议栈开发 USB 设备。你将掌握完整工程目录的搭建方法、tusb_config.h中全部关键配置宏的含义与取值、usb_descriptors.c中描述符弱函数的实现要点,以及 USB PHY 初始化、协议栈启动和设备层回调的完整流程。文中所有配置与代码均可在仓库的 usb_device_uvc(UVC 摄像头设备)与 usb_device_uac(UAC 音频设备)组件中找到真实对应实现。

一、工程目录结构

使用原生 TinyUSB 开发 USB 设备时,需要建立以下目录结构:

project_name | |-- main |-- CMakeLists.txt |-- idf_component.yml |-- main.c |-- tusb |-- tusb_config.h |-- usb_descriptors.c |-- usb_descriptors.h

其中:

  • main/是 ESP-IDF 工程的默认应用组件,包含入口main.c和组件构建文件CMakeLists.txt
  • main/idf_component.yml用于声明组件依赖;
  • main/tusb/目录专门放置反向提供给 TinyUSB 的文件tusb_config.husb_descriptors.c/h),单独放在一个文件夹中,可以保证依赖关系的简单与清晰。

在该工程的main组件中,通过idf_component.yml添加组件依赖espressif/tinyusb,即可拉取 TinyUSB 组件。

二、解决反向依赖:CMakeLists.txt 的关键配置

工程需要依赖 TinyUSB,同时又要向 TinyUSB 提供tusb_config.h和描述符源文件,这会不可避免地产生反向依赖问题。目前的解决方案是:将 TinyUSB 的全部关键文件作为源码直接编译到main组件中

具体做法是在main/CMakeLists.txt中、idf_component_register之后添加以下语句:

# espressif__tinyusb 应匹配当前依赖的 tinyusb 名称 idf_component_get_property(tusb_lib espressif__tinyusb COMPONENT_LIB) target_include_directories(${tusb_lib} PUBLIC "${COMPONENT_DIR}/tusb") target_sources(${tusb_lib} PUBLIC "${COMPONENT_DIR}/tusb/usb_descriptors.c")
  • idf_component_get_property获取名为espressif__tinyusb的组件库对象(该名称需与idf_component.yml中声明的依赖名称一致,命名规则为命名空间__组件名);
  • target_include_directoriesmain/tusb目录加入 TinyUSB 组件的头文件搜索路径,使 TinyUSB 内部的tusb_config.h引用得以解析;
  • target_sourcesusb_descriptors.c作为源文件编译进 TinyUSB 组件,从而让描述符回调函数(TinyUSB 以弱符号方式声明)与协议栈链接在一起。

三、tusb_config.h:功能开关的宏配置总览

TinyUSB 大部分功能的启用和关闭都是通过宏来控制的,因此需要在tusb_config.h中声明所需的功能。以下宏分为系统设置、USB 设备、USB Class 三类。

3.1 系统设置的宏

作用与取值
CFG_TUSB_RHPORT0_MODE定义连接到 USB Phy 的方式和速率。下面的定义表示 USB device 设备,速率为USB 全速#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE \| OPT_MODE_FULL_SPEED)
CFG_TUSB_RHPORT1_MODE定义连接到 USB Phy 的方式和速率。下面的定义表示 USB device 设备,速率为USB 高速#define CFG_TUSB_RHPORT1_MODE (OPT_MODE_DEVICE \| OPT_MODE_HIGH_SPEED)
ESP_PLATFORM使用 ESP-IDF 平台进行编译,需启用该宏:#define ESP_PLATFORM 1
CFG_TUSB_OS定义 TinyUSB 使用的操作系统。若使用 FreeRTOS 需启用该宏,也可以不启用操作系统:#define CFG_TUSB_OS OPT_OS_FREERTOS
CFG_TUSB_OS_INC_PATH在 ESP-IDF 中,include 路径要求添加"freertos/"前缀:#define CFG_TUSB_OS_INC_PATH freertos/
CFG_TUSB_DEBUG启用 TinyUSB 的 LOG 打印等级,共三级(0 关闭、1 基本、2 详细):#define CFG_TUSB_DEBUG 0
CFG_TUSB_DEBUG_PRINTF定义 TinyUSB 的 log 打印函数:#define CFG_TUSB_DEBUG_PRINTF esp_rom_printf
CFG_TUD_ENABLED设为 1 启用 TinyUSB device 功能:#define CFG_TUD_ENABLED 1
CFG_TUSB_MEM_SECTION启用后可将 TinyUSB 的内存分配到特定内存段,例如 DMA 受限的 SRAM 区域:#define CFG_TUSB_MEM_SECTION __attribute__ ((section(".usb_ram")))
CFG_TUSB_MEM_ALIGN定义内存对齐方式:#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))

在仓库的 usb_device_uvc/tusb/tusb_config.h 中可以看到这些宏的真实组合方式,并且它通过CONFIG_TINYUSB_RHPORT_HSCONFIG_IDF_TARGET_ESP32P4的组合,自动决定使用高速端口(CFG_TUSB_RHPORT1_MODE,适用于 ESP32-P4 的内置 HS PHY)还是全速端口(CFG_TUSB_RHPORT0_MODE,适用于 ESP32-S2/S3 等),并同步设置CONFIG_USB_HS供上层逻辑使用。该文件还通过#ifndef CFG_TUSB_MCU / #error强制要求CFG_TUSB_MCU由编译器标志传入。

3.2 USB 设备的宏

  • CFG_TUSB_ENDPOINT0_SIZE:用于定义端点 0 的最大包大小,通常为 64 字节:#define CFG_TUD_ENDPOINT0_SIZE 64

3.3 USB Class 的宏

每个 USB Class 都有单独的宏定义,这里以 UVC Class 为例:

  • CFG_TUD_VIDEO:配置视频控制接口(Video Control Interface)的数量;
  • CFG_TUD_VIDEO_STREAMING:配置视频流接口(Video Streaming Interface)的数量。

在 usb_device_uvc/tusb/tusb_config.h 中,这两个宏会随CONFIG_UVC_SUPPORT_TWO_CAM(双摄像头支持)动态取值:单摄像头时为 1,双摄像头时为 2:

#if CONFIG_UVC_SUPPORT_TWO_CAM #define CFG_TUD_VIDEO 2 #define CFG_TUD_VIDEO_STREAMING 2 #else #define CFG_TUD_VIDEO 1 #define CFG_TUD_VIDEO_STREAMING 1 #endif

此外,不同的 USB Class 还会有一些特殊宏,用于定义软件 FIFO 大小或启用某些功能。例如 UVC Class 中的CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE用于定义视频传输流端点的 buffer 大小。从 usb_device_uvc/tusb/tusb_config.h 可以看出该宏与速率/传输类型的强关联:全速(FS)等时传输时为 512,高速(HS)等时传输时为 1023,而 Bulk 模式(CFG_TUD_CAM1_VIDEO_STREAMING_BULK)下为 64(FS)/ 512(HS)。这印证了"宏决定端点 buffer 大小"这一核心用法。

可参考的tusb_config.h完整示例:

  • usb_device_uac/tusb/tusb_config.h(UAC 音频设备)
  • usb_device_uvc/tusb/tusb_config.h(UVC 视频设备)
  • usb_hid_device/hid_device/tusb_config.h(HID 设备)

四、usb_descriptors.h:自定义 USB 描述符(可选)

该文件主要用来放置自定义的 USB 描述符。TinyUSB 提供了很多描述符的模板,如果默认模板不满足需求,就需要自己定义一套 USB 描述符。需要注意的是,尽量使用 TinyUSB 中预定义好的一些描述符,这样可以很方便地进行描述符组装和长度计算。

以 usb_device_uvc/tusb/usb_descriptors.h 为例,它基于 TinyUSB 的TUD_VIDEO_*系列宏模板,通过宏组合的方式定义了一整套 UVC 1.5 描述符生成器(TUD_VIDEO_CAPTURE_DESCRIPTOR_MJPEG_UNCOMPR_H264_BULK等),并定义了端点地址(如EPNUM_CAM1_VIDEO_IN 0x81)、接口编号枚举(ITF_NUM_VIDEO_CONTROLITF_NUM_VIDEO_STREAMING)以及各段描述符的长度计算宏(TUD_VIDEO_CAPTURE_DESC_*_LEN)。这种"模板 + 长度宏"的做法,正是利用 TinyUSB 预定义描述符实现"描述符组装和长度计算"的典型范例。

可参考的usb_descriptors.h示例:

  • usb_device_uac/tusb_uac/uac_descriptors.h
  • usb_device_uvc/tusb/usb_descriptors.h
  • usb_hid_device/hid_device/usb_descriptors.h

五、usb_descriptors.c:实现三个描述符弱函数

该文件主要实现了几个获取描述符的弱函数,分别是获取设备描述符、配置描述符和字符串描述符:

uint8_t const *tud_descriptor_device_cb(void); uint8_t const *tud_descriptor_configuration_cb(uint8_t index); uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid);

在 usb_device_uvc/tusb/usb_descriptors.c 中可以直观看到这三个函数的完整实现逻辑:

  • tud_descriptor_device_cb返回静态定义的tusb_desc_device_t desc_device,其中使用TUSB_CLASS_MISC / MISC_SUBCLASS_COMMON / MISC_PROTOCOL_IAD声明这是一个通过 IAD(Interface Association Descriptor)组合多接口的复合设备(UVC 的视频控制 + 视频流接口),VID/PID 由CONFIG_TUSB_VIDCONFIG_TUSB_PID配置;
  • tud_descriptor_configuration_cb返回静态数组desc_fs_configuration,它以TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0, 500)开头,随后按CONFIG_FORMAT_MJPEG_CAM1CONFIG_FORMAT_H264_CAM1等编译选项拼装对应格式的描述符;
  • tud_descriptor_string_cb通过string_desc_arr[]字符串表(制造商、产品名、序列号、"UVC CAM1"等)将 ASCII 字符串转换为 UTF-16LE 的字符串描述符。

注意点:

  • 配置描述符的长度一定要等于实际的长度。仓库通过CONFIG_TOTAL_LEN = TUD_CONFIG_DESC_LEN + TUD_CAM1_VIDEO_CAPTURE_DESC_LEN + ...用宏精确计算总长度,再由TUD_CONFIG_DESCRIPTOR写入描述符头部,从机制上避免长度不一致;
  • 配置描述符中各个端点描述符的端点号要避免重复。在 UVC 双摄像头配置中,摄像头 1 使用0x81、摄像头 2 使用0x82,正是为了规避端点号冲突。

可参考的usb_descriptors.c示例:

  • usb_device_uvc/tusb/usb_descriptors.c
  • usb_device_uac/tusb/usb_descriptors.c
  • usb_hid_device/hid_device/usb_descriptors.c

六、初始化 USB Phy

USB 协议栈运行前需要先初始化 USB PHY。初始化内部 USB Phy的代码如下:

static void usb_phy_init(void) { // Configure USB PHY usb_phy_config_t phy_conf = { .controller = USB_PHY_CTRL_OTG, .otg_mode = USB_OTG_MODE_DEVICE, .target = USB_PHY_TARGET_INT, }; usb_new_phy(&phy_conf, &s_uvc_device.phy_hdl); }

关键字段说明:

  • .controller = USB_PHY_CTRL_OTG:选择 USB-OTG 控制器;
  • .otg_mode = USB_OTG_MODE_DEVICE:以 device(设备)模式运行;
  • .target = USB_PHY_TARGET_INT:使用芯片内部 PHY;
  • usb_new_phy返回的句柄保存在phy_hdl中,用于后续释放(usb_del_phy)。

关于 PHY 的背景知识可参考仓库的 USB PHY/Transceiver 介绍:ESP32-S2/S3/P4 内置 USB Full-speed PHY,ESP32-P4 还内置 USB High-Speed PHY。内部 PHY 对应固定 GPIO(如 ESP32-S3 的 D+ 为 GPIO20、D- 为 GPIO19),同一时间 USB-OTG 与 USB-Serial-JTAG 只能有一个占用内部 PHY。如果使用外部 USB Phy(仅 ESP32-S2/S3 支持,用于让 OTG 与 Serial-JTAG 同时工作),则需要参考 usb_phy.rst 中external_phy章节的配置方式(SP5301 或同等功能 PHY,占用至少 6 个 GPIO)。

七、初始化 TinyUSB 协议栈

PHY 初始化完成后,调用tusb_init()启动协议栈,并创建独立任务循环调用tud_task()处理 USB 事件:

static void tusb_device_task(void *arg) { while (1) { tud_task(); } } int main(void) { usb_phy_init(); bool usb_init = tusb_init(); if (!usb_init) { ESP_LOGE(TAG, "USB Device Stack Init Fail"); return ESP_FAIL; } xTaskCreatePinnedToCore(tusb_device_task, "TinyUSB", 4096, NULL, 5, NULL, 0); }

实现要点:

  • tusb_init()返回值用于判断协议栈是否初始化成功,失败时直接返回错误;
  • tud_task()必须被持续调用(通常放在独立任务中)才能响应 USB 事件;示例中创建了名为"TinyUSB"的任务,栈大小 4096,优先级 5,并固定到 0 号核运行(xTaskCreatePinnedToCore)。

八、实现设备层的弱函数

TinyUSB 提供了设备层(Device)的弱函数,用于获取设备的插入、拔出、暂停、恢复等事件:

// Invoked when device is mounted void tud_mount_cb(void) { } // Invoked when device is unmounted void tud_umount_cb(void) { } // Invoked when device is suspended void tud_suspend_cb(bool remote_wakeup_en) { } // Invoked when usb bus is resumed void tud_resume_cb(void) { }

这些回调是弱符号定义,应用层按需实现即可:例如在tud_mount_cb中通知应用"主机已连接",在tud_suspend_cb(带remote_wakeup_en参数,指示远程唤醒是否使能)中进入低功耗逻辑。即使不实现,协议栈也能正常运行。

九、实现 USB Class 的特殊回调函数

除了设备层回调,每个 USB Class 还提供了一些弱函数来完成基本功能。下面以 UVC 驱动为例展开。

通过观察 UVC Class 的 API 可以发现,它提供了两个函数和一个回调函数

bool tud_video_n_streaming(uint_fast8_t ctl_idx, uint_fast8_t stm_idx); bool tud_video_n_frame_xfer(uint_fast8_t ctl_idx, uint_fast8_t stm_idx, void *buffer, size_t bufsize); TU_ATTR_WEAK void tud_video_frame_xfer_complete_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx);

典型用法:

  • 调用tud_video_n_streaming查询/确认指定索引的流接口是否处于 streaming 状态;
  • 调用tud_video_n_frame_xfer传输一帧图像,传入图像数据缓冲区buffer与长度bufsize
  • 通过实现tud_video_frame_xfer_complete_cb回调来检查这一帧是否传输完成,在回调中继续提交下一帧,形成"逐帧提交 + 完成回调"的视频流驱动模式。

完整的传输流程可在 usb_device_uvc.c 组件源码中印证,其测试程序位于 usb_device_uvc/test_apps/main/usb_device_uvc_test.c,配套示例工程为 examples/usb/device/usb_webcam。同一模式的 UAC 音频流驱动见 usb_device_uac.c,示例工程为 examples/usb/device/usb_uac。

十、总结:从配置到运行的最小闭环

综合以上步骤,一个基于原生 TinyUSB 的 USB 设备开发闭环是:

  1. 按第一节建立工程目录,在idf_component.yml中添加espressif/tinyusb依赖;
  2. main/CMakeLists.txt中通过target_include_directoriestarget_sourcestusb/目录反向注入 TinyUSB 组件;
  3. tusb_config.h中通过宏声明速率(FS/HS)、OS(FreeRTOS)、调试等级、内存对齐以及所需 Class 数量与端点 buffer 大小;
  4. usb_descriptors.c/h中实现/复用三个描述符弱函数,确保配置描述符长度精确、端点号不重复;
  5. 初始化 USB PHY(内部或外部,参考 usb_phy.rst)后调用tusb_init(),并创建任务持续执行tud_task()
  6. 按需实现设备层回调(tud_mount_cb等)与 Class 层回调(如 UVC 的tud_video_frame_xfer_complete_cb),驱动具体业务功能。

除 UVC/UAC 外,仓库还提供大量基于 TinyUSB 的完整示例工程,例如 usb_hid_device(HID 输入设备)、usb_msc_wireless_disk(大容量存储)、usb_dual_uvc_device(双摄像头 UVC)等,可直接作为二次开发的起点,结合 usb_device_solutions.rst 中介绍的设备解决方案按需选用。

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询