警告

This document is not updated for ESP32P4 yet, so some of the content may not be correct.

This warning was automatically inserted due to the source file being in the add_warnings_pages list.

USB 主机维护者注意事项(设计指南)

[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()),以便维护者更好地区分哪些函数应该从临界区内调用。例如:

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

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

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

锁机制

锁机制

嵌套结构体

描述

临界区

dynamic

从任务上下文和 ISR 上下文访问的共享数据受临界区保护。

任务互斥锁

mux_protected

仅从任务上下文访问的共享数据受 FreeRTOS 互斥锁保护。

单线程

single_thread

只由同一任务访问的数据无需锁保护。

常量

constant

在对象实例化时设置常量数据,之后常量将不再改变。因此,只要变量不被写入,任何任务或 ISR 都可以自由访问常量数据。

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

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;

此文档对您有帮助吗?