Back to Esp Idf

USB 主机

docs/zh_CN/api-reference/peripherals/usb_host.rst

6.1-dev36.9 KB
Original Source

USB 主机

:link_to_translation:en:[English]

{IDF_TARGET_OTG_NUM_HOST_CHAN: default="8", esp32p4="16"}

本文档提供了 USB 主机库的相关信息,按以下章节展开:

.. contents:: 章节 :depth: 2

.. ---------------------------------------------------- Overview -------------------------------------------------------

概述

USB 主机库(以下简称主机库)是 USB 主机栈的最底层,提供面向公众开放的 API。应用程序使用 USB 主机功能时,通常无需与主机库直接交互,而是使用某个主机 Class 驱动提供的 API,这些主机 Class 驱动构建在主机库之上。

然而,由于以下的某些原因(但不仅限于此),有时你可能需要直接使用主机库:

  • 需要实现自定义主机 Class 驱动程序
  • 需要更低级别的 USB 主机 API

特性和限制 ^^^^^^^^^^^^^^^^^^^^^^

主机库具有以下特性:

.. list::

:esp32s2 or esp32s3 or esp32h4: - 支持全速 (FS) 和低速 (LS) 设备。
:esp32p4: - 支持高速 (HS)、全速 (FS) 和低速 (LS) 设备。
- 支持四种传输类型,即控制传输、块传输、中断传输和同步传输。
:esp32p4: - 支持高带宽等时性端点。
:esp32p4: - {IDF_TARGET_NAME} 包含两个 USB 2.0 OTG 外设:USB 2.0 OTG 高速和 USB 2.0 OTG 全速,二者均支持 USB 主机功能。但由于当前软件的限制,同一时间仅能有一个作为 USB 主机工作。未来版本计划支持两个 USB 主机同时运行。
- 支持多个 Class 驱动程序同时运行,即主机的多个客户端同时运行。
- 单个设备可以由多个客户端同时使用,如复合设备。
- 主机库及其底层主机栈不会在内部自动创建操作系统任务,任务数量完全由主机库接口的使用方式决定。一般来说,任务数量为 ``(运行中的主机 Class 驱动程序数量 + 1)``。
- 支持单个 Hub(启用选项 `CONFIG_USB_HOST_HUBS_SUPPORTED`)。
- 支持多个 Hub(启用选项 `CONFIG_USB_HOST_HUB_MULTI_LEVEL`)。
- 支持全局挂起与恢复,通过挂起或恢复整个总线来实现。
- 支持通过提交传输自动触发全局恢复。

目前,主机库及其底层主机栈存在以下限制:

.. list::

- 仅支持异步传输。
- 仅支持使用发现的首个配置,尚不支持变更为其他配置。
- 尚不支持传输超时。
- 尚未支持选择性(按设备/按端口)挂起/恢复。
- 尚不支持由 USB 设备发起的远程唤醒。
- 外部 Hub 驱动:不支持远程唤醒功能(即使没有设备插入,外部 Hub 也处于工作状态)。
- 外部 Hub 驱动:不处理错误用例(尚未实现过流处理、初始化错误等功能)。
- 外部 Hub 驱动:不支持接口选择。驱动程序使用具有 Hub 类代码 (09h) 的第一个可用接口。
- 外部端口驱动:无下游端口去抖动机制(尚未实现)。
:esp32p4: - 外部 Hub 驱动:无事务转换层(当 Hub 连接到高速主机时,不支持全速/低速设备)。

.. -------------------------------------------------- Architecture -----------------------------------------------------

架构

.. figure:: ../../../_static/usb_host_lib_entities.png :align: center :alt: USB 主机功能的关键实体 :figclass: align-center

USB 主机功能涉及的关键实体

上图展示了使用 USB 主机功能时涉及的关键实体,包括:

  • 主机库
  • 主机库的 客户端
  • 设备
  • 主机库的 守护进程任务

主机库 ^^^^^^^^^^^^

主机库是 ESP-IDF USB 主机栈中面向公众开放的最底层的 API 层。任何其他 ESP-IDF 组件(如 Class 驱动程序或用户组件),如果需要与连接的 USB 设备通信,只能直接或间接使用主机库 API。

主机库 API 分为两类,即 库 API客户端 API

  • 客户端 API 负责主机库的客户端与一或多个 USB 设备间的通信,该 API 只能由主机库的注册客户端调用。
  • 库 API 负责主机库处理的通信中不特定于单个客户端的通信,如设备枚举。该 API 通常由主机库的守护进程任务调用。

客户端 ^^^^^^^

主机库的客户端指使用主机库与 USB 设备通信的软件组件,如主机 Class 驱动程序或用户组件。每个客户端通常与任务间存在一对一关系,这表明,对特定客户端而言,其所有客户端 API 都应该在同一任务的上下文中调用。

通过将使用主机库的软件组件进行分类,划分为独立的客户端,主机库可以将所有客户端特定事件的处理委托给客户端对应的任务。换句话说,每个客户端任务负责管理与其对应的客户端之间的所有 USB 通信操作和事件处理,无需关心其他客户端的事件。

守护进程任务 ^^^^^^^^^^^^

尽管主机库将客户端事件的处理委托给客户端本身,但仍然需要处理主机库事件,即不特定于客户端的事件。主机库事件处理可能涉及以下内容:

  • 处理 USB 设备的连接、枚举和断连
  • 将控制传输从/向客户端进行重定向
  • 将事件转发给客户端

因此,除客户端任务外,主机库也需要一个任务来处理所有的库事件,这个任务通常是主机库守护进程任务。

设备 ^^^^^^^

主机库隔离了客户端与设备处理的细节,包括连接、内存分配和枚举等,客户端只需提供已连接且已枚举的设备列表供选择。默认情况下,在枚举过程中,每个设备都会自动配置为使用找到的第一个配置,即通过获取配置描述符请求返回的第一个配置描述符。对于大多数标准设备,通常将第一个配置的 bConfigurationValue 设置为 1。启用选项 CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK 后,可以选择不同的 bConfigurationValue。获取更多详细信息,请参阅 多项配置支持_。

只要不与相同接口通信,两个及以上的客户端可以同时与同一设备通信。然而,多个客户端同时与相同设备的默认端点(即 EP0)通信,将导致它们的控制传输序列化。

要与设备通信,客户端必须满足以下条件:

#. 使用设备地址打开设备,告知主机库,客户端正在使用该设备。 #. 获取将用于通信的接口,防止其他客户端获取相同的接口。 #. 通过已经获取了使用权的设备接口的端点通信通道发送数据传输。客户端的任务负责处理其与 USB 设备通信相关的操作和事件。

.. ------------------------------------------------------ Usage --------------------------------------------------------

用法

主机库及底层主机栈不会创建任何任务,客户端任务和守护进程任务等均需由 Class 驱动程序或用户自行创建。然而,主机库提供了两个事件处理函数,可以处理所有必要的主机库操作,这些函数应从客户端任务和守护进程任务中重复调用。因此,客户端任务和守护进程任务的使用将主要集中于调用这些事件处理函数。

主机库与守护进程任务 ^^^^^^^^^^^^^^^^^^^^^^^^^^

基本用法 """""""""""

主机库 API 提供了 :cpp:func:usb_host_lib_handle_events,可以处理库事件。该函数需要反复调用,通常是从守护进程任务中调用。函数 :cpp:func:usb_host_lib_handle_events 有以下特点:

  • 该函数会持续阻塞,直至需要处理库事件。
  • 每次调用该函数都会返回事件标志,有助于了解卸载主机库的时机。

最基础的守护进程任务通常类似以下代码片段:

.. code-block:: c

#include "usb/usb_host.h"

void daemon_task(void *arg)
{
    ...
    bool exit = false;
    while (!exit) {
        uint32_t event_flags;
        usb_host_lib_handle_events(portMAX_DELAY, &event_flags);
        if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) {
            ...
        }
        if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) {
            ...
        }
        ...
    }
    ...
}

.. note::

了解守护进程任务的完整示例,请前往 :example:`peripherals/usb/host/usb_host_lib`。

生命周期 """""""""

.. figure:: ../../../_static/usb_host_lib_lifecycle.png :align: center :alt: USB 主机库典型生命周期 :figclass: align-center

USB 主机库典型生命周期

上图展示了 USB 主机库的典型生命周期,其中涉及多个客户端和设备。具体而言,示例涉及以下内容:

  • 两个已注册的客户端(客户端 1 和客户端 2)。
  • 两个已连接的设备(设备 1 和设备 2),其中客户端 1 与设备 1 通信,客户端 2 与设备 2 通信。

参考上图可知,典型 USB 主机库生命周期包括以下关键阶段:

  1. 调用 :cpp:func:usb_host_install,安装主机库。
    • 调用任意主机库 API 前,请确保已完成主机库安装。
    • 调用 :cpp:func:usb_host_install 的位置(如从守护进程任务或其他任务中调用)取决于守护系统任务、客户端任务和系统其余部分间的同步逻辑。
  2. 安装完主机库后,调用 :cpp:func:usb_host_client_register 注册客户端。
    • 该注册函数通常从客户端任务调用,而客户端任务需等待来自守护进程任务的信号。
    • 调用过 :cpp:func:usb_host_install 后,如有需要,也可以在其他地方调用该注册函数。
  3. 设备 1 连接,并进行枚举。
    • 每个已注册的客户端(本案例中为客户端 1 和客户端 2)都会通过 :cpp:enumerator:USB_HOST_CLIENT_EVENT_NEW_DEV 事件得到新设备的通知。
    • 客户端 1 开启设备 1,并与之通信。
  4. 设备 2 连接,并进行枚举。
    • 客户端 1 和 2 通过 :cpp:enumerator:USB_HOST_CLIENT_EVENT_NEW_DEV 事件得到新设备的通知。
    • 客户端 2 开启设备 2,并与之通信。
  5. 设备 1 突然断开连接。
    • 客户端 1 通过 :cpp:enumerator:USB_HOST_CLIENT_EVENT_DEV_GONE 得到通知,并开始清理,关闭和释放客户端 1 与设备 1 之间的关联资源。
    • 客户端 2 不会收到通知,因为它并未开启设备 1。
  6. 客户端 1 完成清理,调用 :cpp:func:usb_host_client_deregister 注销客户端。
    • 该注销函数通常在任务退出前,从客户端任务中调用。
    • 如有需要,只要客户端 1 已完成清理,也可以在其他地方调用该注销函数。
  7. 客户端 2 完成与设备 2 的通信,随后关闭设备 2,并自行注销。
    • 由于客户端 2 是最后一个注销的客户端,通过 :c:macro:USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS 事件标志,守护进程任务可以得知,所有客户端已注销。
    • 设备 2 未释放,仍会进行分配,因为虽然没有任何客户端打开设备 2,但它仍处于连接状态。
  8. 所有客户端注销后,守护进程任务开始清理。
    • 守护进程任务需要先调用 :cpp:func:usb_host_device_free_all,释放设备 2。
    • 如果 :cpp:func:usb_host_device_free_all 能够成功释放所有设备,函数将返回 ESP_OK,表明已释放所有设备。
    • 如果 :cpp:func:usb_host_device_free_all 无法成功释放所有设备,例如因为设备仍由某个客户端开启,函数将返回 ESP_ERR_NOT_FINISHED
    • 守护进程任务必须等待 :cpp:func:usb_host_lib_handle_events 返回事件标志 :c:macro:USB_HOST_LIB_EVENT_FLAGS_ALL_FREE,方知何时所有设备均已释放。
  9. 一旦守护进程任务确认所有客户端均已注销,且所有设备均已释放,便可调用 :cpp:func:usb_host_uninstall,卸载主机库。

客户端与 Class 驱动程序 ^^^^^^^^^^^^^^^^^^^^^^^^^^

基本用法 """""""""""

主机库 API 提供函数 :cpp:func:usb_host_client_handle_events,可以处理特定客户端事件。该函数需要反复调用,通常是从客户端任务中调用。函数 :cpp:func:usb_host_client_handle_events 有以下特点:

  • 该函数可以持续阻塞,直至需要处理客户端事件。
  • 该函数的主要目的是发生客户端事件时,调用多个事件处理回调函数。

以下回调函数均从 :cpp:func:usb_host_client_handle_events 中调用,因此客户端任务能够获取事件通知。

  • 类型为 :cpp:type:usb_host_client_event_cb_t 的客户端事件回调函数会将客户端事件消息传递给客户端,提示添加、移除设备等事件。
  • 类型为 :cpp:type:usb_transfer_cb_t 的 USB 传输完成回调函数表明,先前由客户端提交的特定 USB 传输已完成。

.. note::

考虑到上述回调函数从 :cpp:func:`usb_host_client_handle_events` 中调用,应避免在回调函数内部阻塞,否则将导致 :cpp:func:`usb_host_client_handle_events` 阻塞,阻止其他待处理的客户端事件得到处理。

以下代码片段展示了一个基础的主机 Class 驱动程序及其客户端任务,代码片段中包括:

  • 一个简单的客户端任务函数 client_task,它会在循环中调用 :cpp:func:usb_host_client_handle_events
  • 使用客户端事件回调函数和传输完成回调函数。
  • 一个用于 Class 驱动程序的简单状态机。该 Class 驱动程序仅支持打开设备,发送 OUT 传输到 EP1,然后关闭设备。

.. code-block:: c

#include <string.h>
#include "usb/usb_host.h"

#define CLASS_DRIVER_ACTION_OPEN_DEV    0x01
#define CLASS_DRIVER_ACTION_TRANSFER    0x02
#define CLASS_DRIVER_ACTION_CLOSE_DEV   0x03

struct class_driver_control {
    uint32_t actions;
    uint8_t dev_addr;
    usb_host_client_handle_t client_hdl;
    usb_device_handle_t dev_hdl;
};

static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg)
{
    //该函数从 usb_host_client_handle_events() 中调用,请勿在此阻塞,并尽量保持简洁
    struct class_driver_control *class_driver_obj = (struct class_driver_control *)arg;
    switch (event_msg->event) {
        case USB_HOST_CLIENT_EVENT_NEW_DEV:
            class_driver_obj->actions |= CLASS_DRIVER_ACTION_OPEN_DEV;
            class_driver_obj->dev_addr = event_msg->new_dev.address; //存储新设备的地址
            break;
        case USB_HOST_CLIENT_EVENT_DEV_GONE:
            class_driver_obj->actions |= CLASS_DRIVER_ACTION_CLOSE_DEV;
            break;
        default:
            break;
    }
}

static void transfer_cb(usb_transfer_t *transfer)
{
    //该函数从 usb_host_client_handle_events() 中调用,请勿在此阻塞,并尽量保持简洁
    struct class_driver_control *class_driver_obj = (struct class_driver_control *)transfer->context;
    printf("Transfer status %d, actual number of bytes transferred %d\n", transfer->status, transfer->actual_num_bytes);
    class_driver_obj->actions |= CLASS_DRIVER_ACTION_CLOSE_DEV;
}

void client_task(void *arg)
{
    ... //等待主机库安装
    //初始化 Class 驱动程序对象
    struct class_driver_control class_driver_obj = {0};
    //注册客户端
    usb_host_client_config_t client_config = {
        .is_synchronous = false,
        .max_num_event_msg = 5,
        .async = {
            .client_event_callback = client_event_cb,
            .callback_arg = &class_driver_obj,
        }
    };
    usb_host_client_register(&client_config, &class_driver_obj.client_hdl);
    //分配一个 USB 传输
    usb_transfer_t *transfer;
    usb_host_transfer_alloc(1024, 0, &transfer);

    //事件处理循环
    bool exit = false;
    while (!exit) {
        //调用客户端事件处理函数
        usb_host_client_handle_events(class_driver_obj.client_hdl, portMAX_DELAY);
        //执行待处理的 Class 驱动程序操作
        if (class_driver_obj.actions & CLASS_DRIVER_ACTION_OPEN_DEV) {
            //开启设备,声明接口 1
            usb_host_device_open(class_driver_obj.client_hdl, class_driver_obj.dev_addr, &class_driver_obj.dev_hdl);
            usb_host_interface_claim(class_driver_obj.client_hdl, class_driver_obj.dev_hdl, 1, 0);
        }
        if (class_driver_obj.actions & CLASS_DRIVER_ACTION_TRANSFER) {
            //发送一个 OUT 传输到 EP1
            memset(transfer->data_buffer, 0xAA, 1024);
            transfer->num_bytes = 1024;
            transfer->device_handle = class_driver_obj.dev_hdl;
            transfer->bEndpointAddress = 0x01;
            transfer->callback = transfer_cb;
            transfer->context = (void *)&class_driver_obj;
            usb_host_transfer_submit(transfer);
        }
        if (class_driver_obj.actions & CLASS_DRIVER_ACTION_CLOSE_DEV) {
            //释放接口,关闭设备
            usb_host_interface_release(class_driver_obj.client_hdl, class_driver_obj.dev_hdl, 1);
            usb_host_device_close(class_driver_obj.client_hdl, class_driver_obj.dev_hdl);
            exit = true;
        }
        ... //处理其他 Class 驱动程序要求的行为
    }

    //清理 Class 驱动程序
    usb_host_transfer_free(transfer);
    usb_host_client_deregister(class_driver_obj.client_hdl);
    ... //删除客户端任务。如有需要,向守护进程任务发送信号。
}

.. note::

在实际应用中,主机 Class 驱动程序还能支持更多功能,因此也存在更复杂的状态机。主机 Class 驱动程序可能需要:

- 能够开启多个设备
- 解析已开启设备的描述符,确定设备是否是目标 Class
- 按特定顺序,与接口的多个端点通信
- 声明设备的多个接口
- 处理各种错误情况

生命周期 """""""""

客户端任务与 Class 驱动程序的典型生命周期包括以下关键阶段:

#. 等待与主机库有关的信号完成安装。 #. 通过 :cpp:func:usb_host_client_register 注册客户端,并分配其他的 Class 驱动程序资源,如使用 :cpp:func:usb_host_transfer_alloc 分配传输。 #. 对于 Class 驱动程序需要与之通信的各个新设备:

a. 调用 :cpp:func:`usb_host_device_addr_list_fill`,检查设备是否已连接。
b. 如果设备尚未连接,则等待客户端事件回调函数的 :cpp:enumerator:`USB_HOST_CLIENT_EVENT_NEW_DEV` 事件。
c. 调用 :cpp:func:`usb_host_device_open` 开启设备。
d. 分别调用 :cpp:func:`usb_host_get_device_descriptor` 和 :cpp:func:`usb_host_get_active_config_descriptor`,解析设备和配置描述符。
e. 调用 :cpp:func:`usb_host_interface_claim`,声明设备的必要接口。

#. 调用 :cpp:func:usb_host_transfer_submit 或 :cpp:func:usb_host_transfer_submit_control,向设备提交传输。 #. 一旦 :cpp:enumerator:USB_HOST_CLIENT_EVENT_DEV_GONE 事件表示,Class 驱动程序不再需要已打开的设备,或者设备断开连接:

a. 在这些端点上调用 :cpp:func:`usb_host_endpoint_halt` 和 :cpp:func:`usb_host_endpoint_flush`,停止先前提交的传输。
b. 调用 :cpp:func:`usb_host_interface_release`,释放先前声明的所有接口。
c. 调用 :cpp:func:`usb_host_device_close`,关闭设备。

#. 调用 :cpp:func:usb_host_client_deregister 注销客户端,并释放其他 Class 驱动程序资源。 #. 删除客户端任务。如有需要,向守护进程任务发送信号。

.. ---------------------------------------------------- 电源管理 -----------------------------------------------

电源管理

全局挂起/恢复 ^^^^^^^^^^^^^^^^^^^^^

USB 主机库支持全局挂起/恢复,也就是挂起或恢复整个 USB 总线。全局挂起恢复通过根端口实现。挂起时,USB 主机会停止发送 SOF 包,从而使连接到 USB 总线的所有设备进入挂起状态。

请注意,全局挂起/恢复并 不会 在 USB 主机端挂起/恢复 USB-OTG 外设,因此并 不会 降低主机控制器本身的功耗。

事件 ^^^^^^

客户端事件 """""""""""""

当设备被挂起或恢复时,所有已打开该受影响设备的客户端都会通过以下事件收到通知。每个事件都包含该被挂起或已恢复设备的句柄:

  • :cpp:enumerator:USB_HOST_CLIENT_EVENT_DEV_SUSPENDED — 设备已进入挂起状态。

  • :cpp:enumerator:USB_HOST_CLIENT_EVENT_DEV_RESUMED — 设备已恢复运行。

USB 主机库事件 """"""""""""""""""""

下列事件与自动挂起定时器相关联。定时器超时后,其回调函数会解除 USB 主机库处理程序的阻塞并传递该事件:

  • :cpp:enumerator:USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND — 表示自动挂起定时器已超时。

自动挂起定时器 ^^^^^^^^^^^^^^^^^^^^^^^^^^^

可使用 :cpp:func:usb_host_lib_set_auto_suspend 配置自动挂起定时器。每当 USB 主机库的客户端处理函数处理来自任意客户端的事件时,或当 USB 主机库本身在处理任何事件时,自动挂起定时器都会被重置。

自动挂起定时器会主动监测 USB 总线活动以及 USB 主机库的活动情况。具体如下:

  • 如果检测到 USB 流量(即发生任何传输或由客户端处理的事件),定时器会自动重置。
  • 如果检测到 USB 主机库的活动(即有新设备连接),定时器会自动重置。
  • 如果 USB 总线和 USB 主机库保持空闲(无流量、客户端事件或库事件),定时器继续倒计时。
  • 一旦定时器达到指定的超时时间(以毫秒为单位),将触发挂起。

这种机制确保 USB 主机仅在总线真正空闲时才进入挂起模式,避免在活跃通信期间出现意外挂起。

自动挂起定时器的重要注意事项:

  • 定时器可配置,即使未连接任何设备也会开始倒计时。

  • 所有设备断开连接后,定时器停止。

  • 设备连接或断开时,定时器自动重置。

  • 定时器超时后,只有在以下情况下才会派发 USB 主机库事件:

    • 当前有设备连接到根端口,并且
    • 根端口尚未处于挂起状态

自动挂起定时器可以配置为以下几种模式:

  • 单次 (:cpp:enumerator:USB_HOST_LIB_AUTO_SUSPEND_ONE_SHOT):定时器超时一次后停止。
  • 周期性 (:cpp:enumerator:USB_HOST_LIB_AUTO_SUSPEND_PERIODIC):定时器在每次超时后自动重启,无限重复。

由传输提交触发自动恢复 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

除了自动挂起定时器之外,USB 主机库还支持在传输提交(控制或非控制)时对已挂起的根端口进行自动恢复。通过此功能,开发人员只需调用传输 API(例如 :cpp:func:usb_host_transfer_submit 或 :cpp:func:usb_host_transfer_submit_control)即可发起恢复,无需显式调用 :cpp:func:usb_host_lib_root_port_resume

当根端口处于挂起状态,且有客户端向已挂起的设备提交传输请求时,USB 主机库将执行以下流程:

  1. 自动向根端口发起恢复信号。
  2. 暂停处理该传输请求,直到恢复信号完成。
  3. 待总线进入活动(已恢复)状态后提交传输。

这一机制简化了客户端实现,避免了因手动触发恢复与提交传输并行进行而可能产生的竞态条件,确保恢复行为一致且安全。

.. note::

仅在根端口处于挂起状态时,才能通过传输提交进行自动恢复。若根端口已处于活动状态,传输提交将照常进行。

下面的代码片段演示了如何使用自动挂起定时器和传输提交自动恢复功能,在挂起与恢复状态之间循环切换。

.. code-block:: c

#include "usb/usb_host.h"

static void client_task(void *arg)
{
    while(true) {
        // 向挂起的设备提交传输,以自动恢复设备
        // 借助自动挂起定时器,设备在无操作 1 秒后自动进入挂起模式
        usb_host_transfer_submit(xfer_out);

        // 切换上下文 10 秒。设备将在空闲约 1 秒后自动挂起
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

void usb_host_lib_task(void *arg)
{
    ...

    // 将自动挂起定时器设置为周期模式,周期为 1 秒,
    // 用于在无操作 1 秒后自动挂起设备,并在超时后自动重启
    usb_host_lib_set_auto_suspend(USB_HOST_LIB_AUTO_SUSPEND_PERIODIC, 1000);

    while (1) {
        uint32_t event_flags;
        usb_host_lib_handle_events(portMAX_DELAY, &event_flags);

        if (event_flags & USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND) {
            // 自动挂起定时器超时,挂起根端口
            usb_host_lib_root_port_suspend();
        }
        ...
    }
    ...
}

.. note::

关于挂起与恢复的更多细节,请参阅 `USB 2.0 规范 <https://www.usb.org/document-library/usb-20-specification>`_ > 第 11.9 章 *Suspend and Resume*。

.. ---------------------------------------------------- Examples -------------------------------------------------------

示例

主机库示例 ^^^^^^^^^^^^^^^^^^^^^

:example:peripherals/usb/host/usb_host_lib 演示了如何使用 USB 主机库 API 来安装和注册客户端、等待设备连接、打印设备信息和处理断开连接,并重复这些步骤,直到退出应用程序。

Class 驱动程序示例 ^^^^^^^^^^^^^^^^^^^^^^^^

USB 主机栈提供了大量示例,展示了如何通过使用主机库 API 创建主机 Class 驱动程序。

CDC-ACM """""""

  • 通信设备 Class(抽象控制模型)的主机 Class 驱动程序通过 乐鑫组件注册表 <https://components.espressif.com/component/espressif/usb_host_cdc_acm>__ 作为受管理的组件分发。
  • 示例 :example:peripherals/usb/host/cdc 演示了如何使用 CDC-ACM 主机驱动程序,让 {IDF_TARGET_NAME} 与 USB CDC-ACM 设备通信,包括 CP210x、FTDI FT23x 或 CH34x 等厂商设备。
  • 示例 esp_modem <https://github.com/espressif/esp-protocols/tree/master/components/esp_modem/examples>__ 中也使用了 CDC-ACM 驱动程序,该程序在这些示例中与蜂窝模块通信。

MSC """

  • 大容量存储 Class(仅支持批量传输)的主机 Class 驱动程序已部署到 乐鑫组件注册表 <https://components.espressif.com/component/espressif/usb_host_msc>__。
  • 示例 :example:peripherals/usb/host/msc 演示了如何使用 USB 大容量存储类来访问、读取、写入和操作 USB 闪存驱动器,包括处理 USB 重新连接和反初始化 USB 主机堆栈。

HID """

  • HID(人机接口设备)的主机 class 驱动作为托管组件通过 乐鑫组件注册表 <https://components.espressif.com/components/espressif/usb_host_hid>__ 分发。
  • 示例 :example:peripherals/usb/host/hid 演示了如何在 {IDF_TARGET_NAME} 上实现基本的 USB 主机 HID 类驱动,以便与 USB HID 设备(如键盘和鼠标)进行通信,并持续扫描设备的连接状态。一旦连接成功,即获取 HID 报告。

UVC """

  • USB 视频设备 Class 的主机 Class 驱动程序作为托管组件通过 乐鑫组件注册表 <https://components.espressif.com/component/espressif/usb_host_uvc>__ 分发。
  • 示例 :example:peripherals/usb/host/uvc 演示了如何使用 UVC 驱动程序从 USB 摄像头捕获视频帧。

.. ---------------------------------------------- USB Host Menuconfig --------------------------------------------------

.. only:: esp32s3

外部 PHY 配置
-------------

{IDF_TARGET_NAME} 内部集成了两个 USB 控制器 —— USB-OTG 和 USB-Serial-JTAG。这两个控制器 **共用同一个 PHY**,因此同一时间只能有一个控制器工作。如果在 USB-Serial-JTAG 工作时(如调试或烧录)时仍需使用 USB 主机功能,必须使用 **外部 PHY**,因为此时内部 PHY 已被 USB-Serial-JTAG 占用。

.. note::
    使用外部 PHY 并不是在 USB 主机或设备功能开启时同时实现调试的唯一办法。也可以通过烧录对应的 eFuse,将调试接口从 USB-Serial-JTAG 切换为传统的 JTAG 接口。具体步骤请参考 ESP-IDF 编程指南中针对你的芯片的 `JTAG 调试 <https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-guides/jtag-debugging/index.html>`_ 章节。

{IDF_TARGET_NAME} 支持连接外部 PHY 芯片,从而实现 USB-OTG 和 USB-Serial-JTAG 控制器的独立工作。不同的外部 PHY 芯片可能需要不同的硬件配置,具体请参阅各芯片的规格书。乐鑫官方文档提供了通用的连接示意图用于参考:`使用外部 PHY <https://docs.espressif.com/projects/esp-iot-solution/en/latest/usb/usb_overview/usb_phy.html#use-an-external-phy>`__。

**已测试的外部 PHY 芯片如下:**

- **SP5301** — {IDF_TARGET_NAME} 原生支持此芯片。原理图与布线方法请参考上文链接。
- **TUSB1106** — {IDF_TARGET_NAME} 原生支持。可通过 GPIO 映射与外部 PHY 驱动配合使用。请遵循 TUSB1106 数据手册中的参考连接(供电方案以及在 D+/D– 上建议的串联电阻)。
- **STUSB03E** — 需要通过模拟开关进行信号路由。请参考下方示例。

.. figure:: ../../../_static/usb_host/ext_phy_schematic_stusb03e.png
   :align: center
   :alt: 使用模拟开关的外部 PHY 原理图(主机模式)

   使用 STUSB03E 与模拟开关的连接示例(主机模式)

.. note::

    此原理图为简化示例,用于演示外部 PHY 连接方式,未包含完整 {IDF_TARGET_NAME} 设计所需的所有元器件和信号(如 VCC、GND、RESET 等)。

    图中包含 +5 V 电源轨(用于为 USB 设备供电)和 VCC 电源轨(通常为 3.3 V)。VCC 电压应与芯片供电电压保持一致。确保为 USB 总线提供的 +5 V 电源可靠,并具备必要的保护措施(如电源开关和限流设计)在支持 USB 总线供电设备时,务必遵守 USB 主机的供电规范。

硬件配置通过将 GPIO 映射到 PHY 引脚实现。任何未使用的引脚(如 :cpp:member:`usb_phy_ext_io_conf_t::suspend_n_io_num`) **必须设置为 -1**。

.. note::

    :cpp:member:`usb_phy_ext_io_conf_t::suspend_n_io_num` 引脚 **当前不支持**,无需连接。

**示例代码:**

.. code-block:: c

    // 外部 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,
        .fs_edge_sel_io_num = 38,
        .suspend_n_io_num = -1,
    };

    // 针对 OTG 控制器(Host 模式)的外部 PHY 配置与初始化
    const usb_phy_config_t phy_config = {
        .controller = USB_PHY_CTRL_OTG,
        .target = USB_PHY_TARGET_EXT,
        .otg_mode = USB_OTG_MODE_HOST,
        .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));

    // 配置 USB 主机使用外部初始化的 PHY
    usb_host_config_t host_config = {
        .skip_phy_setup = true,
        // 根据需求添加其他 host 配置字段
    };
    ESP_ERROR_CHECK(usb_host_install(&host_config));

该配置确保 USB 主机协议栈使用 **外部 PHY**,并跳过 PHY 初始化步骤。

主机栈配置

非兼容设备支持 ^^^^^^^^^^^^^^

为了支持某些非兼容或具有特定行为的 USB 设备,可以对 USB 主机栈进行配置。

USB 设备可能是热插拔的,因此必须配置电源开关和设备连接之间的延迟,以及设备内部电源稳定后的延迟。

枚举配置 """"""""

在枚举已连接 USB 设备的过程中,需要给一些事件配置合适的间隔时间以确保设备正常运行。

.. figure:: ../../../_static/usb_host/poweron-timings.png :align: center :alt: USB 根集线器上电和连接事件时序

USB 根集线器上电和连接事件时序

上图展示了与连接设备时开启端口电源和热插拔设备相关的所有间隔时间。

  • 端口复位或恢复运行后,USB 系统软件应提供 10 毫秒的恢复时间,此后连接到端口的设备才会响应数据传输。
  • 恢复时间结束后,如果设备收到 SetAddress() 请求,设备必须能够完成对该请求的处理,并能在 50 毫秒内成功完成请求的状态 (Status) 阶段。
  • 状态阶段结束后,设备允许有 2 毫秒的 SetAddress() 恢复时间。

.. note::

有关连接事件时序的更多信息,请参阅 `通用串行总线 2.0 规范 <https://www.usb.org/document-library/usb-20-specification>`_ > 第 7.1.7.3 章 *连接和断开信令*。

可通过 Menuconfig 选项设置 USB 主机栈的可配置参数。

  • CONFIG_USB_HOST_DEBOUNCE_DELAY_MS 用于配置防抖延迟。
  • CONFIG_USB_HOST_RESET_HOLD_MS 用于配置重置保持时间。
  • CONFIG_USB_HOST_RESET_RECOVERY_MS 用于配置重置恢复时间。
  • CONFIG_USB_HOST_SET_ADDR_RECOVERY_MS 用于配置 SetAddress() 恢复时间。

下游端口配置 ^^^^^^^^^^^^

当支持外部 Hub 功能时,可以为外部 Hub 端口配置多个参数。

每个外部 Hub 都有一个 Hub 描述符,用于描述设备特性。

.. note::

有关 Hub 描述符的详细信息,请参考 `USB 2.0 规范 <https://www.usb.org/document-library/usb-20-specification>`_ > 章节 11.23.2.1 *Hub Descriptor*。

可以通过 Menuconfig 配置下游端口的可配置参数。

  • 对于在端口上电后稳定电源的自定义值(PwrOn2PwrGood 值),请参阅 CONFIG_USB_HOST_EXT_PORT_CUSTOM_POWER_ON_DELAY_MS
  • 对于复位恢复间隔,请参阅 CONFIG_USB_HOST_EXT_PORT_RESET_RECOVERY_DELAY_MS

.. note::

规范规定,对于没有电源开关的 Hub,PwrOn2PwrGood 必须设置为零。同时,对于某些设备,可以增加此值以提供额外的上电时间。如需启用此功能,请参考 `CONFIG_USB_HOST_EXT_PORT_CUSTOM_POWER_ON_DELAY_ENABLE`。

主机通道 """""""""""""

当启用外部 Hub 支持功能(CONFIG_USB_HOST_HUBS_SUPPORTED)时,主机通道的数量非常重要,因为每个下游设备都需要空闲通道。

每个连接的设备需要不同数量的通道,而所需通道数则取决于设备类别(EP 数量)。

对于 {IDF_TARGET_NAME},支持的通道数量为 {IDF_TARGET_OTG_NUM_HOST_CHAN}。

.. note::

- 需要一个空闲通道来枚举设备。
- 需要 1 到 N(N 为 EP 数量)个空闲通道来占用接口。
- 如果所有的主机通道都已经被占用,则设备无法进行枚举,也无法获取接口。

多项配置支持 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

对于具有多项配置的 USB 设备,可以在设备枚举过程中指定所需的配置编号。

枚举过滤器 """"""""""""""""""

枚举过滤器是类型为 :cpp:type:usb_host_enum_filter_cb_t 的回调函数。从新连接的 USB 设备上读取设备描述符后,USB 主机栈会在枚举过程开始时调用枚举过滤器,从而为用户提供读取的设备描述符。借助此回调,用户得以:

  • 选择 USB 设备的配置。
  • 过滤应该进行枚举的 USB 设备。

在 menuconfig 中启用 CONFIG_USB_HOST_ENABLE_ENUM_FILTER_CALLBACK 选项即可启用枚举过滤器。可以通过设置 :cpp:member:usb_host_config_t::enum_filter_cb 来指定回调函数,该函数会在调用 :cpp:func:usb_host_install 时传递至主机库。

.. -------------------------------------------------- API Reference ----------------------------------------------------

API 参考

USB 主机库的 API 包含以下头文件,但应用程序调用该 API 时只需 #include "usb/usb_host.h",就可以包含所有 USB 主机库的头文件。

  • usb/include/usb/usb_host.h 包含 USB 主机库的函数和类型。
  • usb/include/usb/usb_helpers.h 包含与 USB 协议相关的各种辅助函数,如描述符解析等。
  • usb/include/usb/usb_types_stack.h 包含在 USB 主机栈的多个层次中使用的类型。
  • usb/include/usb/usb_types_ch9.h 包含了与 USB 2.0 规范中第 9 章相关的类型和宏,即描述符和标准请求。
  • usb/include/usb/usb_types_ch11.h 包含与 USB2.0 规范第 11 章相关的类型和宏,即集线器规范。

头文件 ^^^^^^^

  • usb_host.h 可以通过以下方式包含:

.. code:: c

#include "usb/usb_host.h"
  • 该头文件是 usb 组件提供的 API 的一部分。usb 组件通过 乐鑫组件注册表 <https://components.espressif.com/components/espressif/usb>__ 分发。因此,若要使用该组件,请通过以下命令将 Host Stack 组件添加为依赖项:

.. code:: bash

idf.py add-dependency usb

.. ------------------------------------------------ Maintainers Notes --------------------------------------------------

维护注意事项

.. note::

有关 USB 主机栈内部实现的更多细节,请参阅 :doc:`/api-reference/peripherals/usb_host/usb_host_notes_index`。

.. toctree:: :hidden: :maxdepth: 0

usb_host/usb_host_notes_index