1.概述
UDC驱动的接口都定义在drivers/usb/gadget/udc/core.c文件中。USB Function驱动通过调用这些接口匹配及访问USB设备控制器,而底层USB控制器驱动要实现这些接口定义的功能。下面分析一下主要的UDC驱动接口调用流程。
2. 接口分析
2.1.驱动绑定
Composite层通过调用UDC core层的usb_udc_attach_driver和usb_gadget_probe_driver接口将Function驱动和UDC驱动绑定。前者通过UDC设备的名称匹配,通常是configfs配置的USB Function驱动使用,后者直接匹配udc_list链表中的第一个UDC驱动,通常是legacy类型的USB Function驱动使用,如g_audio驱动。使用usb_gadget_unregister_driver函数解除Function驱动和UDC驱动的绑定。具体的绑定过程,在分析具体的Function驱动时说明。
[drivers/usb/gadget/udc/core.c]
int usb_udc_attach_driver(const char *name, struct usb_gadget_driver *driver);
int usb_gadget_probe_driver(struct usb_gadget_driver *driver);
int usb_gadget_unregister_driver(struct usb_gadget_driver *driver);
底层的USB设备控制器驱动(dwc3 gadget)使用usb_add_gadget_udc函数将自身加入到Core UDC Framework中,使用usb_del_gadget_udc从Core UDC Framework删除。加入过程在UDC驱动初始化的时候已经分析过了,这里不再赘述。
[include/linux/usb/gadget.h]
int usb_add_gadget_udc(struct device *parent, struct usb_gadget *gadget);
void usb_del_gadget_udc(struct usb_gadget *gadget);
2.2.开启和停止UDC
下面两个函数用来开启和停止UDC,Function驱动和UDC驱动绑定的时候通过调用usb_udc_attach_driver或usb_gadget_probe_driver间接调用usb_gadget_udc_start开启UDC,Function驱动和UDC驱动解除绑定的时候通过调用usb_gadget_unregister_driver间接调用usb_gadget_udc_stop停止UDC。
[drivers/usb/gadget/udc/core.c]
static inline int usb_gadget_udc_start(struct usb_udc *udc);
static inline void usb_gadget_udc_stop(struct usb_udc *udc);
[include/linux/usb/gadget.h]
int usb_udc_attach_driver(const char *name, struct usb_gadget_driver *driver);
int usb_gadget_probe_driver(struct usb_gadget_driver *driver);
int usb_gadget_unregister_driver(struct usb_gadget_driver *driver);
2.2.1.开启UDC
USB Function驱动和UDC绑定成功后,通过调用usb_gadget_udc_start接口开启USB设备控制器,接收主机发送的请求,其内部调用的dwc3设备控制器驱动的dwc3_gadget_start函数。主要的工作是设置相关参数并使能USB设备控制器,具体如下:
- 注册中断上半部分处理函数dwc3_interrupt和中断下半部分处理函数dwc3_thread_interrupt(运行在线程上下文中).
- 将composite层提供的usb_gadget_driver结构体地址保存到dwc3结构体中。legacy Function驱动使用的usb_gadget_driver结构体是composite_driver_template,configfs Function驱动使用的usb_gadget_driver结构体是configfs_driver_template。
- 设置控制器速度(默认超高速)、使能接收数据包数量统计、设置端点0能处理的最大包长为512字节(SuperSpeed Default)。
- 使能ep0_out和ep0_in硬件端点,设备枚举阶段需要使用端点0,因此需要先使能。使能端点0的步骤如下:
-
向控制器发送开始新配置命令DWC3_DEPCMD_DEPSTARTCFG(初始化的时候需要发送此命令),所有端点设置端点传输资源配置命令DWC3_DEPCMD_SETTRANSFRESOURCE。向端点发送命令需要通过USB3_DEPnCMD、USB3_DEPnCMDPAR0、USB3_DEPnCMDPAR1、USB3_DEPnCMDPAR2寄存器,第一个寄存器保存命令,后面三个寄存器保存命令的参数。向端点发送命令都是通过dwc3_send_gadget_ep_cmd函数完成,后面会经常看到。
-
设置端点0的一些参数。如端点类型(控制端点)、端点的MaxPacketSize(512)、设置USB3.0的Burst size(端点的burst为1)、使能设备端点完成传输事件DWC3_DEPCFG_XFER_COMPLETE_EN和传输未准备好事件DWC3_DEPCFG_XFER_NOT_READY_EN。
-
设置端点编号,直接使用dep->number。
-
设置端点对应的FIFO编号为dep->number >> 1,控制端点的输入和输出端点其FIFO编号相同,其他输出端点FIFO编号为0。
-
设置端点的bInterval。
-
上述这些参数通过向控制器发送DWC3_DEPCMD_SETEPCONFIG命令设置。
-
保存端点描述符、USB3.0伴侣描述符、保存端点类型、设置端点使能标志、向寄存器中写入端点编号以使能端点,非控制端点还要初始化TRB。
-
- 开始ep0 out传输,用于响应主机发送的请求。首先准备一个TRB,接着将TRB中的缓冲区地址设置为控制请求DMA缓冲区ctrl_req_addr地址,最后向控制器发送开始传输命令DWC3_DEPCMD_STARTTRANSFER,参数为TRB自身的DMA地址。
- 写DWC3_DEVTEN寄存器,使能设备事件(中断),这样控制器就可响应主机发送的请求了,可以正常被枚举了。
2.2.2.停止UDC
USB Function驱动和UDC解除绑定成功后会调用usb_gadget_udc_stop函数停止USB设备控制器。主要的工作是清除USB控制器上的USB请求,然后关闭端点,具体如下:
- 将DWC3_DEVTEN寄存器清空,屏蔽所有设备事件(中断)。
- 关闭ep0_out和ep0_in硬件端点。
- 向控制器发送停止传输命令DWC3_DEPCMD_ENDTRANSFER。
- 移除端点上的所有USB请求,并调用其回调函数。
- 若端点处于STALL状态,则需要先清除该状态
- 关闭硬件端点
- 清空端点描述符。
- 清空USB3.0端点伴侣描述符。
- 清空端点类型。
- 清空端点标志。
- 注销注册的中断处理程序。
2.2.3.端点0描述符
端点0的端点描述符定义在dwc3的设备控制器驱动中,Function驱动无需定义,具体如下。
[drivers/usb/dwc3/gadget.c]
/* 端点0的描述符在dwc3 gadget驱动中定义 */
static struct usb_endpoint_descriptor dwc3_gadget_ep0_desc = {
.bLength = USB_DT_ENDPOINT_SIZE, // 端点描述符长度
.bDescriptorType = USB_DT_ENDPOINT, // 描述符类型为端点描述符
.bmAttributes = USB_ENDPOINT_XFER_CONTROL, // 端点0使用控制传输
};
2.2.4.命令和参数
从上面的分析中可以看出,软件通过向USB设备控制器端点USB3_DEPnCMD寄存器写入命令来驱动USB端点工作,命令可以携带参数,参数写到USB3_DEPnCMDPAR1、USB3_DEPnCMDPAR2、USB3_DEPnCMDPAR3寄存器。命令的类型由USB3_DEPnCMD寄存器的bit[0:3]位决定,bit[10]控制命令是否执行,软件设置为1则端点开始执行命令,执行完毕,端点自动清零,软件可以根据此位判断端点是否执行完命令。USB3_DEPnCMD寄存器的低11bit意义如下图所示。在执行命令之前,需要提前把命令参数写到参数寄存器中,若不需要参数,则写入0即可。
下面是参数寄存器USB3_DEPnCMDPAR0、USB3_DEPnCMDPAR1、USB3_DEPnCMDPAR2的具体意义。
- USB3_DEPnCMDPAR0
位域 | 说明 |
---|---|
31:30 | 配置行为。 0-初始化端点状态,用于第一次配置端点 1-恢复端点状态,用于休眠唤醒后恢复端点状态,需要恢复的状态保存到DEPCMDPAR2寄存器中 2-修改端点状态,用于修改已经配置好的端点配置,例如修改DEPEVTEN使能位、interrupt number、MaxPacketSize。 |
29:26 | Reserved |
25:22 | 设置突发传输数值Burst Size,即一个微帧内传输多少包数据,只有USB3.0及以上支持此特性。 0:Burst length = 1 1:Burst length = 2… 15:Burst length = 16 |
21:17 | 分配给此端点的FIFO编号。对于控制端点,输出端点和输入端点的值应该相等,即ep0_in和ep0_out的FIFO编号相等。对于输出端点此值为0。尽管在DRD模式中TxFIFOs数量超过16个,但在设备模式中必须使用较低的16个。 |
16:14 | Reserved |
13:3 | 输入输出端点支持的最大包长。USB3.0支持的最大包长为1024字节。 |
2:1 | 端点类型 2’b00: Control 2’b01: Isochronous 2’b10: Bulk 2’b11: Interrupt |
- USB3_DEPnCMDPAR1
位域 | 说明 |
---|---|
31 | FIFO-based |
30 | Reserved |
29:25 | 设置USB端点编号。 bit[29:26]: Endpoint number bit[25]: Endpoint direction,0-OUT,1-IN 物理端点0必须分配给控制器输出端点 Stream的能力,指出此端点具有流传输功能(MaxStreams != 0) |
24 | 物理端点1必须分配给控制器输入端点 |
23:16 | 设置bInterval的值,端点描述符中设置了该值(写入寄存器的值为真实bInterval-1)。即轮训数据传送端点的时间间隔,对于批量传送和控制传送的端点忽略,对于同步传送的端点,必须为1,对于中断传送的端点,范围为1-255。dwc3控制器支持的有效值范围为0-13 |
15 | 如果bulk端点使用了 External Buffer Control (EBC)模式 |
14 | 如果bit15设置了该位才能设置,否则不要设置此位。 1:控制器不会更新TRB的HWO位域 0:控制器会更新TRB的HWO位域 |
13:8 | 使能设备端点的特殊事件DEPEVTEN,写入0关闭。 Bit 13: Stream Event Enable (StreamEvtEn) Bit 12: Reserved Bit 11: Reserved Bit 10: XferNotReady Enable (XferNRdyEn) Bit 9: XferInProgress Enable (XferInProgEn) Bit 8: XferComplete Enable (XferCmplEn) |
7:5 | Reserved |
4:0 | 输入和输出端点的中断编号。指示此端点产生的与端点相关的中断的中断/事件缓冲区号。如果没有使能multiple Interrupter配置,则此位域必须设置为0 |
- USB3_DEPnCMDPAR2
位域 | 说明 |
---|---|
31:0 | 当USB3_DEPnCMDPAR0寄存器的bit[31:30]为1时,该寄存器中保存的是需要恢复端点状态的值,否则为Reserved |
2.3.打开和禁止端点
2.3.1.打开端点
USB Function驱动通过调用usb_ep_enable打开除端点0以外的端点。端点0打开逻辑由dwc3驱动自身处理,不需要USB Function驱动直接参与。
[include/linux/usb/gadget.h]
int usb_ep_enable(struct usb_ep *ep);
usb_ep_enable函数的工作流程如下图所示,和usb_gadget_udc_start函数一样,最终通过调用__dwc3_gadget_ep_enable实现。__dwc3_gadget_ep_enable函数的执行流程见2.2.1.节,下面主要说不同的地方:
- 设置端点参数,见2.2.2.节,对于非端点0,若是bulk传输,还会使能stream事件,并且还会使能DWC3_DEPCFG_XFER_IN_PROGRESS_EN事件
- 保存端点数据、设置端点使能标志、使能端点
- 非控制端点需要设置TRB相关内容。首先将TRB ring的索引trb_dequeue和trb_enqueue清零,接着将trb_pool全部清零,最后设置link TRB,即trb_pool最后一个TRB中保存第一个TRB的地址,并且设置DWC3_TRBCTL_LINK_TRB和DWC3_TRB_CTRL_HWO标志。
- 设置端点使能状态为true。
2.3.2.关闭端点
USB Function驱动通过调用usb_ep_disable
关闭除端点0以外的端点。端点0关闭逻辑由dwc3驱动自身处理,不需要USB Function驱动直接参与。
[include/linux/usb/gadget.h]
int usb_ep_disable(struct usb_ep *ep);
usb_ep_disable函数的工作流程如下图所示,和usb_gadget_udc_stop函数一样,最终通过调用__dwc3_gadget_ep_disable实现。__dwc3_gadget_ep_disable函数的执行流程见2.2.1.节,最后设置端点状态关闭。
2.4.匹配端点
USB Function驱动可以通过gadget_find_ep_by_name和usb_gadget_ep_match_desc函数来匹配要使用的端点。前者实现较为简单,通过端点的名称进行匹配,使用的不多。后者通过端点描述符和USB3.0端点伴侣描述符进行匹配,使用较多,返回值为1时表示匹配成功,返回值为0表示匹配失败,其匹配的流程如下:
-
若ep->claimed字段为true说明该端点已经被使用,则直接返回0。
-
检查端点描述符和要匹配的端点输入输出方向是否相同,不相同则返回0。
-
检查描述符的maxpacket_limit是否大于真实端点,若大于则返回0。
-
检查各种传输类型的参数是否设置正确。
- 对于控制传输直接返回0,控制传输使用端点0,无法被匹配
- 对于等时传输,若使用全速模式则最大包长为1023字节。
- 对于批量传输,若使用超高速模式,则描述符请求的流数量要大于端点支持的最大流数量。
- 对于中断传输,若使用全速模式则最大包长为64字节,高速和超高速最大包长为1024字节。
-
若上述条件都满足,则匹配成功,返回1。
[include/linux/usb/gadget.h]
// name为要匹配端点的名称
struct usb_ep *gadget_find_ep_by_name(
struct usb_gadget *g, const char *name);
// ep为要匹配端点的数据结构,desc匹配端点的端点描述符
// ep_comp匹配端点的USB3.0端点伴侣描述符
int usb_gadget_ep_match_desc(struct usb_gadget *gadget,
struct usb_ep *ep, struct usb_endpoint_descriptor *desc,
struct usb_ss_ep_comp_descriptor *ep_comp);
2.5.分配和释放USB请求
USB Function驱动通过调用usb_ep_alloc_request和usb_ep_free_request分配和释放USB I/O请求。
[include/linux/usb/gadget.h]
struct usb_request *usb_ep_alloc_request(struct usb_ep *ep, gfp_t gfp_flags);
void usb_ep_free_request(struct usb_ep *ep, struct usb_request *req);
2.5.1.分配USB请求
dwc3所有端点都是通过dwc3_gadget_ep_alloc_request分配USB请求,实质上分配的是dwc3_request数据结构,内部包含了通用的USB请求数据结构usb_request。
2.5.2.释放USB请求
dwc3所有端点都是通过dwc3_gadget_ep_free_request释放USB请求。
2.6.提交和取消USB请求
USB Function驱动通过调用usb_ep_queue和usb_ep_dequeue将USB I/O请求提交到发送队列和从发送队列取消。
[include/linux/usb/gadget.h]
int usb_ep_queue(struct usb_ep *ep,
struct usb_request *req, gfp_t gfp_flags);
int usb_ep_dequeue(struct usb_ep *ep, struct usb_request *req);
2.6.1.提交USB请求
向端点0和非端点0提交USB请求,usb_ep_queue内部调用的函数不同。端点0调用dwc3_gadget_ep0_queue函数,非端点0调用dwc3_gadget_ep_queue函数。
2.6.1.1.向端点0提交USB请求
如下图所示,向端点0提交USB请求时,会调用dwc3 gadget驱动的dwc3_gadget_ep0_queue函数,其主要的执行流程为:
- 端点0共享一个TRB,若pending_list链表不为空,说明还有未完成的request,则直接退出。端点0的request传输完成后,在其回调函数中会将request从pending_list移除。
- 设置request的actual、status字段,保存端点0的编号,将request放入pending_list链表。
- 调用usb_gadget_map_request映射request的缓冲区。
- 准备一个TRB,将其和request绑定,固定使用dwc->ep0_trb[0]。初始化的时候端点0分配了2个TRB。
- 调用dwc3_ep0_start_trans开始传输request,该函数前面已经介绍过了。
2.6.1.2.向非端点0提交USB请求
usb_ep_queue向非端点0提交USB请求的过程如下图所示,最终通过__dwc3_gadget_ep_queue函数提交。下面分析一下向非端点0提交USB请求的主要工作内容:
- 设置USB请求的actual、status、direction、epnum和length等字段,对于输出端点,length必须是MaxPacketSize的整数倍。
- 对USB请求的缓冲区进行流式DMA映射。USB请求缓冲区是Function驱动使用kmalloc等函数分配,DMA不能直接使用,需要进行DMA映射,具体的映射过程在2.7小结介绍。
- 将提交的USB请求先放到pending_list链表。
- 调用__dwc3_gadget_kick_transfer函数驱动USB设备控制器发送USB请求。这里有几个特殊情况,图里面没有画出,下面简要说明一下。
- bulk和control传输,则直接调用__dwc3_gadget_kick_transfer发送USB请求。
- int和isoc传输,需要处理XferNotReady、XferInProgress、Stream Capable Bulk Endpoints等情况。
- 调用dwc3_prepare_trbs函数遍历pending_list将TRB和request绑定,执行完后pending_list链表上的request都会被放到started_list链表上。该函数会根据DMA缓冲区的使用形式做不同的处理,若DMA支持Scatter-gather,则调用dwc3_prepare_one_trb_sg函数,否则调用dwc3_prepare_one_trb_linear函数,最终都是通过dwc3_prepare_one_trb将TRB和request绑定。
- 从started_list链表中获取一个USB请求。若端点空闲,将其绑定的TRB DMA地址设置到param0和param1寄存器中,命令设置为DWC3_DEPCMD_STARTTRANSFER,开始传输。若端点忙碌,则将命令设置为DWC3_DEPCMD_UPDATETRANSFER,只更新传输,则不发送本次的USB请求。端点是否忙碌通过DWC3_EP_BUSY标志判断。
- 若端点空闲,则获取端点资源索引,用于辨别是那一次传输。
上面说过了,TRB和request最终通过dwc3_prepare_one_trb实现,该函数的执行过程如下图所示,主要的工作内容为:
- 从trb_pool获取一个空闲的TRB,trb_pool的索引为dep->trb_enqueu。
- 设置request状态为开始传输,将request放到started_list链表上。
- request保存TRB的虚拟地址、保存TRB的DMA地址、保存绑定TRB的索引、dep->trb_enqueu增大。
- TRB保存request中缓冲区的长度、保存request中缓冲区的DMA地址。
- 设置TRB的控制位。
2.6.2.取消USB请求
端点0和非端点0取消USB请求的过程是一样的,最终都是通过调用dwc3 gadget驱动的dwc3_gadget_ep_dequeue实现,其主要的工作内容如下:
- 遍历pending_list链表,查找要取消的request是否在该链表中,若在,则调用dwc3_gadget_giveback函数回调request的回调函数,将其从pending_list链表中删除。
- 若要取消的request不在pending_list链表,则遍历started_list链表查找,若找不到,直接返回-EINVAL错误,反之调用dwc3_stop_active_transfer函数向控制发送DWC3_DEPCMD_ENDTRANSFER命令停止传输,最后再调用dwc3_gadget_giveback函数回调request的回调函数,将其从pending_list链表中删除。
2.7.map和unmap USB请求
dwc3驱动在发送USB请求的时候需要调用usb_gadget_map_request映射缓冲区,获取缓冲区的DMA地址。USB请求发送完或dequeue USB请求的时候,需要调用usb_gadget_unmap_request将缓冲区进行反向映射。
[include/linux/usb/gadget.h]
int usb_gadget_map_request(struct usb_gadget *gadget,
struct usb_request *req, int is_in);
void usb_gadget_unmap_request(struct usb_gadget *gadget,
struct usb_request *req, int is_in);
2.7.1.map USB请求
usb_gadget_map_request的执行流程如下图所示,dma_map_sg适用于支持SG的DMA,dma_map_single适用于不支持SG的DMA,底层映射的机制是相同的,这里只说明dma_map_single。
- 获取DMA的操作函数集合dma_map_ops,RK3399上使用swiotlb_dma_ops。在drivers/iommu/rockchip-iommu.c文件中调用arch_setup_dma_ops设置。
- 验证DMA的方向,是DMA_BIDIRECTIONAL(数据可双向移动)、DMA_TO_DEVICE(数据传输到设备中)、DMA_FROM_DEVICE(数据从设备中传出)其中之一。
- 验证DMA的方向,是DMA_BIDIRECTIONAL(数据可双向移动)、DMA_TO_DEVICE(数据传输到设备中)、DMA_FROM_DEVICE(数据从设备中传出)其中之一。
2.7.2.unmap USB请求
usb_gadget_unmap_request的执行流程如下图所示,dma_unmap_sg适用于支持SG的DMA,dma_unmap_single适用于不支持SG的DMA,底层映射的机制是相同的,这里只说明dma_unmap_single。
- 获取DMA的操作函数集合dma_map_ops,rk3399上使用swiotlb_dma_ops。在drivers/iommu/rockchip-iommu.c文件中调用arch_setup_dma_ops设置。
- 验证DMA的方向,是DMA_BIDIRECTIONAL(数据可双向移动)、DMA_TO_DEVICE(数据传输到设备中)、DMA_FROM_DEVICE(数据从设备中传出)其中之一。
- 若不是coherent缓冲区,且是DMA_FROM_DEVICE,则需要invalidate cache,接着调用swiotlb_unmap_page解除缓冲区映射。
2.7.3.swiotlb_dma_ops
swiotlb_dma_ops是arm64平台上使用软件实现的smmu/iommu,定义如下。
[arch/arm64/mm/dma-mapping.c]
void arch_setup_dma_ops(struct device *dev, u64 dma_base, u64 size,
struct iommu_ops *iommu, bool coherent);
static struct dma_map_ops swiotlb_dma_ops = {
.alloc = __dma_alloc,
.free = __dma_free,
.mmap = __swiotlb_mmap,
.get_sgtable = __swiotlb_get_sgtable,
.map_page = __swiotlb_map_page,
.unmap_page = __swiotlb_unmap_page,
.map_sg = __swiotlb_map_sg_attrs,
.unmap_sg = __swiotlb_unmap_sg_attrs,
.sync_single_for_cpu = __swiotlb_sync_single_for_cpu,
.sync_single_for_device = __swiotlb_sync_single_for_device,
.sync_sg_for_cpu = __swiotlb_sync_sg_for_cpu,
.sync_sg_for_device = __swiotlb_sync_sg_for_device,
.dma_supported = swiotlb_dma_supported,
.mapping_error = swiotlb_dma_mapping_error,
};
2.8.USB请求回调函数
当USB请求传输完成后,可通过usb_gadget_giveback_request函数回调USB请求的回调函数。USB请求的回调函数一般由Function驱动设置。
[include/linux/usb/gadget.h]
void usb_gadget_giveback_request(struct usb_ep *ep, struct usb_request *req)
{
if (likely(req->status == 0))
usb_led_activity(USB_LED_EVENT_GADGET);
trace_usb_gadget_giveback_request(ep, req, 0);
req->complete(ep, req);
}
2.9.端点状态
USB协议定义了一组联络代码,表明传输的状态。联络代码会在数据信息包或联络信息包中传输。联络代码有ACK、NAK、STALL、NYET和ERR。具体如下:
- ACK(acknowledge)
表明主机或设备已经正确的接收到了数据。如果令牌和数据信息包被正确的接收了,设备必须在Setup事物的联络信息包中返回ACK。设备也可能会在OUT事物的联络信息包中返回ACK。与设备一样,如果令牌和数据信息包被正确的接收了,主机也会在IN事务的联络信息包中返回ACK。 - NAK(negative acknowledge)
表明设备繁忙或没有数据要返回。若主机在设备繁忙的时候请求数据,则设备端点会在联络信息包中返回NAK。若主机向设备请求数据,而此时设备没有数据要发送,则设备端点会在数据信息包中返回NAK。主机从不发送NAK,等时传输不使用NAK。 - STALL(失败)
表示不被支持的控制请求、控制请求失败或端点失败。
设备在收到不支持的控制请求时,将在数据或状态阶段返回STALL。若设备虽支持请求,但由于某种原因不能执行所请求的动作,设备也会返回STALL。为了解决STALL,主机会发送另一个设置信息包,开始新的控制传输。USB 2.0规范称这种失败类型为“协议失败”(protocol stall)。
另一种失败的方式是端点的Halt(暂停)特性被设置时的响应,这表示端点根本不能发送和接数据。USB规范称这类失败为”功能失败“(function stall)。
批量和中断端点必须支持功能失败。USB 2.0控制端点也可能会支持功能失败,但很少这么做。功能失败的控制端点必须正常响应其他请求,以监视和控制失败状态。端点”能够响应请求“是指能够通信且因此不能被设为设备。等时传输不使用STALL。超高速控制端点不使用功能失败。主机收到功能失败的时,将放弃等待设备的请求,并且不恢复通信,直到主机发送控制请求,成功的清除了设备上的Halt特性。主机从不发送STALL。 - NYET(not yet)
只有高速设备才会发送NYET。高速批量和控制传输都支持一种协议,它使主机在有设备可接收数据的情况下,能够在发送数据之前找到此设备。在低速或全速状态下,主机若想在控制、批量或中断传翰中发送数据,它将首先发送令牌和数据信息包,并接收设备在事务的联络包中所作出的回复。若设备还没准备好接收数据,会返回NAK,主机会在稍候重新尝试。如果数据信息包很大且设备总是没有准备好,不断重新尝试可能会浪费很多总线时间。这种情况下,使用带有多个数据信息包的高速批量和控制传输将是更好的途径。在接收到数据信息包后,设备端点可返回NYET联络信息,它表明端点接收此数据但没有准备好接收另一数据信息包。当主机认为设备可能准备好的时候,主机可发送PING令牌信息包。端点则会返回ACK来表明设备已可接收下个数据信息包,或者如果设备还是没有准备好,将返回NAK或STALL。发送PING的方法只在确定了设备没有准备好,必须在稍后重传的情况下才会比发送整个数据信息包的方式更为有效。主机是否使用PING是可选的。 USB 2.0集线器可能会在完成分割事务中返回NYET。主机以及低速或全速设备从不发送NYET。 - ERR
ERR联络信息只在完成分割事务中,被高速集线器使用。ERR表明设备没有返回事务所预期的表明集线器与主机完成通信的联络信息号。 - 无响应
另一种状态指示类型发生在主机或设备期望接收联络但却没有收到任何信息的情况下。接收端的错误检查发现错误时,就会产生这种响应缺失。若没接收到任何响应,发送端需要重新尝试,在多个尝试失败后,发送端可采取其他动作。
UDC驱动提供了设置端点状态的函数,如下所示。usb_ep_set_halt可以将端点特性设置为Halt,此时端点将处于STALL状态,不发送数据也不接收数据,除非主机发送CLEAR_FEATURE请求。usb_ep_clear_halt函数用于清除端点的Halt特性,此时端点将从STALL状态中恢复。usb_ep_set_wedge将端点特性设置为Halt并且会忽略CLEAR_FEATURE请求,只有Function驱动才可以清除Halt特性。
[include/linux/usb/gadget.h]
int usb_ep_set_halt(struct usb_ep *ep);
int usb_ep_clear_halt(struct usb_ep *ep);
int usb_ep_set_wedge(struct usb_ep *ep);
2.9.1.端点0 halt特性
端点0设置和清除halt特性的执行流程如下图所示。usb_ep_set_halt和usb_ep_clear_halt函数最终都会调到dwc3 gadget驱动的dwc3_gadget_ep0_set_halt函数,当第二个参数value=1时表示usb_ep_set_halt,value=0时表示usb_ep_clear_halt。虽然上层区t分了se halt和clear halt,但对于端点0,底层都是进行stall and restart操作,流程如下:
- 将ep0_in端点标志设置为DWC3_EP_ENABLED。
- 将ep0_out端点的状态设置为stall。
- 清除ep0_out端点pending_list上所有USB请求。
- 设置ep0_ou端点控制传输的阶段为EP0_SETUP_PHASE,准备被主机枚举。
- 设置ep0_out开始传输,准备接收主机发送的USB控制请求。
2.9.2.非端点0 halt特性
非端点0设置和清除halt特性的执行流程如下图所示。usb_ep_set_halt和usb_ep_clear_halt函数最终都会调到dwc3 gadget驱动的dwc3_gadget_ep_set_halt函数,当第二个参数value=1时表示usb_ep_set_halt,value=0时表示usb_ep_clear_halt。蓝色表示usb_ep_set_halt函数执行路径,橙黄色表示usb_ep_clear_halt函数执行路径。绿色两者都会执行。需要注意的是isoc端点无法设置和清除halt特性。
usb_ep_set_halt执行流程如下:
- 非protocol STALL的IN端点并且最近使用的TRB未传输完成或OUT端点有pending的request,说明该端点还有未处理完的request,不能halt,返回-EAGAIN错误。
- 若端点上没有未处理request,则将该端点设置为stall状态。
- 端点标志设置为DWC3_EP_STALL。
usb_ep_clear_halt执行流程如下:
- 发送DEPCMD_CLEARSTALL命令,清除端点的stall状态。
- 清除端点的DWC3_EP_STALL和DWC3_EP_WEDGE标志,DWC3_EP_WEDGE标志设置时主机无法清除设备端点的stall状态,但设备驱动可以自己清除。
2.9.3.set_wedge
usb_ep_set_wedge函数将端点的状态设置为stall,并且会忽略主机发送的CLEAR_FEATURE请求,也就是说主机无法清除带有DWC3_EP_WEDGE标志的端点的stall状态,只有设备驱动自己可以清除。
2.10.连接和断开
软件可以通过usb_gadget_connect和usb_gadget_disconnect函数控制设备连接主机和设备和主机断开。usb_gadget_connect调用后,设备会将D+上拉,主机识别D+上拉后开始枚举设备。
[include/linux/usb/gadget.h]
int usb_gadget_connect(struct usb_gadget *gadget);
int usb_gadget_disconnect(struct usb_gadget *gadget);
usb_gadget_connect和usb_gadget_disconnect的执行流程如下图所示。将DWC3_DCTL寄存器第31位设置为1时,USB设备控制器开始运行,会将D+上拉,主机识别D+上拉后开始枚举设备。将DWC3_DCTL寄存器第31位设置为0时,USB设备停止运行,会和主机断开连接,在将第31位设置为0之前,需要将所有传输的USB请求清空,将31位设置为0之后,需要等待设备和主机断开连接的操作完成,通过读取DWC3_DSTS寄存器的第22位判断,为0时表示已断开连接。
Function驱动和UDC驱动绑定的时候,实质上是通过usb_udc_connect_control函数控制设备和主机连接和断开。主机通过vbus向设备供电,若设备不需要主机供电,则udc->vbus=true。若需要,则通过usb_udc_vbus_handler函数更新vbus的状态,像U盘、移动硬盘这类设备,必须要主机供电,供电以后才能开始工作和被主机枚举。
[include/linux/usb/gadget.h]
static void usb_udc_connect_control(struct usb_udc *udc)
{
if (udc->vbus)
usb_gadget_connect(udc->gadget);
else
usb_gadget_disconnect(udc->gadget);
}
/* updates the udc core vbus status, and try to
* connect or disconnect gadget
*/
void usb_udc_vbus_handler(struct usb_gadget *gadget, bool status)
{
struct usb_udc *udc = gadget->udc;
if (udc) {
udc->vbus = status;
usb_udc_connect_control(udc);
}
}
2.11.属性文件
UDC驱动向/sys目录导出了一些属性文件,供使用者在用户空间查询和操作。下面以rk3399为例,进行介绍:
查看当前的USB控制器是否支持OTG
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/is_otg
查看当前的USB控制器是否处于设备模式
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/is_a_peripheral
查看当前的USB控制器是否是自供电
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/is_selfpowered
查看当前的USB控制器速度
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/current_speed
查看当前的USB控制器支持的最大速度
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/maximum_speed
USB控制器的速度定义如下:
"UNKNOWN","low-speed","full-speed","high-speed","wireless","super-speed","super-speed-plus"。
控制USB设备控制器连接主机,connect-连接,disconnect-断开连接
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/soft_connect
查看当前USB设备控制器的状态
/sys/devices/platform/usb0/fe800000.dwc3/udc/fe800000.dwc3/state
USB设备控制器的状态定义如下:
"not attached","attached","powered","reconnecting","unauthenticated","default","addressed","configured","suspended"。
3.总结
上面总结了常用的UDC驱动接口,这些接口大多数可被Function驱动直接调用,少部分经过封装被Function驱动调用。通过分析这些接口的调用流程,对认识USB控制器内部的工作流程有很大的帮助。其实最重要的是弄清楚USB设备控制器接收数据和发送数据的流程,发送数据通过usb_ep_queue发送,上面已经介绍过了,但接收数据的流程牵扯到中断处理和中断处理线程,后面专门开一章节介绍USB设备控制器的中断处理过程和数据接收过程。
参考资料
- Rockchip RK3399TRM V1.3 Part1
- Rockchip RK3399TRM V1.3 Part2
- Linux内核4.4.179版本源码
- USB开发大全(第四版)