# USB 设备栈

[English]

## 概述

USB 设备栈（以下简称设备栈）支持在 ESP32-S3 上启用 USB 设备支持。通过使用设备栈，可以为 ESP32-S3 烧录任意具有明确定义的 USB 设备功能（如键盘、鼠标、摄像头）、自定义功能（也称特定供应商类别）或上述功能的组合（也称复合设备）。

设备栈基于 TinyUSB 栈构建，但对 TinyUSB 进行了一些小的功能扩展和修改，使其更好地集成到 ESP-IDF。设备栈通过 乐鑫组件注册表 作为托管组件分发。

## 功能列表

- 支持多种设备类别 (CDC, HID, MIDI, MSC)

- 支持复合设备

- 支持特定供应商类别

- 最多支持 6 个端点

  > - 5 个输入/输出端点

  > - 1 个输入端点

- 自供电设备的 VBUS 监测

## 硬件连接

ESP32-S3 将 USB D+ 和 D- 信号分别路由到 GPIO 20 和 19。为了实现 USB 设备功能，这些 GPIO 应通过某种方式连接到总线（例如，通过 Micro-B 端口、USB-C 端口或直接连接到标准-A 插头）。

备注

如果你使用带有两个 USB 端口的 ESP32-S3 开发板，标有 "USB" 的端口已经连接到 D+ 和 D- GPIO。

备注

自供电设备还必须通过电压分压器或比较器连接 VBUS，详情请参阅 自供电设备。

## 外部 PHY 配置

ESP32-S3 内部集成了两个 USB 控制器：USB-OTG 与 USB-Serial-JTAG。这两个控制器 共用同一个 PHY，因此同一时间只能有一个控制器工作。如果在 USB-Serial-JTAG 工作时（例如调试或烧录）时仍需要使用 USB 设备功能，必须使用 外部 PHY，因为此时内部 PHY 已被 USB-Serial-JTAG 占用。

备注

使用外部 PHY 并不是在 USB 主机或设备功能开启时同时实现调试的唯一办法。也可以通过烧录对应的 eFuse，将调试接口从 USB-Serial-JTAG 切换为传统的 JTAG 接口。具体步骤请参考 ESP-IDF 编程指南中针对你的芯片的 JTAG 调试 章节。

ESP32-S3 支持连接外部 PHY 芯片。当需要在使用 USB-Serial-JTAG 控制器的同时提供全速 USB 设备功能时，这一点尤其重要。不同的外部 PHY 芯片可能需要不同的硬件配置，请参考各自芯片的规格书。乐鑫官方文档提供了如下的通用连接示意图供参考。如需了解更多内容，请参阅 使用外部 PHY。

连接外部 PHY 芯片的典型电路图

已测试的外部 PHY 芯片如下：

- SP5301 — ESP32-S3 原生支持此芯片。原理图与布线方法请参考上文链接。

- TUSB1106 — ESP32-S3 原生支持。可通过 GPIO 映射与外部 PHY 驱动配合使用。请遵循 TUSB1106 数据手册中的参考连接（供电方案以及在 D+/D– 上建议的串联电阻）。

- STUSB03E — 需要通过模拟开关进行信号切换，详情参考下方示例。

使用 STUSB03E 与模拟开关的连接示例（设备模式）

备注

此原理图仅为简化示例，用于展示外部 PHY 的连接方式，未包含完整 ESP32-S3 设计所需的所有元件和信号（如 VCC、GND、RESET 等）。 图中包含 +5 V 电源轨（通常来自 USB VBUS）和 VCC 电源轨。VCC 电压应与芯片供电电压一致（通常为 3.3 V），并确保外部 PHY 与芯片使用同一电压域供电。如果设计自供电 USB 设备，请将外部 PHY 的 VBUSDET 信号接入 ESP32-S3，以便实现对 VBUS 电压状态的监控。

硬件配置通过将 GPIO 映射到 PHY 管脚实现。任何未使用的管脚（如 `usb_phy_ext_io_conf_t::suspend_n_io_num`、`usb_phy_ext_io_conf_t::fs_edge_sel_io_num`） 必须设置为 -1。

备注

`usb_phy_ext_io_conf_t::suspend_n_io_num` 管脚 当前不受支持，无需连接。 `usb_phy_ext_io_conf_t::fs_edge_sel_io_num` 管脚为可选管脚，仅需在低速和全速模式间切换时使用。

ESP TinyUSB 设备栈从 2.0 版本开始支持外部 PHY。要在设备模式下使用外部 PHY，需执行以下步骤：

1. 使用 `usb_phy_config_t` 配置 GPIO 映射与 PHY。

2. 调用 `usb_new_phy()` 创建 PHY。

3. 使用 `TINYUSB_DEFAULT_CONFIG()` 初始化 `tinyusb_config_t`。

4. 将 `tinyusb_config_t` 的 phy.skip_setup 字段设为 `true`，从而跳过 PHY 的重新初始化，直接使用已配置的外部 PHY。

示例代码：

    // 外部 PHY 的 GPIO 配置
    const usb_phy_ext_io_conf_t ext_io_conf = {
        .vp_io_num  = 8,
        .vm_io_num  = 5,
        .rcv_io_num = 11,
        .oen_io_num = 17,
        .vpo_io_num = 4,
        .vmo_io_num = 46,
        .suspend_n_io_num = -1,
        .fs_edge_sel_io_num = -1,
    };

    // 针对 OTG 控制器（设备模式）的外部 PHY 配置与初始化
    const usb_phy_config_t phy_config = {
        .controller = USB_PHY_CTRL_OTG,
        .target = USB_PHY_TARGET_EXT,
        .otg_mode = USB_OTG_MODE_DEVICE,
        .otg_speed = USB_PHY_SPEED_FULL,
        .ext_io_conf = &ext_io_conf
    };

    usb_phy_handle_t phy_hdl;
    ESP_ERROR_CHECK(usb_new_phy(&phy_config, &phy_hdl));

    // 使用默认配置初始化 TinyUSB（可根据需要设置事件处理函数）
    tinyusb_config_t config = TINYUSB_DEFAULT_CONFIG();
    config.phy.skip_setup = true;

    tinyusb_driver_install(&config);

通过上述配置，USB 设备栈会直接使用 外部 PHY，而不会尝试配置内部 PHY。

## 设备栈结构

设备栈以 TinyUSB 栈为基础，在此基础上，该设备栈实现了以下功能：

- 自定义 USB 描述符

- 支持串行设备

- 通过串行设备重定向标准流

- 提供用于 USB 设备 MSC 类的存储介质（SPI-Flash 和 SD 卡）

- 封装设备栈中处理 TinyUSB 服务的任务

## 组件依赖项

设备栈通过 乐鑫组件注册表 分发，使用前，请使用以下命令将设备栈组件添加为依赖项：

    idf.py add-dependency esp_tinyusb

### 配置选项

通过 menuconfig 选项，可以对设备栈进行以下多方面配置：

- TinyUSB 日志的详细程度

- 设备栈任务相关选项

- 默认设备/字符串描述符选项

- 特定类别的选项

### 配置描述符

结构体 `tinyusb_config_t` 提供了与 USB 描述符相关的字段，应进行初始化。

无论是全速 USB 设备还是高速 USB 设备，都应初始化以下描述符：

- `device_descriptor`

- `string_descriptor`

全速 USB 设备应初始化以下字段，以提供相应的配置描述符：

- `configuration_descriptor`

调用 `tinyusb_driver_install()` 时，设备栈将基于上述字段中提供的描述符实现 USB 设备。

设备栈还提供了默认描述符，将 `tinyusb_driver_install()` 中的相应字段设置为 `NULL` 即可安装。默认描述符包括：

- 默认设备描述符：如需启用，将 `device_descriptor` 设置为 `NULL`。默认设备描述符将使用相应的 menuconfig 选项设置的值（如 PID、VID、bcdDevice 等）。

- 默认字符串描述符：如需启用，将 `string_descriptor` 设置为 `NULL`。默认字符串描述符将使用相应的 menuconfig 选项设置的值（如制造商、产品和序列字符串描述符选项）。

- 默认配置描述符。某些很少需要自定义配置的类别（如 CDC 和 MSC）将提供默认配置描述符。如需启用，将相应的配置描述符字段设置为 `NULL`。

  > - `configuration_descriptor`：全速描述符，仅适用于全速设备

  > - `fs_configuration_descriptor`：全速描述符，适用于高速设备

  > - `hs_configuration_descriptor`：高速描述符，适用于高速设备

备注

为实现向后兼容性，若设备栈支持高速，可使用 `configuration_descriptor` 代替 `fs_configuration_descriptor` 来设置全速配置描述符。

## 安装设备栈

请调用 `tinyusb_driver_install()` 安装设备栈。结构体 `tinyusb_config_t` 指定了设备栈的配置，而 `tinyusb_config_t` 作为参数传递给 `tinyusb_driver_install()`。

备注

结构体 `tinyusb_config_t` 可以实现零初始化（如 `const tinyusb_config_t tusb_cfg = { 0 };`）或部分初始化（如下所示）。对于结构体中任何初始化为 `0` 或 `NULL` 的成员，设备栈将使用其默认配置，请参阅如下示例。

    const tinyusb_config_t partial_init = {
        .device_descriptor = NULL,  // 使用在 menuconfig 中指定的默认设备描述符
        .string_descriptor = NULL,  // 使用在 menuconfig 中指定的默认字符串描述符
        .external_phy = false,      // 使用内部 USB PHY
    #if (TUD_OPT_HIGH_SPEED)
        .fs_configuration_descriptor = NULL, // 使用在 menuconfig 中根据设置指定的默认全速配置描述符
        .hs_configuration_descriptor = NULL, // 使用在 menuconfig 中根据设置指定的默认高速配置描述符
        .qualifier_descriptor = NULL,  // 使用默认限定描述符，值取自默认设备描述符
    #else
        .configuration_descriptor = NULL,   // 使用在 menuconfig 中根据设置指定的默认配置描述符
    #endif // TUD_OPT_HIGH_SPEED

    };

## 自供电设备

USB 规范要求自供电设备监测 USB 的 VBUS 信号的电压水平。与总线供电设备相反，即使没有 USB 连接，自供电设备也可以正常工作。通过监测 VBUS 电压水平，自供电设备可以检测连接和断开事件。当 VBUS 电压升高到 4.75 V 以上时视为有效；当 VBUS 电压下降到 4.35 V 以下时视为无效。

在 ESP32-S3 上，需要使用一个 GPIO 作为电压感测管脚，检测 VBUS 处于在规定阈值之上/之下。然而，由于 ESP32-S3 管脚具有 3.3 V 容差，即使 VBUS 上升/下降到高于/低于上述规定阈值，ESP32-S3 仍会显示为逻辑高电平。因此，为了检测 VBUS 是否有效，可以采用以下方法：

- 将 VBUS 连接至电压比较器芯片/电路，该芯片/电路可检测上述阈值（即 4.35 V 和 4.75 V），并向 ESP32-S3 输出 3.3 V 逻辑电平，指示 VBUS 是否有效。

- 如果 VBUS 为 4.4 V，则使用电阻分压器输出 (0.75 x Vdd)（见下图）。

备注

在这两种情况下，设备从 USB 主机拔出后 3 毫秒内，传感管脚上的电压必须为逻辑低电平。

用于 VBUS 监测的简易分压器

请在结构体 `tinyusb_config_t` 中将 `self_powered` 设置为 `true`，并将 `vbus_monitor_io` 设置为用于 VBUS 监测的 GPIO 管脚编号以使用此功能。

## USB 串行设备 (CDC-ACM)

如果在 menuconfig 中启用了 CDC 选项，则可以根据 `tinyusb_config_cdcacm_t` 的设置，使用 `tusb_cdc_acm_init()` 初始化 USB 串行设备，请参阅如下示例：

    const tinyusb_config_cdcacm_t acm_cfg = {
        .usb_dev = TINYUSB_USBDEV_0,
        .cdc_port = TINYUSB_CDC_ACM_0,
        .rx_unread_buf_sz = 64,
        .callback_rx = NULL,
        .callback_rx_wanted_char = NULL,
        .callback_line_state_changed = NULL,
        .callback_line_coding_changed = NULL
    };
    tusb_cdc_acm_init(&acm_cfg);

可以在配置结构体中设置指向 `tusb_cdcacm_callback_t` 函数的指针指定回调函数，或在初始化 USB 串行设备后，调用 `tinyusb_cdcacm_register_callback()` 指定回调函数。

### USB 串行控制台

USB 串行设备支持将所有标准输入/输出流 (stdin、stdout、stderr) 重定向到 USB。因此，调用如 `printf()` 等标准库输入/输出函数将导致通过 USB 而不是 UART 发送/接收数据。

建议调用 `esp_tusb_init_console()` 将标准输入/输出流切换到 USB，并调用 `esp_tusb_deinit_console()` 将其切换回 UART。

## USB 大容量存储设备 (MSC)

在 menuconfig 中启用 MSC `CONFIG_TINYUSB_MSC_ENABLED` 选项时，可以将 ESP 芯片作为 USB 大容量存储设备使用。按如下示例，可以初始化存储媒介（SPI-Flash 或 SD 卡）。

- SPI-Flash

    static esp_err_t storage_init_spiflash(wl_handle_t *wl_handle)
    {
        ***
        esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
        ***
        wl_mount(data_partition, wl_handle);
        ***
    }
    storage_init_spiflash(&wl_handle);

    const tinyusb_msc_spiflash_config_t config_spi = {
        .wl_handle = wl_handle
    };
    tinyusb_msc_storage_init_spiflash(&config_spi);

- SD 卡

    static esp_err_t storage_init_sdmmc(sdmmc_card_t **card)
    {
        ***
        sdmmc_host_t host = SDMMC_HOST_DEFAULT();
        sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
        // 对于 SD 卡，设置要使用的总线宽度

        slot_config.width = 4;
        slot_config.clk = CONFIG_EXAMPLE_PIN_CLK;
        slot_config.cmd = CONFIG_EXAMPLE_PIN_CMD;
        slot_config.d0 = CONFIG_EXAMPLE_PIN_D0;
        slot_config.d1 = CONFIG_EXAMPLE_PIN_D1;
        slot_config.d2 = CONFIG_EXAMPLE_PIN_D2;
        slot_config.d3 = CONFIG_EXAMPLE_PIN_D3;
        slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

        sd_card = (sdmmc_card_t *)malloc(sizeof(sdmmc_card_t));
        (*host.init)();
        sdmmc_host_init_slot(host.slot, (const sdmmc_slot_config_t *) &slot_config);
        sdmmc_card_init(&host, sd_card);
        ***
    }
    storage_init_sdmmc(&card);

    const tinyusb_msc_sdmmc_config_t config_sdmmc = {
        .card = card
    };
    tinyusb_msc_storage_init_sdmmc(&config_sdmmc);

### MSC 性能优化

single-buffer 方案

single-buffer 方案通过使用专用 buffer 临时存储接收到的写入数据，而不是在回调中立即处理，从而提升性能。

- 可配置的 buffer 大小： 通过 `CONFIG_TINYUSB_MSC_BUFSIZE` 参数设置 buffer 大小，用户可以根据实际需要，灵活调整性能与存储占用的平衡点。

该方案确保了 USB 传输的效率，同时避免因存储操作可能带来的延迟。

USB MSC 驱动器性能

FIFO 大小 | 读取速度 | 写入速度
--- | --- | ---
512 B | 0.566 MB/s | 0.236 MB/s
8192 B | 0.925 MB/s | 0.928 MB/s

性能限制：

- 内部 SPI flash 性能 受架构限制影响。程序执行和存储访问共享同一 flash 芯片，导致写入 flash 时必须暂停程序执行，会显著影响性能。

- 内部 SPI flash 主要适用于演示场景。在需要更高性能的实际应用中，在支持的情况下使用 外部存储设备，如 SD 卡或外部 SPI flash 芯片。

SD 卡不受此限制影响，因此能获得更高的性能提升。

## 应用示例

如需查看相关示例，请前往目录 peripherals/usb/device。

- peripherals/usb/device/tusb_console 演示了如何使用 TinyUSB 组件配置 ESP32-S3，以通过串行设备连接获取和输出日志，适用于任何支持 USB-OTG 的乐鑫开发板。

- peripherals/usb/device/tusb_serial_device 演示了如何使用 TinyUSB 组件将 ESP32-S3 配置为 USB 串行设备，还支持配置为双串行设备。

- peripherals/usb/device/tusb_midi 演示了如何使用 TinyUSB 组件将 ESP32-S3 配置为 USB MIDI 设备，从而通过本地 USB 端口输出 MIDI 音符序列。

- peripherals/usb/device/tusb_hid 演示了如何使用 TinyUSB 组件实现 USB 键盘和鼠标，在连接到 USB 主机时发送 “按下和释放 key a/A” 事件，并使鼠标沿方形轨迹移动。

- peripherals/usb/device/tusb_msc 演示了如何使用 USB 功能创建一个可以被 USB 主机识别的大容量存储设备，允许访问其内部数据存储，支持 SPI Flash 和 SD MMC 卡存储介质。

- peripherals/usb/device/tusb_composite_msc_serialdevice 演示了如何使用 TinyUSB 组件将 ESP32-S3 同时配置为 USB 串行设备和 MSC 设备（存储介质为 SPI-Flash）运行。

- peripherals/usb/device/tusb_ncm 演示了使用 TinyUSB 组件，借助网络控制模型 (NCM) 将 Wi-Fi 数据通过 USB 传输到 Linux 或 Windows 主机。NCM 是通信设备类 (CDC) USB 设备的一个子类，专用于 Ethernet-over-USB 应用。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。