这次看一下内部的具体调用流程,顺便解决枚举时间过长问题
1 数据结构
1.1 涉及的文件
对比STM32官方的框架,发现整体框架差不多, 就是把USB中间层换成了rtthread自己的了
1.2 u*** device_list
存在一个全局的device_list 管理 udevice设备。 udevice中有两个重要的成员:
cfg_list 管理 uconfig 链表,uconfig->func_list->inf_list 获取接口 udcd_t 管理具体的内核和Hal层的接口
1.3 cdc creat
另一个重要的链表,主要在cdc_vcom.c中完成构造,这要是设备接口和端点的构造。
2 枚举
2.1 Callbacks
1)首先在drv_u***d.c中实现了USB的中断处理函数USBD_IRQ_HANDLER(OTG_FS_IRQHandler的重定义),里面调用了ST 提供的HAL_PCD_IRQHandler
2)HAL_PCD_IRQHandler处理不同类型的中断源,然后调用具体的回调函数。这些回调函数在stm32f4xx_hal_pcd.c中均定义为__WEAK弱函数,并未实现具体内容,需要具体协议或接口部分来实现。rtthread把这部分也放在了drv_u***d.c中
各回调函数简介如下
下面按照枚举过程分析一下具体的内部处理
2.2 获取设备描述符
大致的调用流程如下:
HAL_PCD_SetupStageCallback调用内核的rt_u***d_ep0_setup_handler,传入u***控制器 _stm_udc 和已经在HAL_PCD_IRQHandler中解析出来的setup包两个参数 rt_u***d_ep0_setup_handler向USB内核发送了一条msg, type类型为 USB_MSG_SETUP_NOtiFY
在u***device_core.c创建的rt_u***d_thread_entry 线程接收该消息后处理 根据msg type类型为 USB_MSG_SETUP_NOTIFY 调用_setup_request() 根据setup->request_type的请求类型(USB_REQ_TYPE_STANDARD)进一步调用_standard_request
根据setup->request_type的接收者(USB_REQ_TYPE_DEVICE)和setup->bRequest请求码(USB_REQ_GET_DESCRIPTOR)进一步调用_get_descriptor
根据setup->wValue描述符类型(USB_DESC_TYPE_DEVICE),最终调用_get_device_descriptor返回具体的设备描述符
2.3 获取配置描述符
获取配置描述符的流程和获取设备描述符一样,只是最后根据setup->wValue的值选择调用的是 _get_config_descriptor
2.4 获取字符串描述符
同上,最后根据setup->wValue的值选择调用的是 _get_string_descriptor
2.5 设置配置
set configuration本身也属于标准请求
_set_config处理如下:
设置 device->curr_cfg = cfg; dcd_set_config(device->dcd, value); 使能端点 FUNC_ENABLE(func) 使能function, 准备接受主机数据
set configuration意味着设备枚举完成,可以正常接受数据了 稍后我们会在涉及到FUNC_ENABLE(func)
2.6 枚举时间过长
在上一篇文章中,发现当前rtthread CDC 设备枚举时间过长,大概8s左右,实在不能接受。
2.6.1 原因
这次直接上分析仪看下
从抓包来看时间主要浪费在了获取 DeviceQualifier。 显示具体细节,发现主机一直在等待设备对该请求的明确回应,但设备端一直在回复NAK,浪费了很多时间
2.6.2 DeviceQualifier Descriptor(设备限定描述符)
设备限定描述符(Device Qualifier Descriptor)说明了能进行高速操作的设备在其他速度时产生的变化信息。例如,如果设备当前在全速下操作,设备限定描述符返回它如何在高速运行的信息。
如果设备既支持全速状态又支持高速状态,那么就必须含有设备限定描述符(Device Qualifier Descriptor)。设备限定描述符(Device Qualifier Descriptor)中含有当前没有使用的速度下这些字段的取值。
如果只能进行全速(full-speed)操作的设备(设备描述符的版本号等于0200H)接收到请求设备限定符的Get Descriptor请求,它必须用请求错误响应,回复STALL来响应。
主机端只能在成功取得设备限定描述符(Device Qualifier Descriptor)之后,才能请求其他速度配置(other_speed_configuration)描述符。
2.6.3 解决
STM32F407-Disc虽然支持High speed ,但是需要外加PHY才行,当前工程默认使用的还是Full speed Device。
从2.6.2可知在接收到请求设备限定符的Get Descriptor请求时,应该及时返回STALL握手包告知主机: 设备不支持限定描述符,无法执行这个请求。
开始review codes, 参考第3节首先找到Get DeviceQualifier Descriptor标准请求最后的处理:
在_get_descriptor里确实对不同speed的设备做了处理, 也有stall的处理。继续深入
最后还是由HAL函数HAL_PCD_EP_SetStall处理
底层寄存器操作:
通过代码可以看出,当前传入的ep_addr是0, 那么ep->is_in= 0,最终设置的是端点0的DOEPCTL的STALL域为1。但是当前是输入事务,应该设置DIEPCTL的STALL域为1。参考一下ST官方的做法,是区分0x80和0x0的
ST的做法是一次性把端点0的输入和输出全部STALL, CherryUSB的做法只处理了输入部分
1.STALL握手包均是由device发向Host,即均是由IN令牌包处理的,涉及的是端点0的输入方向
IN事务,设备直接在IN令牌包后,回复Data/NAK/ACK OUT事务,设备先接收数据,然后根据情况发送ACK/NAK/STALL(批量事务还存在NYET) 如果OUT方向STALL,回复STALL握手包这是协议规定的; IN方向设置了STALL,也会导致IN状态包回复一个STALL握手包,效果一样。
2.控制断点0的输出方向其实不受状态的影响(为了保证setup包能被一直接收成功),而且一旦接收到setup包会自动清零两个方向
所以我们直接按照CherryUSB的做法,直接设置0x80输入方向的STALL即可。
非控制端点0,STALL是需要根据端点方向设置的
重新编译后,不支持的Get DeviceQualifier Descriptor,设备很快返回了STALL握手包,枚举时间正常1s以内。
有没有发现,获取描述符很有意思:
就像一个痴男或者痴女(Host),一直在向另一方(Device)要求一个结果(IN package) 接受(Return Data): Host接收确认后,再发一个Out状态确认一下,圆满了,恭喜 ! 模棱两可(NAK) : 我还没准备好啊!(害苦了痴男痴女,一直不停询问) 明确拒绝(STALL) : 痴男痴女瞬间觉醒,不再坚持
3 数据传输
除了默认的控制端点0和CDC Communication类接口使用了一个中断输入端点,CDC Data类接口中还使用了一对批量端点,工程中使用的是批量输入端点1,批量输出端点1。下面章节主要说明批量端点上的数据传输。
BULK端点数据收发均由u***device_core.c中的 rt_u***d_io_request函数完成
3.1 workflows
rt_u***d_io_request 发起一个USB端点的读或写请求 数据传输完成触发USBD_IRQ_HANDLER,然后调用drv_u***d.c中的HAL_PCD_DataOutStageCallback/HAL_PCD_DataInStageCallback 调用rt_u***d_ep_out_handler(&_stm_udc, epnum, hpcd->OUT_ep[epnum].xfer_count)或 rt_u***d_ep_in_handler(&_stm_udc, 0x80 | epnum, hpcd->IN_ep[epnum].xfer_count) 向内核发送一个u***_mq消息,type类型为 USB_MSG_DATA_NOTIFY 在u***device_core.c创建的rt_u***d_thread_entry 线程接收该消息后处理 根据msg type类型为 USB_MSG_DATA_NOTIFY 调用_data_notify()
到了_data_notify意味着已经收到或者发送了一包数据,下面是处理剩余的数据,是否再次发起rt_u***d_io_request请求
3.2 ep_out
3.2.1 _function_enable
如我们经常使用的主机设备模型(设备一直等待接收host的命令,然后处理), CDC类设备也要时刻准备这接收Host的发来的数据
回顾一下2.4中提到的FUNC_ENABLE(func),它在set configuration后被调用
该宏最后调用的是cdc_vcom.c中的_function_enable。很明显在set configuration 完成后,做的第一件事情就是,准备接收数据。
接收到Host发送的数据,处理主要在_data_notify的else分支,而且ep->request.req_type == UIO_REQUEST_READ_BEST,所以直接进入
EP_HANDLER根据ep端点最终调用的是vcom_cdc.c中的_ep_out_handler函数,主要完成:
把接收的数据发放vcom设备的rx_ringbuffer中 通知serial设备 再次发起一个设备读请求,准备接收下一包数据
3.2.2 size< wMaxPacketSize
这种情况下,主机接收到数据小于最大包长MPS, 认为传输完成
3.2.3 size == wMaxPacketSize*n
因为设备端都是按照MPS接收的,一个或多个包
3.2.4 size > wMaxPacketSize && size % wMaxPacketSize !=0
多MPS包,和一个不够MSP长度的包(结束包)
3.3 ep_in
3.3.1 发送数据流程
按照当前的CDC结构,由注册的一个serial设备,调用rt_device_write向其tx_ringbuffer写入数据
在cdc_vcom.c里注册了一个vcom_tx_thread_entry线程
代码部分省略精简 vcom_tx_thread_entry 主要做了几件事:
1)查询tx_ringbuffer是否有数据,无数据继续查询 2)发现数据,发起一个USB IO写请求,向Host发送数据 3)如ep_out一样,会进入_data_notify,但走的是if分支
如果剩余待发送的数据size > MPS,发送一个MPS长度的包 如果剩余待发送的数据size>0 (size<= MPS) ,发生剩余长度 如果剩余待发送的数据size==0,数据全部发生完成进入EP_HANDLER,具体是进入_ep_in_handler,完成data->wait完成量
4.vcom_tx_thread_entry等到data->wait完成量,通知serial设备,发送完成
3.3.2 size < wMaxPacketSize
只进入_data_notify一次,然后调用_ _ep_in_handler
3.3.3 size == wMaxPacketSize*n
_ep_in_handler里需要追加一个ZLP包,告知Host传输结束,由于ringbuffer的使用,不太好抓,暂不展示
3.3.4 size > wMaxPacketSize && size % wMaxPacketSize !=0
多个MPS 包 + 一个小于 MPS作为结束包
4.总结
总体来说,rtthread USB协议栈和CherryUSB,ST官方的整体调用框架差不多,但比CherryUSB和ST官方的稍显复杂,主要体现在它的数据结构上,初看有点懵。
另外它的描述符部分做的不太好,先定义了全局Const结构体,然后在注册构造设备时又malloc空间,memcpy到内存,感觉没什么必要。
还有就是不太建议在当前CDC基础上直接添加复杂用户协议(可以参考它,改成Customer自定义设备),如果是用于UART设备倒是可以的。
|