USB 主机维护者注意事项（设计指南）
=====================================

:link_to_translation:`en:[English]`

设计考虑
--------

设计主机协议栈时应考虑以下要点：

**硬件资源有限：**

与较大的主机系统相比，嵌入式主机协议栈的硬件资源（如内存和处理能力）有限。

**USB 2.0 第 10 章：**

USB 2.0 规范的第 10 章中规定了 USB 主机系统的某些要求，特别是 USB 主机系统软件所需的必要层次。

**用例复杂性广泛：**

嵌入式主机协议栈用例广泛，包含不同的复杂性。一些 USB 主机应用只支持单个厂商的特定设备，而其他应用则需要支持不同类别的各种设备。

要求
----

考虑到以上设计要素，在设计主机协议栈时满足了以下要求：

支持 DMA
^^^^^^^^^^

**要求：主机协议栈必须支持 DMA。**

为降低 CPU 负载，主机协议栈必须支持 DMA。通过 DMA，无需 CPU 干预，即可自动复制 USB 传输数据到主机控制器中，以及自动从主机控制器中复制 USB 传输数据。这一点对于嵌入式 CPU（即，较低的 CPU 频率）和较大的最大 USB 传输数据负载（例如，同步传输的 1023 字节）来说尤为重要。

减少内存拷贝
^^^^^^^^^^^^

**要求：在不同层之间传递数据时主机协议栈应尽量减少内存拷贝次数。**

各种数据和对象（例如 USB 传输）需要在主机协议栈的多个层之间传递。主机协议栈应一次性为数据或对象分配内存，并在各层之间传递指向该数据或对象的指针，从而减少各层间的内存拷贝次数。因此，主机协议栈需要能够在不同层之间共享的标准化数据类型（参见 USB 2.0 规范的第 10.3.4 节）。

支持事件驱动
^^^^^^^^^^^^

**要求：主机协议栈必须支持事件驱动（即，主机协议栈的 API 不是轮询的方式）。**

主机协议栈应支持 CPU 密集型应用场景，例如视频流传输（即，UVC Class）。因此，主机协议栈应支持事件驱动操作，减少 CPU 占用，从而将大部分 CPU 时间留给应用本身（如，视频编码或解码）。

主机协议栈需要通过中断、回调和 FreeRTOS 同步原语（例如队列和信号量）在各层之间传递事件。

不得创建任务
^^^^^^^^^^^^

**要求：主机库层及其下层的所有主机协议栈层都不得创建任何任务。**

任务栈通常是 ESP-IDF 应用程序中最占内存的部分之一。由于应用场景广泛，创建的任务数量（以及任务的栈大小）差别可能很大。例如：

- 需要低延迟或高吞吐量（例如同步传输）的应用可能会创建一个专门的任务来处理传输数据，确保延迟最小化。
- 对延迟要求不高（例如批量传输）的应用可能会通过共享任务来处理传输数据，以节省内存。

因此，主机库层及其下层的所有主机协议栈层都不得创建任何任务，而应公开处理函数，供上层创建的任务调用。任务创建将委托给 Class 驱动程序或应用程序层。

可在不同层操作
^^^^^^^^^^^^^^

主机协议栈用例广泛，情况复杂，应确保其可在不同层操作。因此无论是在较低层（例如 HCD 或 HAL），还是在较高层（例如 Class 驱动程序），用户都可以使用主机协议栈。

用户可以在不同的层上使用主机协议栈，并根据应用程序的特点，权衡便利性、可控性和优化度，例如：

- 支持专用定制设备的主机协议栈应用可能会使用较低级别的抽象，从而调用优化度高、可控性强、简单便利的 API。
- 支持各种设备 Class 的主机协议栈应用需要使用完整的主机协议栈，以便自动处理设备枚举。

编码约定
--------

主机协议栈遵循下列编码规范，以提高代码可读性与可维护性：

符号应使用层名作为前缀
^^^^^^^^^^^^^^^^^^^^^^

主机协议栈每层公开的符号（即函数、类型、宏）必须以该层的名称作为前缀。例如，HCD 层公开的符号将以 ``hcd_...`` 或 ``HCD_...`` 为前缀。

但内部符号（例如静态函数）**不应** 以其所在层的名称作为前缀，以便修改该层源代码时能更好地区分内部和外部符号。

临界区函数应以 ``_`` 为前缀
^^^^^^^^^^^^^^^^^^^^^^^^^^^

主机协议栈的每层中都有各种必须在临界区内调用的静态函数。这些函数的名称以 ``_`` 为前缀（例如，``_func_called_from_crit()``），以便维护者更好地区分哪些函数应该从临界区内调用。例如：

.. code-block:: c

    some_func();  // 从临界区外调用
    taskENTER_CRITICAL(&some_lock);
    _some_func_crit();  // 从临界区内调用。使用 _ 前缀，使其便于区分
    taskEXIT_CRITICAL(&some_lock);

根据锁机制对结构体成员进行分组
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

主机协议栈的某些层使用多种锁机制（例如临界区和任务互斥锁）来确保线程安全。每种锁机制能提供不同级别的保护，而同一对象的成员变量受不同锁机制保护。因此，为了清晰地区分不同的锁机制及其相关变量，结构体成员按锁机制被分组为嵌套结构体。

.. list-table:: 锁机制
    :widths: 20 10 80
    :header-rows: 1

    * - 锁机制
      - 嵌套结构体
      - 描述
    * - 临界区
      - ``dynamic``
      - 从任务上下文和 ISR 上下文访问的共享数据受临界区保护。
    * - 任务互斥锁
      - ``mux_protected``
      - 仅从任务上下文访问的共享数据受 FreeRTOS 互斥锁保护。
    * - 单线程
      - ``single_thread``
      - 只由同一任务访问的数据无需锁保护。
    * - 常量
      - ``constant``
      - 在对象实例化时设置常量数据，之后常量将不再改变。因此，只要变量不被写入，任何任务或 ISR 都可以自由访问常量数据。

根据锁机制对结构体成员进行分组，能清晰地显示访问特定成员变量时需要哪种锁机制，使得代码易于维护，如下所示：

.. code-block:: c

    typedef struct some_obj some_obj_t;

    some_obj_t obj;

    // 访问动态成员需要临界区
    taskENTER_CRITICAL(&some_lock);
    obj.dynamic.varA = 1;
    taskEXIT_CRITICAL(&some_lock);

    // 访问受互斥锁保护的成员需要获取互斥锁
    xSemaphoreTake(&some_mux, portMAX_DELAY);
    obj.mux_protected.varB = 1;
    xSemaphoreGive(&some_mux);

    // 只由某一任务访问的单线程成员数据无需锁保护
    obj.single_thread.varC = 1;

    // 访问常量成员无需锁保护，但此种访问为只读
    int local_var = obj.constant.varD;
