这次看一下内部的具体调用流程,顺便解决枚举时间过长
1 数据结构
1.1 涉及的文件
文件描述
对比STM32官方的框架,发现整个框架就好了,就是把USB中间换层变成rtthread自己的了
1.2 USB 设备列表
udevice 有两个重要的成员:
cfg_list 管理 uconfig 链表,uconfig->func_list->inf_list 接口获取
cd_t 管理具体的内核和Hal层的接口
1.3 CDC 创建
重要的链表,主要在 cdc_vcom.c 中完成构造,这另一个设备接口和终结的构造。
2 枚举
2.1 回调
1)首先在drv_usbd.c中实现了USB的中断处理函数USBD_IRQ_HANDLER(OTG_FS_IRQHandler的重定义),里面调用了ST的HAL_PCD_IRQHandler
2)PCD_IRQHandler处理的类型的中断然后调用不同的针对性的函数源。这些函数部分在32f4xxhal_d.中定义线程均__WEAK协议,具体为接口实现内容,需要具体实现。这部分也闯了drv_usbd.c中
各函数简介
下面按照枚举过程分析一下具体的内部处理
2.2 获取设备
的一般流程如下:
USBD_IRQ_HANDLER
->HAL_PCD_SetupStageCallback
->rt_usbd_ep0_setup_handler
-> msg.type = USB_MSG_SETUP_NOTIFY;
msg.dcd = dcd;
rt_usbd_event_signal(&msg);
->rt_mq_send(&usb_mq, (void*)msg, sizeof(struct udev_msg))
------------------------------------------------------------------------------------------------------------------------- rt_usbd_thread_entry
->rt_mq_recv(&usb_mq, &msg, sizeof(struct udev_msg),RT_WAITING_FOREVER)
switch (msg.type)
{
case USB_MSG_SETUP_NOTIFY:
_setup_request(device, &msg.content.setup);
->_standard_request(device, setup);
-> _get_descriptor(device, setup):
if(setup->request_type == USB_REQ_TYPE_DIR_IN)
{
switch(setup->wValue >> 8)
{
case USB_DESC_TYPE_DEVICE:
_get_device_descriptor(device, setup);
break;
case USB_DESC_TYPE_CONFIGURATION:
_get_config_descriptor(device, setup);
break;
case USB_DESC_TYPE_STRING:
_get_string_descriptor(device, setup);
break;
HAL_PCD_IRQHandler 的内核调用 bd_setup_handler_PC_setb_Setup_handler_PC_setb_Setup_HAL_PCD_IRQHandler 的两个参数
rt_usbd_ep0_setup_handler向USB内核发送了一条msg, type类型为USB_MSG_SETUP_NOTIFY
在usbdevice_core.c创建的rt_usd_thread_entry线程接收该消息后处理
可知msg类型为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->w根据类型的值(或返回类型_DESC_TYPEDEVICE),最终调用_get_device_descriptor
2.3 获取配置
简单配置的最后一个流程和获取设备或者简单的脚本,根据设置->wValue的选择调用是__config_de
/**
- This function will handle get_descriptor bRequest.
- @param device the usb device object.
- @param setup the setup bRequest.
- @return RT_EOK on successful.
*/
static rt_err_t _get_descriptor(struct udevice* device, ureq_t setup)
{
RT_ASSERT(device != RT_NULL);
RT_ASSERT(setup != RT_NULL);
if(setup->request_type == USB_REQ_TYPE_DIR_IN)
{
switch(setup->wValue >> 8)
{
case USB_DESC_TYPE_DEVICE:
_get_device_descriptor(device, setup);
break;
case USB_DESC_TYPE_CONFIGURATION:
_get_config_descriptor(device, setup);
break;
case USB_DESC_TYPE_STRING:
_get_string_descriptor(device, setup);
break;
case USB_DESC_TYPE_DEVICEQUALIFIER:
if(device->dcd->device_is_hs)
{
_get_qualifier_descriptor(device, setup);
}
else
{
rt_usbd_ep0_set_stall(device);
}
break;
case USB_DESC_TYPE_OTHERSPEED:
_get_config_descriptor(device, setup);
break;
default:
rt_kprintf("unsupported descriptor request
");
rt_usbd_ep0_set_stall(device);
break;
}
}
else
{
rt_kprintf("request direction error
");
rt_usbd_ep0_set_stall(device);
}
return RT_EOK;
}
2.4 获取遗产
同上,最后根据setup->wValue的值选择调用是_get_string_descriptor
2.5 设置配置
设置配置本身也属于标准请求
->_standard_request(device, setup);
->_set_config(device, setup);
_set_config 处理如下:
设置设备->curr_cfg = cfg;
dcd_set_config(设备->dcd, 值);
使能端点
FUNC_ENABLE(func) 使能功能,准备接受主机数据
set configuration 意味着数据设备枚举完成,可以正常接受了
稍后我们会在涉及到FUNC_ENABLE(func)
2.6 枚举时间过长
在上一篇文章中,发现当前rtthread CDC设备枚举时间过长,大概8秒左右,不能实际接受。
2.6.1 原因
本次直接上分析仪看下
从抓包看主要浪费端在获取设备限定符。
2 DeviceQualifier(设备限定词)描述符。6.2
如果设备运行运行命令(运行脚本)Device Qualifier 说明了可以高速运行的设备的其他信息。
如果支持既定设备的状态就必须同时包含所有设备的价值定义(或设备质量)。设备设备定义使用脚本(或)设备定义器描述(或)设备描述器的速度没有足够的速度支持当前获取这些设备的条件。
如果进行全部的请求(设备必须按照要求尽快使用规定的请求版本号(0H)接收到设备的请求描述符),则必须响应,STALL 来回复响应。
端在成功取得设备成功成就其他要求(设备限定符德或)成功配置)。
2.6.3 解决
STM32F407-Disc虽然支持高速,但需要外加PHY才行,目前工程默认使用的还是全速设备。
26.2 设备在接收到请求定义符的获取描述符请求时,应及时返回 STALL 请求包表示设备支持定义,无法执行此请求。
开始审核代码,参考第 3 节首先找到 DeviceQualifier Descriptor 的标准请求最后的处理:
在_get_descriptor里确实对不同速度的设备进行了处理,也有stall的处理。继续深入
rt_usbd_ep0_set_stall(device);
->dcd_ep_set_stall(device->dcd, 0);
->dcd->ops->ep_set_stall(0);
->_ep_set_stall(0)
->HAL_PCD_EP_SetStall(&_stm_pcd, 0);
最后还是由HAL函数HAL_PCD_EP_SetStall处理
智能操作:
通过代码可以设置为当前域的地址,当前域的 ep_addr 为 0,ep-> 为 0,最终设置为域的 0,即当前域的 STALL1 的 DOEPCTL 的 STALL1。但是是输入事务,应为 DIEPC看看ST官方的做法,是什么0x80和0x0
/**
*/
void USBD_CtlError(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
UNUSED(req);
(void)USBD_LL_StallEP(pdev, 0x80U);
(void)USBD_LL_StallEP(pdev, 0U);
}
/**
- @brief Sets a Stall condition on an endpoint of the Low Level Driver.
- @param pdev: Device handle
- @param ep_addr: Endpoint number
- @retval USBD status
*/
USBD_StatusTypeDef USBD_LL_StallEP(USBD_HandleTypeDef *pdev, uint8_t ep_addr)
{
HAL_StatusTypeDef hal_status = HAL_OK;
USBD_StatusTypeDef usb_status = USBD_OK;
hal_status = HAL_PCD_EP_SetStall(pdev->pData, ep_addr);
usb_status = USBD_Get_USB_Status(hal_status);
return usb_status;
}
ST的做法是最后把0的输入和输出全部ST,CherryUSB的做法只是处理了输入部分
/* Default USB control EP, always 0 and 0x80 */
#define USB_CONTROL_OUT_EP0 0
#define USB_CONTROL_IN_EP0 0x80
1.STALL逗包均是由设备发向Host,均是由IN令牌包处理的,涉及是0个端点的输入方向
IN事务,设备直接在IN包裹后,回复Data/NAK/ACK
OUT 事务,设备先接收数据,然后根据情况发送 NAK/STALLACK(数据队列,然后还存在 NYET)
如果方向ALL,回复STALL招招包这是协议规定的;IN方向设置了STALL,也会导致IN状态包回复一个STALL招招包,效果一样。
2.控制断点0的输出实际上不受状态的影响(确保设置包能够持续接收成功),并且被接收到设置包会自动清零两个方向
所以我们直接按照CherryUSB的做法,直接设置0x80输入方向的STALL启用。
非控制终点0,STALL是需要根据方向的
rt_err_t rt_usbd_ep_set_stall(udevice_t device, uep_t ep)
{
rt_err_t ret;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(ep != RT_NULL);
RT_ASSERT(ep->ep_desc != RT_NULL);
ret = dcd_ep_set_stall(device->dcd, EP_ADDRESS(ep));
if(ret == RT_EOK)
{
ep->stalled = RT_TRUE;
}
return ret;
}
重新编译后,不支持的Get DeviceQualifier Descriptor,设备很快就返回了STALL招包,枚举时间正常1s以内。
有没有发现,发现有异常:
就像一个痴男或痴女(Host),一直在向另一个(Device)一个结果(IN package)
接受(返回数据):主机接收确认后,再一个外出状态确认,一下发了,恭喜!
模棱两可(NAK):我痴痴地准备好啊!(害苦了男女,一直不停地了解)
明确拒绝(STALL):痴男痴女瞬间觉醒,不再坚持
3 数据传输
的控制终端0和CDC类通讯类使用了一个普通接口输入,CDC数据类使用了一个普通接口,工程中的终端中类输入1,还使用示例1。扩展上的数据传输。
BULKdevice端点数据分发均由usb_core.c中的rt_usbd_io_request函数完成
rt_size_t rt_usbd_io_request(udevice_t device, uep_t ep, uio_request_t req)
{
rt_size_t size = 0;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(req != RT_NULL);
if(ep->stalled == RT_FALSE)
{
switch(req->req_type)
{
case UIO_REQUEST_READ_BEST:
case UIO_REQUEST_READ_FULL:
ep->request.remain_size = ep->request.size;
size = rt_usbd_ep_read_prepare(device, ep, req->buffer, req->size);
break;
case UIO_REQUEST_WRITE:
ep->request.remain_size = ep->request.size;
size = rt_usbd_ep_write(device, ep, req->buffer, req->size);
break;
default:
rt_kprintf("unknown request type
");
break;
}
}
else
{
rt_list_insert_before(&ep->request_list, &req->list);
RT_DEBUG_LOG(RT_DEBUG_USB, ("suspend a request
"));
}
return size;
}
3.1 工作流程
rt_iousbd_request 发起一个终结USB的读或写请求
数据传输完成触发USBD_IRQ_HANDLER,然后调用drv_usbd.c中的HAL_PCD_DataOutStageCallback/HAL_PCD_DataInStageCallback
调用rt_usbd_ep_out_handler(&_stm_udc, epnum, hpcd->OUT_ep[epnum].xfer_count)或rt_usbd_ep_in_handler(&_stm_udc, 0x80 | epnum, hpcd->IN_ep[epnum].xfer_count)
向内核发送一个usb_mq消息,type类型为USB_MSG_DATA_NOTIFY
在usbdevice_core.c创建的rt_usd_thread_entry线程接收该消息后处理
可知msg类型为USB_MSG_DATA_NOTIFY调用_data_notify()
USBD_IRQ_HANDLER
->HAL_PCD_DataOutStageCallback/HAL_PCD_DataInStageCallback
->rt_usbd_ep_out_handler(&_stm_udc, epnum, hpcd->OUT_ep[epnum].xfer_count)
/rt_usbd_ep_in_handler(&_stm_udc, 0x80 | epnum, hpcd->IN_ep[epnum].xfer_count)
-> msg.type = USB_MSG_DATA_NOTIFY;
msg.dcd = dcd;
msg.content.ep_msg.ep_addr = address;
msg.content.ep_msg.size = size;
rt_usbd_event_signal(&msg);
->rt_mq_send(&usb_mq, (void*)msg, sizeof(struct udev_msg))
rt_usbd_thread_entry
->rt_mq_recv(&usb_mq, &msg, sizeof(struct udev_msg),RT_WAITING_FOREVER)
_data_notify(device, &msg.content.ep_msg)
到了_data_notify数据已经收到或者发送了一个数据包,下面是剩余的数据,是否正在处理rt_usbd_io_request请求
3.2 ep_out
3.2.1 _function_enable
如我们经常使用的主机设备模型(一直等待接收主机的命令,然后),CDC 类设备也要时刻准备接收主机的发来的数据
回顾一下2.4中提到的FUNC_ENABLE(func),它在set配置后被调用
#define FUNC_ENABLE(func) do{
if(func->ops->enable != RT_NULL &&
func->enabled == RT_FALSE)
{
if(func->ops->enable(func) == RT_EOK)
func->enabled = RT_TRUE;
}
该宏最后调用的是cdc_vcom.c中的_function_enable。很明显在set configuration完成后,做的第一件事情就是,准备接收数据。
接收到Host发送的数据,主要处理在_data_notify的else分支,而且是直接ep->request.req_type == UIO_REQUEST_READ_BEST,所以进入
EP_HANDLER最终最终调用是vcom_cdc.c中的_ep_out_handler函数,主要完成:
把接收中的数据发送到vcom设备的rx_ringbuffer
通知串口设备
再次发起一个设备读请求,准备接收下一包数据
static rt_err_t _ep_out_handler(ufunction_t func, rt_size_t size)
{
rt_uint32_t level;
struct vcom *data;
RT_ASSERT(func != RT_NULL);
RT_DEBUG_LOG(RT_DEBUG_USB, ("_ep_out_handler %d
", size));
data = (struct vcom*)func->user_data;
if((data->serial.parent.flag & RT_DEVICE_FLAG_ACTIVATED)
&& (data->serial.parent.open_flag & RT_DEVICE_OFLAG_OPEN))
{
level = rt_hw_interrupt_disable();
rt_ringbuffer_put(&data->rx_ringbuffer, data->ep_out->buffer, size);
rt_hw_interrupt_enable(level);
rt_hw_serial_isr(&data->serial,RT_SERIAL_EVENT_RX_IND);
}
data->ep_out->request.buffer = data->ep_out->buffer;
data->ep_out->request.size = EP_MAXPACKET(data->ep_out);
data->ep_out->request.req_type = UIO_REQUEST_READ_BEST;
rt_usbd_io_request(func->device, data->ep_out, &data->ep_out->request);
return RT_EOK;
}
3.2.2 大小< wMaxPacketSize
在这种情况下,主机接收到最小的数据包长 MPS,认为传输完成
3.2.3 大小== wMaxPacketSize*n
因为设备端按照MPS接收的,一个或多个包
3.2.4 尺寸 > wMaxPacketSize && 尺寸 % wMaxPacketSize !=0
多MPS包,和一个MSP长度的包(结束包)
3.3 ep_in
3.3.1 发送数据流程
按照当前的CDC结构,由注册的一个串行设备,调用rt_device_write向其tx_ringbuffer写入数据
在cdc_vcom.c里注册了一个vcom_tx_thread_entry线程
while(rt_ringbuffer_data_len(&data->tx_ringbuffer))
{
level = rt_hw_interrupt_disable();
res = rt_ringbuffer_get(&data->tx_ringbuffer, ch, CDC_BULKIN_MAXSIZE);
rt_hw_interrupt_enable(level);
.....
rt_completion_init(&data->wait);
data->ep_in->request.buffer = ch;
data->ep_in->request.size = res;
data->ep_in->request.req_type = UIO_REQUEST_WRITE;
rt_usbd_io_request(func->device, data->ep_in, &data->ep_in->request);
if (rt_completion_wait(&data->wait, VCOM_TX_TIMEOUT) != RT_EOK)
{
RT_DEBUG_LOG(RT_DEBUG_USB, ("vcom tx timeout
"));
}
if(data->serial.parent.open_flag & RT_DEVICE_FLAG_INT_TX)
{
rt_hw_serial_isr(&data->serial,RT_SERIAL_EVENT_TX_DONE);
rt_event_send(&data->tx_event, CDC_TX_HAS_SPACE);
}
代码部分省略简化
vcom_tx_thread_entry 主要做了几件事:
1)查询tx_ringbuffer是否有数据,无数据继续查询
2)发现数据,发送USB IO写入请求,向Host数据
3)如ep_out一样,会进入_data_notify,但如果分支则走
如果剩余等待发送的数据大小 > MPS,发送一个 MPS 长度的包
如果剩下待发送的数据size>0 (size<= MPS) , 发生剩余长度
待发送的数据大小==0,数据全部完成进入EP_HANDLER,具体是进入_ep_in_handler,完成数据->等待完成量
static rt_err_t _ep_in_handler(ufunction_t func, rt_size_t size)
{
struct vcom *data;
rt_size_t request_size;
RT_ASSERT(func != RT_NULL);
data = (struct vcom*)func->user_data;
request_size = data->ep_in->request.size;
RT_DEBUG_LOG(RT_DEBUG_USB, ("_ep_in_handler %d
", request_size));
if ((request_size != 0) && ((request_size % EP_MAXPACKET(data->ep_in)) == 0))
{
data->in_sending = RT_TRUE;
data->ep_in->request.buffer = RT_NULL;
data->ep_in->request.size = 0;
data->ep_in->request.req_type = UIO_REQUEST_WRITE;
rt_usbd_io_request(func->device, data->ep_in, &data->ep_in->request);
return RT_EOK;
}
rt_completion_done(&data->wait);
return RT_EOK;
}
4.vcom_tx_thread_entry等到数据->等待完成量,通知串口设备,发送完成
3.3.2 大小 < wMaxPacketSize
只进入一次data_notify,然后调用 _ep_in_handler
3.3.3 大小== wMaxPacketSize*n
_in_handler里需要附加一个ZLP,告知主机传输结束,由于buffer的使用,很好抓,暂不展示
3.3.4 尺寸 > wMaxPacketSize && 尺寸 % wMaxPacketSize !=0
多个MPS包+一个小MPS作为结束包
4 总结
说起来,rtthread USB 协议,ST 官方的 USB 框架差不多,但只是 Cherry USB 和 ST 官方的有点显眼,主要比 Cherry 在它的数据结构上,主要看在它的数据结构上。
它的结构体又多了一个定义好的结构,然后在配置注册设备时空间,memc 到内存,感觉没有什么不对。
还有就是建议在当前CDC基础上直接添加复杂用户协议(可以把它改成自定义设备),如果是用于UART设备倒是可以的。
原作者:blta