1. 引言
CherryUSB项目于2022年发布,旨在为资源受限的嵌入式系统提供一套简洁易学且高效的USB协议栈[1]。在设计上,CherryUSB采用树状化编程,所有类驱动和USBIP驱动模板化设计,没有复杂的C语言语法,方便用户学习和新增驱动。对用户常用的数据收发API采用类比法,将复杂USB通信类比为UART + DMA通信,简化了通信逻辑,方便用户使用。为了突出USB的性能,主从驱动直接对接寄存器操作,并且使用零拷贝、硬件DMA直通和中断快响应等技术,充分释放USB硬件带宽。
自发布以来,CherryUSB得到了众多开发者和半导体企业的支持,包括博流智能、先楫、平头哥、飞腾、恩智浦、乐鑫、匠心创、博通、嘉楠等。并应用到众多嵌入式设备,包括鼠标、键盘、可视门铃、网络通信、图传、存储、调试、bootrom等应用。并且CherryUSB采用Apache 2.0授权,可免费在商用解决方案中使用。
随着热度的提升,有越来越多的开源项目使用 CherryUSB,同样CherryUSB也对火热的开源项目进行了支持,包括daplink,blackmagic,lvgl,lwip,nuttx,zephyr等,并加入到rt-thread国产OS项目中,成为rt-thread标准USB协议栈。
USB协议栈是一个庞大的体系,如何化繁为简,降低用户开发门槛,理解USB的实现原理,是一个难题。本文将对CherryUSB的主机和从机代码进行原理性分析,提取关键信息,帮用户理清脉络,并使用rt-thread artpi2开源硬件进行移植和主从应用的实践。
2. CherryUSB从机协议栈概述
CherryUSB从机协议栈主要包括从机控制器IP驱动,USB设备枚举,USB驱动加载。其中,设备枚举和驱动加载部分包括USB描述符注册,USB接口驱动注册,USB端点注册。CherryUSB设备协议栈支持多种USB设备类驱动,包括CDC、HID、MSC、VIDEO、AUDIO、RNDIS、MTP等,基本涵盖所有常用类驱动。本章将从IP驱动设计切入,分析从机协议栈的整个实现原理。
2.1. 从机控制器IP设计
USB从机控制器IP是一种专用集成电路设计模块,用于实现USB协议栈的数据链路层功能,使嵌入式系统能够作为USB从设备与主机建立通信。负责管理PHY交互、数据包处理、端点配置和传输调度等底层操作,同时向上层软件提供标准寄存器定义。而从机控制器IP驱动则是根据这些寄存器定义,进行一些配置,使得USB设备能够与主机进行通信。目前,在CherryUSB中,支持多种从机控制器IP,商业性的IP包括MUSB、DWC2、FOTG210等。下面我们以DWC2为例,分析该IP的设计思路。
1) DWC2 IP寄存器定义
DWC2 IP寄存器包含全局寄存器、从机寄存器、主机寄存器三类。从机寄存器中又包含端点寄存器组,包括IN端点寄存器组和OUT端点寄存器组,各支持最大16组。寄存器组的定义如下:
typedef struct
{
__IO uint32_t DIEPCTL; /*!< dev IN Endpoint Control Reg 900h + (ep_num * 20h) + 00h */
uint32_t Reserved04; /*!< Reserved 900h + (ep_num * 20h) + 04h */
__IO uint32_t DIEPINT; /*!< dev IN Endpoint Itr Reg 900h + (ep_num * 20h) + 08h */
uint32_t Reserved0C; /*!< Reserved 900h + (ep_num * 20h) + 0Ch */
__IO uint32_t DIEPTSIZ; /*!< IN Endpoint Txfer Size 900h + (ep_num * 20h) + 10h */
__IO uint32_t DIEPDMA; /*!< IN Endpoint DMA Address Reg 900h + (ep_num * 20h) + 14h */
__IO uint32_t DTXFSTS; /*!< IN Endpoint Tx FIFO Status Reg 900h + (ep_num * 20h) + 18h */
uint32_t Reserved18; /*!< Reserved 900h + (ep_num*20h) + 1Ch-900h + (ep_num * 20h) + 1Ch */
} DWC2_INEndpointTypeDef;
typedef struct
{
__IO uint32_t DOEPCTL; /*!< dev OUT Endpoint Control Reg B00h + (ep_num * 20h) + 00h */
uint32_t Reserved04; /*!< Reserved B00h + (ep_num * 20h) + 04h */
__IO uint32_t DOEPINT; /*!< dev OUT Endpoint Itr Reg B00h + (ep_num * 20h) + 08h */
uint32_t Reserved0C; /*!< Reserved B00h + (ep_num * 20h) + 0Ch */
__IO uint32_t DOEPTSIZ; /*!< dev OUT Endpoint Txfer Size B00h + (ep_num * 20h) + 10h */
__IO uint32_t DOEPDMA; /*!< dev OUT Endpoint DMA Address B00h + (ep_num * 20h) + 14h */
uint32_t Reserved18[2]; /*!< Reserved B00h + (ep_num * 20h) + 18h - B00h + (ep_num * 20h) + 1Ch */
} DWC2_OUTEndpointTypeDef;
从上述寄存器总结如下表1,从中得出,数据的传输,主要是依靠端点,并配置端点相关的寄存器,下表1中IN代表发送,OUT代表接收:
Table 1. DWC2 IP endpoint register description
表1. DWC2 IP 端点寄存器说明
寄存器 |
功能 |
DIEPCTL\DOEPCTL |
配置端点属性,类型,最大数据包长度 |
DIEPTSIZ\DOEPTSIZ |
配置端点发送或者接收长度 |
DIEPDMA\DOEPDMA |
配置端点发送或者接收buffer地址 |
DIEPINT\DOEPINT |
配置发送或者接收端点中断 |
2) DWC2 IP中断
DWC2的中断标志有很多,这里只列举常用的一些中断,包括:
USB_OTG_GINTSTS_OEPINT
USB_OTG_GINTSTS_IEPINT
USB_OTG_GINTSTS_USBRST
USB_OTG_GINTSTS_USBSUSP
USB_OTG_GINTSTS_WKUINT
USB_OTG_GINTSTS_OEPINT和USB_OTG_GINTSTS_IEPINT代表是端点中断,其余几个表示USB的一些状态。在端点中断中,又进一步划分
Table 2. DWC2 IP OUT endpoint interrupt register description
表2. DWC2 IP OUT端点中断标志说明
中断标志 |
功能 |
USB_OTG_DOEPINT_XFRC |
接收完成中断 |
USB_OTG_DOEPINT_STUP |
接收 SETUP 包完成中断 |
Table 3. DWC2 IP IN endpoint interrupt register description
表3. DWC2 IP IN端点中断标志说明
中断标志 |
功能 |
USB_OTG_DIEPINT_XFRC |
发送完成中断 |
USB_OTG_DIEPINT_TXFE |
发送空中断,用于持续发送数据,直到发送完成 |
根据表2和表3,可以分析出,从机控制器IP的特点包括:多组端点寄存器、发送(IN)完成中断、接收(OUT)完成中断、复位\挂起\恢复等状态中断。同样地,其余从机控制器IP也是包含这些特点,根据这些特点,从而能够帮助我们定义从机控制器驱动的API。
2.2. 从机控制器驱动设计
根据从机控制器IP的特点,CherryUSB中制定了通用标准的从机控制器驱动API (cherryusb/common/ usb_dc.h)。如下表4所示。
Table 4. CherryUSB device controller API
表4. CherryUSB从机控制器API
函数 |
功能 |
usb_dc_init |
初始化从机控制器 |
usb_dc_deinit |
反初始化从机控制器 |
usbd_ep_open |
配置端点相关属性 |
usbd_ep_close |
关闭端点 |
usbd_ep_start_write |
端点启动端点发送 |
usbd_ep_start_read |
端点启动端点接收 |
usbd_event_ep0_setup_complete_handler |
setup包完成中断处理 |
usbd_event_ep_in_complete_handler |
端点发送完成中断处理 |
usbd_event_ep_out_complete_handler |
端点接收完成中断处理 |
API和寄存器中大量提到了USB的一个名词:端点(endpoint) [2]。端点是USB设备内部的基本通信结构单元,本质上是一块缓冲区存储器,用作数据传输的源或目的地,并通过管道(pipe)进行传输。端点具有方向性,包括IN和OUT方向;端点支持一种传输方式,包括控制传输、批量传输、中断传输、同步传输[3];端点传输一个包时有最大数据包长度限制,称为MPS。这里,我们采用类比法,将端点比作DMA通道,端点的发送与接收看作是外设发送和接收,端点的传输方式看作是UART\SPI\I2C传输等等,端点的完成中断看作是DMA通道完成中断。当我们采用类比法以后,可以更快地理解端点的含义和作用,更快的理解从机控制器IP和驱动的设计思想。
2.3. 从机协议栈设计
在USB中,另一个重要的概念叫做setup包[4],它是主机和从机枚举所定义的一个格式,如图1所示。主机和从机通过端点0进行控制传输,并发送setup包给到从机,从机会执行到usbd_event_ep0_setup_complete_handler函数,该函数则是用于处理获取的setup包,并按照不同的bRequest执行不同的处理,最终完成USB的枚举以及接口驱动的加载工作。
Figure 1. Format of setup packet
图1. setup包格式
下图2完整描述了CherryUSB从机协议栈的执行过程和调用关系,和从机控制器IP驱动设计紧密结合。
Figure 2. CherryUSB device stack framework
图2. CherryUSB 设备协议栈框图
3. CherryUSB主机协议栈概述
CherryUSB主机协议栈主要包括主机控制器IP驱动,USB设备枚举和驱动加载。相比于从机的一对一的通信方式,主机更具复杂性,由于是一对多的方式,同时需要支持多个USB设备,因此,在CherryUSB中,采用了RTOS的接管,如果使用bare mental的方式管理,增加例如死等,状态机的代码,会让用户在阅读和使用上变得非常麻烦,并且也让代码的健壮性变得不可控。CherryUSB主机协议栈支持多种USB设备类驱动,包括CDC、HID、MSC、VIDEO、AUDIO、RNDIS、HUB等,可以支持1托多个USB设备,每个设备还可以是复合设备(支持多种USB类接口的设备)。本章同从机一样,从主机控制器IP设计入手进行分析。
3.1. 主机控制器IP设计
USB主机控制器IP是一个为嵌入式系统提供实现USB主机功能的硬件模块,主要负责总线仲裁,数据传输调度,USB协议的处理,并向上层软件提供标准寄存器定义。而主机控制器IP驱动则是根据这些寄存器定义,进行配置并能够与USB设备进行通信。CherryUSB支持多个主机控制器IP,商业性的IP包括DWC2、MUSB、OHCI、EHCI、XHCI等。其中MUSB和DWC2部分模式采用寄存器配置,OHCI/EHCI/XHCI和部分DWC2部分模式采用描述符链表配置,前者更多是把功能交给软件做,而后者则是将复杂功能交给硬件做,降低了软件的负载率,但是同时也增加了软件的编写难度。OHCI/EHCI/XHCI是intel制定的一套主机控制器标准,目前也是市面上最流行也是最通用的,也是最推荐使用的。由于主机控制器设计较为复杂,本章只简单介绍中断设计相关。如下表5所示。
Table 5. Interrupt of USB host controller
表5. USB主机控制器中断种类
IP |
中断标志 |
功能 |
DWC2 |
USB_OTG_GINTSTS_HCINT |
中断完成/错误中断 |
MUSB |
MUSB_TXIS/MUSB_RXIS |
所有pipe中断完成/错误中断 |
OHCI |
XHCI_USBSTS |
所有pipe中断完成/错误中断 |
EHCI |
EHCI_USBSTS_INT/EHCI_USBSTS_ERR |
所有pipe中断完成/错误中断 |
XHCI |
OHCI_INT_WDH |
所有pipe中断完成/错误中断 |
从上述寄存器功能可以看出,无论主机控制器如何设计,都是具备相似点的,无非只是软件编写方式的不同。在这里pipe和从机的端点是对应的,一个端点对应一个pipe,自然一个pipe对应一个端点,但是由于主机是一对多的方式,因此存在多个相同的端点,但是属于不同的USB设备,因此,在主机中,统称为pipe。借助中断,我们可以定义出主机控制器IP的驱动。
3.2. 主机控制器驱动设计
主机控制器IP驱动,CherryUSB参考了linux的urb设计[5],也是采用urb (usb request block)的方式,urb内容如下:
struct usbh_urb {
usb_slist_t list;
void *hcpriv;
struct usbh_hubport *hport;
struct usb_endpoint_descriptor *ep;
uint8_t data_toggle;
uint32_t interval;
struct usb_setup_packet *setup;
uint8_t *transfer_buffer;
uint32_t transfer_buffer_length;
int transfer_flags;
uint32_t actual_length;
uint32_t timeout;
int errorcode;
uint32_t num_of_iso_packets;
uint32_t start_frame;
usbh_complete_callback_t complete;
void *arg;
#if defined(__ICCARM__) || defined(__ICCRISCV__) || defined(__ICCRX__)
struct usbh_iso_frame_packet *iso_packet;
#else
struct usbh_iso_frame_packet iso_packet[0];
#endif
};
hport对应一个USB设备,当前urb将会在哪个USB设备上使用
ep对应一个USB设备上的一个端点,当前urb会和该USB设备的哪个端点通信
setup表示控制传输对应的setup包
transfer_buffer则表示传输的地址
transfer_buffer_length表示传输的长度
actual_length表示最终发送或者接收的实际长度
虽然urb看上去内容很多,但是实际上,和从机协议栈一样,最终都是端点通信,并且都会触发端点的完成中断,只不过在主机叫做pipe完成中断,同样也是可以类比成UART + DMA的形式,将pipe比作DMA通道,传输比作一个外设,比如UART/SPI/I2C,完成中断比作是DMA通道完成中断。
3.3. 主机协议栈设计
主机协议栈主要是对接入的USB设备进行枚举,并且解析描述符,再根据描述符找到对应的USB类驱动,并初始化类驱动,最后提供用户可用的API供调用,对拔出的设备,进行资源的释放。关于主机协议栈整体调用,如图3所示。
在主机中,有一个叫做hub的概念,USB hub (USB集线器)是一种允许多个USB设备连接到一个单个USB端口的设备。它基本上是USB接口的分线器或扩展器。每个hub上的USB接口,称为hubport,每个hubport可以接入一个USB设备,因此 USB 设备的枚举就是通过hub上的hubport进行。而hub又分roothub和external hub,roothub代表主机控制器本身,external hub自身是一个USB设备,具备连接多个USB设备的功能,如图4所示。因此在CherryUSB中会创建一个hub守护线程,作用就是探测hub上所有USB接口是否有插入和拔出事件,如果是插入事件,则进行枚举和驱动加载,如果是拔出事件则进行资源释放。如果接入的是hub设备,则通过hub通信继续检测插拔事件,最终都会通过统一的一个hub守护线程去操作hub上的所有USB端口。
4. 基于CherryUSB的应用开发
本章中,我们将基于RT-Thread ART-PI2开源硬件,进行CherryUSB的主机和从机移植和应用开发。
4.1. 使用从机枚举虚拟串口
首先根据CherryUSB官方文档(https://cherryusb.cherry-embedded.org/quick_start/transplant#usb-device)完成从机的基础移植,主要包含非USBIP相关的内容,包括USB引脚,USB时钟,USB中断等,这些不属于USBIP驱动中的内容。由于我们使用RT-Thread ART-PI2,可以直接使用ART-PI2官方仓库(https://github.com/RT-Thread-Studio/sdk-bsp-stm32h7r-realthread-artpi2),并选择art_pi2_cherryusb_usbdev_cdc_acm工程编译即可。具体则是在env环境中使用menuconfig勾选CherryUSB模块,选择USBIP和cdc acm类和demo并保存,如图5所示。
Figure 3. CherryUSB host stack framework
图3. CherryUSB主机协议栈框图
Figure 4. USB hub topological structure
图4. USB hub拓扑结构
Figure 5. CherryUSB device menuconfig
图5. CherryUSB从机menuconfig配置
然后执行scons --target=mdk5,并在main函数中调用cdc_acm_init初始化cdc acm设备,然后编译构建即可。最终将会在电脑上显示一个USB COM口,并可以进行数据的通信。如图6所示。
Figure 6. CDC ACM read write test
图6. CDC ACM读写测试
4.2. 使用主机枚举U盘
主机的移植可以参考CherryUSB官方文档
(https://cherryusb.cherry-embedded.org/quick_start/transplant#usb-host),同样在ART-PI2官方仓库中也包含主机的例程,选择art_pi2_cherryusb_usbhost工程[6],并使用menuconfig勾选USBIP和MSC类设备并保存,如图7所示。CherryUSB将会对接到RT-Thread的DFS (虚拟文件系统)组件中。
Figure 7. CherryUSB host menuconfig
图7. CherryUSB主机menuconfig配置
然后执行scons--target=mdk5,并在main函数中使用usbh_initialize初始化主机协议栈初始化,然后编译构建即可。
当我们插入一个U盘时,协议栈会识别并进行枚举,加载MSC驱动,最终注册到DFS组件中,可以使用‘ls’,‘cat’,‘echo’相关API进行访问。如图8所示。
Figure 8. CherryUSB host flash drive test
图8. CherryUSB主机接入U盘测试
5. 结语
CherryUSB的设计是为了让用户更好地理解和使用USB,将复杂的功能采用简易代码和类比等形式简化,并实现了众多USB类设备,统一了USBIP驱动,降低了用户移植到新嵌入式系统中的难度,也方便了嵌入式开发者进行应用开发或者是IP验证。CherryUSB希望通过这些来吸引更多的开发者和企业用户。希望通过本文,读者对CherryUSB的原理有个初步的了解,不必拘泥于复杂的USB概念,而是从USBIP设计入手去发现规律,总结规律,从而形成一个大体的框架,最终对USB的整体会有一个很深入的理解。
CherryUSB近年来得到了很多开发者和企业的支持,在很多产品当中都有CherryUSB的身影,项目也是趋于高度稳定,期待CherryUSB在未来能够支持更多的硬件支持,支持更多的开源项目,成为一个主流标准的USB协议栈[7]。