上一篇文章讲了在STM32F407 Disc1上使用RT-Thread CDC
这次看一下内部的具体调用流程,顺便解决枚举时间过长问题
1 数据结构
1.1 涉及的文件
files |
description |
|
components/drivers/usb/usbdevice/core/usbdevice.c |
usb device 注册 |
|
components/drivers/usb/usbdevice/core/usbdevice_core.c |
rtthread usb内核文件 |
|
components/drivers/usb/usbdevice/class/cdc_vcom.c |
cdc类注册及数据处理 |
|
drivers/drv_usbd.c |
对接STM32 HAL driver 和 内核 |
对比STM32官方的框架,发现整体框架差不多, 就是把USB中间层换成了rtthread自己的了

图片来源:CSDN博主「king_jie0210」
原文链接:https://blog.csdn.net/king_jie0210/article/details/76713938
1.2 usb 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_usbd.c中实现了USB的中断处理函数USBD_IRQ_HANDLER(OTG_FS_IRQHandler的重定义),里面调用了ST 提供的HAL_PCD_IRQHandler

2)HAL_PCD_IRQHandler处理不同类型的中断源,然后调用具体的回调函数。这些回调函数在stm32f4xx_hal_pcd.c中均定义为__WEAK弱函数,并未实现具体内容,需要具体协议或接口部分来实现。rtthread把这部分也放在了drv_usbd.c中

各回调函数简介如下
Calllback |
Description |
Note |
HAL_PCD_ResetCallback |
复位设备(禁用),设置设备状态为USB_STATE_NOTATTACHED |
|
HAL_PCD_SetupStageCallback |
Setup令牌包的处理 |
|
HAL_PCD_DataInStageCallback |
IN令牌包的处理 |
|
HAL_PCD_ConnectCallback |
设置设备状态为USB_STATE_ATTACHED |
|
HAL_PCD_SOFCallback |
SOF令牌包的处理(空函数,不处理) |
|
HAL_PCD_DisconnectCallback |
禁用设备,设置设备状态为USB_STATE_NOTATTACHED |
|
HAL_PCD_DataOutStageCallback |
OUT令牌包的处理 |
|
HAL_PCDEx_SetConnectionState |
STM32F4上为空函数 |
下面按照枚举过程分析一下具体的内部处理
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_SetupStageCallback调用内核的rt_usbd_ep0_setup_handler,传入usb控制器 _stm_udc 和已经在HAL_PCD_IRQHandler中解析出来的setup包两个参数
- rt_usbd_ep0_setup_handler向USB内核发送了一条msg, type类型为
USB_MSG_SETUP_NOTIFY
- 在usbdevice_core.c创建的rt_usbd_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
/**
* 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)
{
/* parameter check */
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 a full-speed only device (with a device descriptor version number equal to 0200H) receives a
GetDescriptor() request for a device_qualifier, it must respond with a request error. The host must not make
a request for an other_speed_configuration descriptor unless it first successfully retrieves the
device_qualifier descriptor. */
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\n");
rt_usbd_ep0_set_stall(device);
break;
}
}
else
{
rt_kprintf("request direction error\n");
rt_usbd_ep0_set_stall(device);
}
return RT_EOK;
}
2.4 获取字符串描述符
同上,最后根据setup->wValue的值选择调用的是 _get_string_descriptor
2.5 设置配置
set configuration本身也属于标准请求
->_standard_request(device, setup);
->_set_config(device, setup);
_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)描述符。
以上摘自CSDN:https://blog.csdn.net/u012028275/article/details/109276309
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的处理。继续深入
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->is_in= 0,最终设置的是端点0的DOEPCTL的STALL域为1。但是当前是输入事务,应该设置DIEPCTL的STALL域为1。参考一下ST官方的做法,是区分0x80和0x0的
/**
* @brief USBD_CtlError
* Handle USB low level Error
* @param pdev: device instance
* @param req: usb request
* @retval None
*/
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的输入和输出全部STALL, CherryUSB的做法只处理了输入部分
/* Default USB control EP, always 0 and 0x80 */
#define USB_CONTROL_OUT_EP0 0
#define USB_CONTROL_IN_EP0 0x80

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是需要根据端点方向设置的
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)
接受(Return Data): Host接收确认后,再发一个Out状态确认一下,圆满了,恭喜 !
模棱两可(NAK) : 我还没准备好啊!(害苦了痴男痴女,一直不停询问)
明确拒绝(STALL) : 痴男痴女瞬间觉醒,不再坚持
3 数据传输
除了默认的控制端点0和CDC Communication类接口使用了一个中断输入端点,CDC Data类接口中还使用了一对批量端点,工程中使用的是批量输入端点1,批量输出端点1。下面章节主要说明批量端点上的数据传输。

BULK端点数据收发均由usbdevice_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\n");
break;
}
}
else
{
rt_list_insert_before(&ep->request_list, &req->list);
RT_DEBUG_LOG(RT_DEBUG_USB, ("suspend a request\n"));
}
return size;
}
3.1 workflows
- rt_usbd_io_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_usbd_thread_entry 线程接收该消息后处理
- 根据msg type类型为
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
如我们经常使用的主机设备模型(设备一直等待接收host的命令,然后处理), CDC类设备也要时刻准备这接收Host的发来的数据
回顾一下2.4中提到的FUNC_ENABLE(func),它在set configuration后被调用
#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根据ep端点最终调用的是vcom_cdc.c中的_ep_out_handler函数,主要完成:
- 把接收的数据发放vcom设备的rx_ringbuffer中
- 通知serial设备
- 再次发起一个设备读请求,准备接收下一包数据
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\n", size));
data = (struct vcom*)func->user_data;
/* ensure serial is active */
if((data->serial.parent.flag & RT_DEVICE_FLAG_ACTIVATED)
&& (data->serial.parent.open_flag & RT_DEVICE_OFLAG_OPEN))
{
/* receive data from USB VCOM */
level = rt_hw_interrupt_disable();
rt_ringbuffer_put(&data->rx_ringbuffer, data->ep_out->buffer, size);
rt_hw_interrupt_enable(level);
/* notify receive data */
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 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线程
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\n"));
}
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
,但走的是if分支

如果剩余待发送的数据size > MPS,发送一个MPS长度的包
如果剩余待发送的数据size>0 (size<= MPS) ,发生剩余长度
如果剩余待发送的数据size==0,数据全部发生完成进入EP_HANDLER,具体是进入_ep_in_handler,完成data->wait完成量
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\n", request_size));
if ((request_size != 0) && ((request_size % EP_MAXPACKET(data->ep_in)) == 0))
{
/* don't have data right now. Send a zero-length-packet to
* terminate the transaction.
*
* FIXME: actually, this might not be the right place to send zlp.
* Only the rt_device_write could know how much data is sending. */
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等到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设备倒是可以的。
大佬总结到位!!👍👍
@sakumisu 你才是大佬,在学习CherryUSB中。确认一下对控制传输端点0,STALL只设置在输入方向是没问题的吧?
@blta 没问题。学习过程中又什么疑问欢迎提出!
上面的数据结构图是用什么软件生成的,sourceinsight生成的么?
@fhqmcu XMind思维导图软件
非常好的总结,想问下博主我的程序插上USB线后,一直收不到USB_MSG_SETUP_NOTIFY这个是什么原因。附上我的日志。
@Game7 你这个看上去一直在USB_MSG_RESET 复位啊,很可能是你USB CLK的时钟有问题,检测一下是不是48MHz,或者系统时钟外部晶振和实际不符
@blta 感谢楼主,检查了下是USB时钟的问题
楼主,我usb枚举失败了,电脑显示未知设备,设备描述符请求失败,楼主知道什么原因吗
@blta
@小小世界
你如果使用的RTT的USB 设备,在stm32上一般都能枚举成功, 可以先检测一下USB外设及时钟是否配置正确了,老版本的rtt需要手动copy clk的初始化。
@blta 楼主,我新建了一个工程,按官方的配置完后,也修改了时钟,但还是不行,这个有什么注意的地方吗

时钟配置
按楼主的方法修改,果然正确返回STALL了,并且识别速度快了很多!