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.h、usb_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_directories将main/tusb目录加入 TinyUSB 组件的头文件搜索路径,使 TinyUSB 内部的tusb_config.h引用得以解析;target_sources将usb_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_HS与CONFIG_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_CONTROL、ITF_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_VID、CONFIG_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_CAM1、CONFIG_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 设备开发闭环是:
- 按第一节建立工程目录,在
idf_component.yml中添加espressif/tinyusb依赖; - 在
main/CMakeLists.txt中通过target_include_directories与target_sources把tusb/目录反向注入 TinyUSB 组件; - 在
tusb_config.h中通过宏声明速率(FS/HS)、OS(FreeRTOS)、调试等级、内存对齐以及所需 Class 数量与端点 buffer 大小; - 在
usb_descriptors.c/h中实现/复用三个描述符弱函数,确保配置描述符长度精确、端点号不重复; - 初始化 USB PHY(内部或外部,参考 usb_phy.rst)后调用
tusb_init(),并创建任务持续执行tud_task(); - 按需实现设备层回调(
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),仅供参考