STM32L151单片机USB虚拟串口传输速度深度解析与实战记录
UART串口常用波特率最高115200的情况下,理论传输速度极限是11KB/s,实际使用差不多在5KB/s左右,使用USB虚拟串口除去不需要设置波特率这个优点以外,主要还是希望提高传输速度,缩短下载数据花费的时间。然而在简单配置虚拟串口替代串口后,发现传输速度没有明显变化,所以实验一下虚拟串口的传输速度的主要影响因素。
首先查看一下CubeMX生成的usb相关源码
复位(每次USB插上时,触发USB外设复位):
void HAL_PCD_ResetCallback(PCD_HandleTypeDef *hpcd)
#endif /* USE_HAL_PCD_REGISTER_CALLBACKS */
{
USBD_SpeedTypeDef speed = USBD_SPEED_FULL;
if ( hpcd->Init.speed != PCD_SPEED_FULL)
{
Error_Handler();
}
/* Set Speed. */
USBD_LL_SetSpeed((USBD_HandleTypeDef*)hpcd->pData, speed);
/* Reset Device. */
USBD_LL_Reset((USBD_HandleTypeDef*)hpcd->pData);
}
其中配置了传输速率,这个速率可以在CubeMx里修改,所以代码就不用动了
由于我研究的是发送,所以接收就不看了,直接看虚拟串口发送:
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 7 */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
if (hcdc->TxState != 0){
return USBD_BUSY;
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
/* USER CODE END 7 */
return result;
}
基本逻辑是判断TxState(发送状态标志)看发送是否处于进行中,如果目前没有发送任务,才给USB外设传入发送任务,源码里我没有找到完成任务后清除这个标志的内容,不知道有没有大佬懂,这里暂且跳过。
从这个逻辑中可以看出来一点,就是USB发送是非阻塞的,USB执行发送任务和核心的计算可以同步进行,因此做一个测试,在循环中输出有规律的字符,由于循环输出字符的运算速度极快,发现输出的字符有大量丢失,说明确实是非阻塞的。
接下来就是测速。
从HAL库里提取代码,重写一个发送函数:
void USBD_Send(uint8_t* Buf, uint16_t Len)
{
extern USBD_HandleTypeDef hUsbDeviceFS;
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
while(hcdc->TxState != 0) //USB繁忙时,阻塞等待
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
/* Tx Transfer in progress */
hcdc->TxState = 1U;
/* Update the packet total length */
hUsbDeviceFS.ep_in[CDC_IN_EP & 0xFU].total_length = hcdc->TxLength;
/* Transmit next packet */
USBD_LL_Transmit(&hUsbDeviceFS, CDC_IN_EP, hcdc->TxBuffer,(uint16_t)hcdc->TxLength);
//udelay(Len*20);//20
}
然后是发送一个512字节的缓存区,并利用SysTick测试发送耗时
extern USBD_HandleTypeDef hUsbDeviceFS;
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
uint8_t pf_buf[512];
memset(pf_buf, 0x30, 512);
//int i = 0;
uint32_t check_time;
check_time = SysTick->VAL;
//while(i<1024){
USBD_Send(pf_buf, 512);
// i++;
//}
while(hcdc->TxState != 0);
ALL_printf("\r\n%d\r\n",check_time-SysTick->VAL);
得到输出的计数为20000~22000,本次我使用的SysTick频率为2097kHz即发送一包512字节数据耗时约10ms,每秒100包,传输速度最高50KB/s
去掉 // 后测试循环发送1024包,,结果发现前几包速度正常,后面出现明显卡顿,最终发送1024包耗时约40秒,速度只有12KB/s左右。经过反复实验,我怀疑是多次阻塞等待造成的影响,于是我在发送后加上延时:
void udelay(uint16_t us)
{
uint32_t wait_time = 0UL;
uint32_t check_time = 0UL;
uint32_t start_time = 0UL;
uint32_t delay_time = 0UL;
start_time = SysTick->VAL;
delay_time = SystemCoreClock;
delay_time = delay_time / 1000000UL;
delay_time = delay_time * us;
while (wait_time < delay_time) {
check_time = SysTick->VAL;
if (check_time > start_time) {
wait_time += SysTick->LOAD - check_time + start_time;
} else {
wait_time += start_time - check_time;
}
start_time = check_time;
}
}
void USBD_Send(uint8_t* Buf, uint16_t Len)
{
extern USBD_HandleTypeDef hUsbDeviceFS;
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
while(hcdc->TxState != 0) udelay(Len*5);
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
/* Tx Transfer in progress */
hcdc->TxState = 1U;
/* Update the packet total length */
hUsbDeviceFS.ep_in[CDC_IN_EP & 0xFU].total_length = hcdc->TxLength;
/* Transmit next packet */
USBD_LL_Transmit(&hUsbDeviceFS, CDC_IN_EP, hcdc->TxBuffer,(uint16_t)hcdc->TxLength);
udelay(Len*20);//延时发送长度x20us,512*20约10ms
}
发送512字节*1024包的耗时成功缩短到10~11s,接近50KB/s的传输速度。奇怪的是,TxState是存在于RAM内的变量,我无法理解为什么反复访问这个变量会影响到USB发送的速度,而且在前几包发送时很明显并没有受到影响。
综上,考虑到USB是非阻塞传输,想达到最大速度,如果下一包发送数据准备的时间小于上一包传输的时间,最好添加一个延时补足时间差,以求达到最大速度,如过一包数据准备的时间大于传输时间(例如用vsnprintf进行字符串生成),则需要尽可能缩短数据准备耗时。
如过还有其他方法及观点欢迎讨论补充。
——————
24-06-25 更新:
在新板上实验发现,另一设计的pcb(MCU为L151CC)能达到最高500KB的传输速度,同时为移植方便,单独编写了一个USB应用层驱动,代码如下:
.h文件:
#ifndef __USBPORT_H
#define __USBPORT_H
#include "main.h"
extern uint16_t BYTE_us;
extern uint32_t wait_cnt;
uint8_t USER_USB_Init(void);
uint8_t USBD_Send(uint8_t* Buf, uint16_t Len);
#endif
.c文件:
#include "USBport.h"
#include "usb_device.h"
#include "usbd_core.h"
#include "usbd_cdc.h"
#include "usbd_cdc_if.h"
#include "delay.h"
#include "serial.h"
#include "commands.h"
#include "transfer.h"
extern uint8_t UserRxBufferFS[APP_RX_DATA_SIZE];
extern USBD_HandleTypeDef hUsbDeviceFS;
extern USBD_CDC_ItfTypeDef USBD_Interface_fops_FS;
static uint16_t Last_len = 10;
uint16_t BYTE_us = 2;
uint32_t wait_cnt;
static int8_t USB_Receive_FS(uint8_t* Buf, uint32_t *Len);
uint8_t USER_USB_Init(void)
{
USBD_Interface_fops_FS.Receive = USB_Receive_FS;
//USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);//如果要修改接收缓存区,调用这个函数
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return 0;
}
//极限1M
uint8_t USBD_Send(uint8_t* Buf, uint16_t Len)
{
if(Len==0) return 0;
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
wait_cnt = 0;
do{
udelay(Last_len*BYTE_us);
if(wait_cnt >> 14) return 1;
wait_cnt ++;
}while(hcdc->TxState != 0);
if(wait_cnt > 2) BYTE_us <<= 1;
else if(wait_cnt < 2) BYTE_us = (BYTE_us>>1)|0x1;
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
/* Tx Transfer in progress */
hcdc->TxState = 1U;
/* Update the packet total length */
hUsbDeviceFS.ep_in[CDC_IN_EP & 0xFU].total_length = hcdc->TxLength;
/* Transmit next packet */
USBD_LL_Transmit(&hUsbDeviceFS, CDC_IN_EP, hcdc->TxBuffer,(uint16_t)hcdc->TxLength);
Last_len = Len;
return 0;
}
static int8_t USB_Receive_FS(uint8_t* Buf, uint32_t *Len)//接收回调函数
{
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
//****此处自行插入接收后处理 例如:
memcpy(get_rxbuf(EPORT_USB), UserRxBufferFS, *Len);
set_rxlen(EPORT_USB, Len);
return (USBD_OK);
}
作者:乌龙三