USB 主机维护者注意事项(设计指南)
设计考虑
设计主机协议栈时应考虑以下要点:
硬件资源有限:
与较大的主机系统相比,嵌入式主机协议栈的硬件资源(如内存和处理能力)有限。
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);
根据锁机制对结构体成员进行分组
主机协议栈的某些层使用多种锁机制(例如临界区和任务互斥锁)来确保线程安全。每种锁机制能提供不同级别的保护,而同一对象的成员变量受不同锁机制保护。因此,为了清晰地区分不同的锁机制及其相关变量,结构体成员按锁机制被分组为嵌套结构体。
锁机制 |
嵌套结构体 |
描述 |
---|---|---|
临界区 |
|
从任务上下文和 ISR 上下文访问的共享数据受临界区保护。 |
任务互斥锁 |
|
仅从任务上下文访问的共享数据受 FreeRTOS 互斥锁保护。 |
单线程 |
|
只由同一任务访问的数据无需锁保护。 |
常量 |
|
在对象实例化时设置常量数据,之后常量将不再改变。因此,只要变量不被写入,任何任务或 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;