您现在的位置是:首页 >学无止境 >STM32 USB CDC VPC网站首页学无止境
STM32 USB CDC VPC
STM32 USB CDC VPC
关键字
STM32,STM32CubeMX,HAL库,USB,虚拟串口,串口不定长接收
1.简介
通过使用stm32cubemx,实现USB CDC虚拟串口,并与硬件串口进行数据传输,实现了硬件串口数据的不定长接收,以及USB虚拟串口超过64字节的数据接收,最终实现了一个简单的USB转串口功能。
使用USB的CDC类来虚拟出一个串口与电脑进行通信,可以省去硬件转换电路,同时由于通信使用USB,速度比硬件串口快。ST针对使用CDC虚拟串口有非常完备的代码支持,几乎是到手即用,本文简单介绍一下如何快速使用USB CDC虚拟串口。
2.使用CubeMX生成工程
本次使用的芯片为STM32F407VET6
首先配置好时钟,debug接口,然后使能测试用的硬件串口,使能串口的接收和发送dma,全部默认即可。
注意一定要打开串口的全局中断,否则串口收发会出问题。
然后开启USB
USB分为全速和高速两种模式,一般芯片只支持全速模式,高速模式需要外接PHY芯片才能实现,全速模式的最高理论速度是12Mbit/s,高速模式的最高理论速度是480Mbit/s,这里只使用了全速模式。
然后打开中间件的选项,选择USB驱动,在USB类选项里面选择CDC类,虚拟串口。
此时直接生成工程运行,连接电脑就可以检测到虚拟出来的串口(我使用的是win11,旧版本的系统可能需要装驱动才能检测到)。
这个串口就是USB虚拟出来的串口。
3.代码分析
简单使用虚拟串口需要更改的只有这一个文件
主要只涉及这4个函数
int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length)
是主机进行一些控制的回调函数
int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
是完成一个USB包接收的回调函数
uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)
是进行USB发送的函数
static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)
是USB发送完成的回调函数
需要注意的是CDC_Receive_FS
是一个回调函数,从机接收USB数据不需要启动,在USB初始化之后就会自动接收,一个包接收完成就会自动调用这个函数,因此需要在这个函数中实现对接收到的数据的处理。
3.1 USB接收代码
进行USB数据的接收需要改写CDC_Receive_FS
函数。
定义的全局变量
uint8_t Rx_Buffer[Rx_Buffer_Len];
__IO uint8_t DataReceive_Flag;
uint32_t SinglePackLength;
uint8_t *p_TempBuf;
uint8_t Num_Packet;
uint32_t Rx_Data_Len;
/**
* @brief Data received over USB OUT endpoint are sent over CDC interface
* through this function.
*
* @note
* This function will issue a NAK packet on any OUT packet received on
* USB endpoint until exiting this function. If you exit this function
* before transfer is complete on CDC interface (ie. using DMA controller)
* it will result in receiving more data while previous ones are still
* not sent.
*
* @param Buf: Buffer of data to be received
* @param Len: Number of data received (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
p_TempBuf = Buf;
SinglePackLength = *Len;
if (SinglePackLength == CDC_DATA_FS_MAX_PACKET_SIZE)
{
memcpy((Rx_Buffer + (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet)), Buf, SinglePackLength);
Rx_Data_Len = (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet) + SinglePackLength;
Num_Packet++;
}
else
{
memcpy((Rx_Buffer + (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet)), Buf, SinglePackLength);
Rx_Data_Len = (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet) + SinglePackLength;
DataReceive_Flag = 1;
Num_Packet = 0;
}
return (USBD_OK);
/* USER CODE END 6 */
}
进行USB接收数据的处理需要了解到一个USB通信的特性,全速USB一个包最大只能有64字节数据,而高速USB一个包可以有512字节。可以从usbd_cdc.h
文件中查看到定义。
在接收到一个数据包之后就会调用一下CDC_Receive_FS
函数,如果主机发送过来的数据没有超过64字节,那么一切正常,可以在回调函数中获得数据的缓存位置以及数据长度,但是如果主机发送的数据长度大于64字节,就会分成多个包发送过来,而回调函数还是一个包调用一次,如果每次回调都认为传输结束,就会丢掉后面的数据,所以必须进行处理。
如果接收到的包长度正好是64字节,那么认为后面还会有包传输过来,不产生接收完成标志,同时将接收到的数据复制到数据缓存,如果接收到的数据包长度不是64,那么认为数据接收完成,产生完成标志,复制数据到缓存区。主程序判断接收完成后,进行下一步的处理。
上述的操作基本可以完成数据的接收,但是有一个问题,如果主机发送的数据刚刚好就是64字节,那么就会一直等待下一个不满64字节的包,无法产生接收完成标志。解决这个问题可以加一个接收延时判断,例如,超过1ms没有数据包接收就认为数据接收结束了,在每次满包的状态进入到回调函数中就重新设置定时器的值,重新开始计时,在计时器中断中进行接收完成标志设定,这样在超时之后就可以实现数据的处理。上述思路提供一个解决办法,但是我并没有实现,单独为了整64字节进行这样的操作有些浪费资源,如果用途不是如此特殊,完全可以在主机发数据时注意一下,不发整64字节倍数的数据就好了。
我实现的小demo是简单的USB转串口,因此在判断接收数据完成之后,主程序利用DMA讲接收到的数据通过串口发送出去即可。
3.2 串口接收不定长数据
在已知通信协议的情况下,每次串口接收的数据长度是已知的,给发送的数据报增加特定的包头包尾进行数据包识别就可以实现串口数据的接收。但是做USB转串口的话,需要接收的数据长度是未知的,需要进行一些特殊处理。这里选择了一种办法是利用串口的空闲中断进行数据的接收。
在接收空闲时,可以产生空闲中断,从而可以判断数据接收完毕。
开启空闲中断接收HAL库没有像正常接收一样有个函数,HAL库只提供了宏定义的方式。
通过下面的定义开启空闲中断。
/** @brief Enable the specified UART interrupt.
* @param __HANDLE__ specifies the UART Handle.
* UART Handle selects the USARTx or UARTy peripheral
* (USART,UART availability and x,y values depending on device).
* @param __INTERRUPT__ specifies the UART interrupt source to enable.
* This parameter can be one of the following values:
* @arg UART_IT_CTS: CTS change interrupt
* @arg UART_IT_LBD: LIN Break detection interrupt
* @arg UART_IT_TXE: Transmit Data Register empty interrupt
* @arg UART_IT_TC: Transmission complete interrupt
* @arg UART_IT_RXNE: Receive Data register not empty interrupt
* @arg UART_IT_IDLE: Idle line detection interrupt
* @arg UART_IT_PE: Parity Error interrupt
* @arg UART_IT_ERR: Error interrupt(Frame error, noise error, overrun error)
* @retval None
*/
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)):
(((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)):
((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))
通过下面的方式清除中断标志位
/** @brief Clears the UART IDLE pending flag.
* @param __HANDLE__ specifies the UART Handle.
* UART Handle selects the USARTx or UARTy peripheral
* (USART,UART availability and x,y values depending on device).
* @retval None
*/
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)
实现过程首先在主函数开启串口接收和空闲中断。
HAL_UART_Receive_DMA(&huart1,uart_data,max_num);
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
之后在中断服务函数中增加对空闲中断的判断。
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
Usart1_Rec_Cnt = max_num - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_DMAStop(&huart1);
HAL_UART_Receive_DMA(&huart1,uart_data,max_num);
RECE_OK = 1;
}
/* USER CODE END USART1_IRQn 1 */
}
如果进入空闲中断,清除标志位,计算一下接收数据的长度,产生接收完成标志,重新开启串口接收。DMA接收的缓冲区设置可以大一些,这样在一般情况下就不会进行到串口DMA接收完成。因为只是一个小demo,没有考虑持续性大量接收数据的情况,只考虑几百字节一次的传输。如果是持续性的大量输出传输,可能还需要考虑缓冲区的设置等处理方式才可以。
在主程序中,判断串口接收标志,然后通过CDC_Transmit_FS
函数将数据通过USB发送出去。
3.3通过主机USB设置串口通信参数
在USB虚拟串口的通信中,主机和STM32是通过USB协议进行数据传输的,因此不需要设置波特率等数据即可进行通信,电脑的串口助手无需设置波特率即可正常通信,但是要实现USB转串口的功能,就必须将电脑串口助手设置的波特率信息同步到硬件串口上,此时需要利用CDC_Control_FS
函数。
/**
* @brief Manage the CDC class requests
* @param cmd: Command code
* @param pbuf: Buffer containing command data (request parameters)
* @param length: Number of data to be sent (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length)
{
/* USER CODE BEGIN 5 */
switch (cmd)
{
case CDC_SEND_ENCAPSULATED_COMMAND:
break;
case CDC_GET_ENCAPSULATED_RESPONSE:
break;
case CDC_SET_COMM_FEATURE:
break;
case CDC_GET_COMM_FEATURE:
break;
case CDC_CLEAR_COMM_FEATURE:
break;
/*******************************************************************************/
/* Line Coding Structure */
/*-----------------------------------------------------------------------------*/
/* Offset | Field | Size | Value | Description */
/* 0 | dwDTERate | 4 | Number |Data terminal rate, in bits per second*/
/* 4 | bCharFormat | 1 | Number | Stop bits */
/* 0 - 1 Stop bit */
/* 1 - 1.5 Stop bits */
/* 2 - 2 Stop bits */
/* 5 | bParityType | 1 | Number | Parity */
/* 0 - None */
/* 1 - Odd */
/* 2 - Even */
/* 3 - Mark */
/* 4 - Space */
/* 6 | bDataBits | 1 | Number Data bits (5, 6, 7, 8 or 16). */
/*******************************************************************************/
case CDC_SET_LINE_CODING:
VCP_Parameters.bitrate = (uint32_t)(pbuf[0] | (pbuf[1] << 8) | (pbuf[2] << 16) | (pbuf[3] << 24));
VCP_Parameters.format = pbuf[4];
VCP_Parameters.paritytype = pbuf[5];
VCP_Parameters.datatype = pbuf[6];
huart1.Init.BaudRate = VCP_Parameters.bitrate;
HAL_UART_Init(&huart1);
break;
case CDC_GET_LINE_CODING:
break;
case CDC_SET_CONTROL_LINE_STATE:
break;
case CDC_SEND_BREAK:
break;
default:
break;
}
return (USBD_OK);
/* USER CODE END 5 */
}
在电脑设置串口参数时会调用这个函数,根据cmd判断主机发送的命令,CDC_SET_LINE_CODING就是主机在进行通信参数的设置,上面的注释已经写明了pbuf
中的数据结构,前4个字节是波特率,然后是停止位,奇偶校验位和数据位数。获取到这个信息即可设置硬件串口的通信参数,这里只设置了波特率。
4.实验
左侧连接硬件串口,右侧连接USB虚拟串口
首先验证通过串口助手设置通信参数。
验证USB发送
可以观察右侧发送的字节数和左侧接收的字节数,没有丢包。
再验证串口发送
左右对比没有丢包,此时验证USB转串口小demo基本实现。
5.总结和疑问
USB通信非常复杂,如需深入了解还需要多多学习。
此外,在我将这个例程移植到H750上时,却工作不正常,整个代码的原理完全一样,在测试硬件串口接收时,只要进行了USB的初始化,没有其他操作,串口就无法进行正常的接收,不进行USB的初始化,串口就一切正常,目前还没有找到原因。
参考链接
https://shequ.stmicroelectronics.cn/thread-638862-1-1.html
https://blog.csdn.net/Naisu_kun/article/details/118192032
https://359303267.github.io/STM32_UART_TxRx_noDMA
https://blog.csdn.net/qq_33160790/article/details/78668950