实现通过USB虚拟串口与PC正常通讯的功能
1 STM32CubeMX配置
1.1 USB接口配置
单片机做从机,所以Mode选择Device,其它设置保持默认即可
使能USB接口的中断:
设置IP,选择Virtual Port Com
其它设置项可保持不变,但要注意最下面两个设置项:Buffer Size,usbd_cdc_if.c中会根据这个值定义两个数组,请根据需要自行设置大小,也可以不使用库提供的buffer。
这两个数组为全局变量,尽量根据实际需要调整大小。
1.2 时钟配置
USB接口使用的时钟频率为48MHz
至此,配置完成,生成代码
2 编写测试程序
2.1 编写打印信息到USB虚拟串口的函数
方法一,编写打印函数:
#include <stdarg.h>
void usb_printf(const char *format, ...)
{
va_list args;
uint32_t length;
va_start(args, format);
length = vsnprintf((char *)UserTxBufferFS, 2048, (char *)format, args);
va_end(args);
CDC_Transmit_FS(UserTxBufferFS, length);
}
这里面的核心是CDC_Transmit_FS()函数,先用vsnprintf函数把要打印的数据整合到UserTxBufferFS数组(前面有定义大小)中,再调用CDC_Transmit_FS函数将数组中的数据发送出去。
方法二,在putc函数中实现发送功能:
int fputc(int ch, FILE *f)
{
uint8_t buf = (uint8_t)ch;
CDC_Transmit_FS(&buf, 1);
}
该方法不使用UserTxBufferFS数组,节省内存,并且程序中的打印函数可以继续使用printf,而不需要更改。
但用该方法测试时不成功,每次只会发送一个字符,后来找到原因,是fputc调用太快造成USB数据包堵塞,因为USB发送是分包进行的,包大小最小64字节,添加一个小延时即可解决:
int fputc(int ch, FILE *f)
{
uint8_t buf = (uint8_t)ch;
CDC_Transmit_FS(&buf, 1);
HAL_Delay(1);
}
但这样会导致程序执行效率变慢,最好还是采用方法一。
2.2 接收数据,以字符型数据为例
USB收到数据后,会调用usbd_cdc_if.c文件中的CDC_Receive_FS函数,因此我们需要修改该函数,处理接收到的数据,本人采用的FIFO方式保存数据
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
Buf[*Len] = 0;
PC_RX_Fifo->PushString((char *)Buf);
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
添加的代码只有前面两行,Buf[*Len]用于添加一个字符结束符,防止PushString保存出错。
PushString函数用于将Buf中的内容全部存入FIFO中,遇到0则停止,因此有效数据中不能有0。
至此,接收数据的功能也完成了。
2.3 处理接收到的数据
编写数据处理函数:
PopLine函数用于从FIFO中取出一行数据,需设置最多取出的字符数量,防止出错
char buffer[100]; //存放从FIFO中取出的数据
while (PC_RX_Fifo->Lines > 0) //串口接收缓冲区中有整行的数据
{
uint16_t len = PC_RX_Fifo->PopLine(buffer, sizeof(buffer));
if (len == 0) //数据长度为0,表示用户按下了回车键,发送笑脸符号
{
printf("@_@");
}
else if (MyMSH_ExecCmd(buffer) == 0) //未识别的命令
{
printf("NG,%s,Undefined command!\r\n@_@", buffer);
}
}
2.4 测试
先编写两个命令函数:
//命令,获取治具名称
void Fixture(char * Payload)
{
printf("OK,%s,%s\r\n@_@", __func__, Fixture_Name);
}
MSH_CMD_EXPORT(Fixture, Get fixture name);
//命令,读取固件版本
void FW_Ver(char * Payload)
{
printf("OK,%s,%s %s %s\r\n@_@", __func__, FW_Version, __DATE__, __TIME__);
}
MSH_CMD_EXPORT(FW_Ver, Get firmware version);
用PC串口助手发送命令,波特率可以随意设置:
测试几条命令,发现单片机都有正常回应:
3 解决每次下载完程序串口都丢失的问题
每次下载完程序串口都会丢失,单片机复位一次就好了,采用博主sudaroot的方法解决了该问题,原文链接: https://blog.youkuaiyun.com/sudaroot/article/details/86627853
原理:PC的usb内部两根数据线都接着下拉电阻,当检测任一个任一根数据线有高电平代表有设备接入初始化。
下面这个函数作用是上电时把两个USB IO拉低,相当于手动断开USB线,然后进行MX_USB_DEVICE_Init()初始化的时候会正确初始化这两个IO,避免我们每次下载复位后需要拨出USB再插上才能用。
static void USB_Status_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11 | GPIO_PIN_12, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//假如不行的话,下面的延时加长即可。
HAL_Delay(20);
}
使用方法:在main()函数中的 SystemClock_Config()函数之后调用USB_Status_Init();