stm32 USB虚拟串口

stm32 USB虚拟串口

通常情况下我习惯使用ch340或者cp2102这一类uart转usb芯片实现串口通信的功能,但是这意味着在电路设计时需要多花一个电路的原理图、占用更多的PCB面积以及更多的器件。最近在设计一块较小的电路板,由于板子上的空间有限,再加上刚好串口芯片用完了,不想再买,因此想要尝试一下使用STM32的USB虚拟串口来实现串口通讯的功能。故记录一下使用方法。

开发平台

软件:CubeMX、Clion

硬件:STM32ZET6核心板

CubeMX配置

首先使用Clion创建工程,创建方法可以参考这里。随后打开CubeMX,进行USB配置,配置参数如下所示:

配置GPIO,这两个引脚是板子上led灯的引脚,测试用,可以不用管。

然后配置时钟,高速时钟选择外部晶振。

选择DEBUG模式,这个和你的烧录器有关。

Connectivity中选择USB,勾选Device(FS)。

选择Middleware and Software Packs中的USB_DEVICE,然后选择Virtual Port Com

最后设置一下时钟

按照常规流程生成工程,注意这里在生成的时候把不同的初始化代码分开生成不同的文件。

功能实现

串口发送

打开main.c文件,在26行左右的位置,引用头文件#include "usbd_cdc_if.h",在这个头文件的104行处声明了发送函数CDC_Transmit_FS(),这个函数有两个参数。Buf是一个指针,指向待发送的数据,Len是待发送数据的长度。

因此,可以直接调用这个函数即可,比如在main()函数中写入

c 复制代码
CDC_Transmit_FS("Hello\r\n", 7);  
CDC_Transmit_FS("from VPC\r\n", 10);  
HAL_Delay(300);

如下图所示,注意代码尽量写在带有/* BEGIN *//* END */的注释中间,否则CubeMX重新生成代码时,会将这些代码删去。

将烧录器和usb连接至板子上正确的位置后,烧录。重新插拔一下usb。

注意usb数据线这时候接,直连STM32的就行了,不要接连串口芯片的那个引脚

打开串口调试助手,可以看到可以定时收到数据了。如果不知道是哪个端口的话,随便插拔一下,看一下哪个端口消失了就能确定,波特率啥的都不用设置。

但是可以发现,连续发送两行数据的时候,第二行消失不见了。

这并非上位机没有收到数据,实际上是因为函数CDC_Transmit_FS()的发送机制问题,当发送一个数据包时,需要间隔一段时间才能继续发送,连续调用发送函数会导致第二段数据无法发送出去。

因此,解决这个问题,最简单的方法就是在两个中间加一个延时。

c 复制代码
CDC_Transmit_FS("Hello\r\n", 7); 
HAL_Delay(1); 
CDC_Transmit_FS("from VPC\r\n", 10);  
HAL_Delay(300);

烧录后,重新插拔一下usb,打开串口助手,可以发现可以接收到第二行数据了。

优化发送功能

在刚才的demo中,我们虽然可以顺利发送数据了,但是还是存在两个不优雅的地方。

  1. 每次烧录后都需要重新插拔一下USB才能重新被电脑识别。
  2. 连续调用函数CDC_Transmit_FS()时都需要添加一个延时函数。

为了解决这两个问题,我们需要对几个函数做一些修改。

首先是第一个问题,我们需要通过模拟插拔 来解决,即在电路上电后,通过将usb接口中的D+(PA12)引脚拉低一段时间,来模拟usb拔出的动作。

我们在main()函数的while()前面可以看到几个初始化函数,其中有MX_USB_DEVICE_Init();,跳转到这个函数的定义处。

可以看到,这个函数调用了很多配置函数,一旦某个配置出现问题,就会跳转到错误的句柄函数。

我们在这些函数的调用之前,加入以下代码:

c 复制代码
__HAL_RCC_GPIOA_CLK_ENABLE();  
GPIO_InitTypeDef GPIOInitStructure = {0};  
GPIOInitStructure.Mode = GPIO_MODE_OUTPUT_PP;  
GPIOInitStructure.Pin = GPIO_PIN_12;  
GPIOInitStructure.Speed = GPIO_SPEED_FREQ_HIGH;  
GPIOInitStructure.Pull = GPIO_PULLDOWN;  
HAL_GPIO_Init(GPIOA, &GPIOInitStructure);  
HAL_Delay(5);

这段代码的功能就是将PA12引脚设置成GPIO后拉低,并让它延时一段时间,保证上位机识别到usb拔出。

随后程序便正常对usb进行初始化,上位机就能识别到了。

这里同样要注意将代码写在/* BEGIN *//* END */的注释中间。

对于第二个问题,我们来需要改写一下CDC_Transmit_FS()函数。

跳转到函数CDC_Transmit_FS()的定义,如下

导致连续发送失败的语句,已经用红框框出。当TxState不为0,即不空闲的时候,那么就直接返回USBD_BUSY,导致后面的发送函数没执行。因此,我们需要注释掉这几行代码,改写成忙时等待,超时才返回错误的功能。实现代码如下:

c 复制代码
uint32_t timeStart = HAL_GetTick(); // 获取当前时间  
while (hcdc->TxState) {  
    if (HAL_GetTick() - 10 > timeStart) {   // 等待超时  
        return USBD_BUSY;  
    }  
}

最终,这个函数被改写为

这次删去main()中的HAL_Delay(1);,编译后烧录。这次我们直接就能打开串口并接收到完整的数据了。

串口接收

当usb接收到数据后,stm32会触发中断,进而调用回调函数static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)

该函数拥有两个参数:

  • uint8_t *Buf:指向缓存区的指针,记录缓存区地址
  • uint32_t *Len:记录当前数据包的字节数

因此,我们直接在回调函数中对这两个参数进行处理就实现串口的接收了。

当然了,为了保证回调函数的实时性,不宜在回调函数中执行特别费时的逻辑,回调函数的执行时间应当尽可能的短。

所以我们采用以下方法:

  1. 在回调函数中将缓存区的内容读出来,清空缓存区
  2. 在主函数中对读取的内容进行进一步的逻辑处理

首先,在usbd_cdc_if.c 中相应的位置(约97行)定义全局变量,作为接收数据的储存区

c 复制代码
uint8_t myUsbRxData[64] = {0};  
uint16_t myUsbRxLen = 0;

然后在回调函数中加入以下代码:

c 复制代码
memset(myUsbRxData, 0, 64); // 清空存储区  
memcpy(myUsbRxData, Buf, *Len); // 将数据从缓存区中读到存储区  
myUsbRxLen = *Len;  // 复制字节数  
memset(Buf, 0, 64);  // 清空缓冲区

回调函数定义在函数CDC_Transmit_FS()定义位置的上方,usbd_cdc_if.c 中约258行的位置。

再在main.h中对刚才的两个变量进行外部调用

c 复制代码
extern uint8_t myUsbRxData[];  
extern uint16_t myUsbRxLen;

最后在main()函数的主循环中写处理逻辑:

c 复制代码
if (myUsbRxLen) {  
    static char myStr[100] = {0};  
    sprintf(myStr, "\r\n收到 %d 个字节:\r内容为:%s\r\n", myUsbRxLen, (char *) myUsbRxData);  
    CDC_Transmit_FS((uint8_t *) myStr, strlen(myStr));  
    myUsbRxLen = 0;  
}

这里注意一下,由于是在发送代码的基础上修改的,因此我最后重新调整了一下原来的代码。

最后main()函数中的内容为:

c 复制代码
int main(void) {  
  
    /* USER CODE BEGIN 1 */  
  
    /* USER CODE END 1 */  
    /* MCU Configuration--------------------------------------------------------*/  
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();  
  
    /* USER CODE BEGIN Init */  
  
    /* USER CODE END Init */  
    /* Configure the system clock */    
    SystemClock_Config();  
  
    /* USER CODE BEGIN SysInit */  
  
    /* USER CODE END SysInit */  
    /* Initialize all configured peripherals */    
    MX_GPIO_Init();  
    MX_USB_DEVICE_Init();  
    /* USER CODE BEGIN 2 */  
  
    /* USER CODE END 2 */  
    /* Infinite loop */    /* USER CODE BEGIN WHILE */    
    while (1) {  
        /* USER CODE END WHILE */  

        /* USER CODE BEGIN 3 */        
        HAL_Delay(1);  
        if (myUsbRxLen) {  
            static char myStr[100] = {0};  
            sprintf(myStr, "\r\n收到 %d 个字节:\r内容为:%s\r\n", myUsbRxLen, (char *) myUsbRxData);  
            CDC_Transmit_FS((uint8_t *) myStr, strlen(myStr));  
            myUsbRxLen = 0;  
        }  
        static uint16_t sendTime = 0;  
        if (++sendTime >= 2000) {  
            sendTime = 0;  
            CDC_Transmit_FS("Hello\r\n", 7);  
            CDC_Transmit_FS("from VPC\r\n", 10);  
        }  
    }  
    /* USER CODE END 3 */  
}

最后的效果如下:

注意事项

由于功能限制,cdc类的设备一次性最多只能接收64个字节的数据,如果想要接收更长的数据,需要在回调函数中增加一些代码,才能实现完整接收。

具体可参考如何让CDC类USB设备批量接收64字节以上数据 - STM32团队 ST意法半导体中文论坛

相关推荐
Wave8457 小时前
基于 STM32 + ESP8266 + W25Q64 的双核 OTA 底层架构总结
stm32·嵌入式硬件·架构
xiangw@GZ8 小时前
WiFi 全世代(WiFi1~WiFi7)技术规范与核心参数总结
嵌入式硬件
振南的单片机世界8 小时前
CPU时钟:频率越高跑越快,但物理极限在“拖后腿”
stm32·单片机·嵌入式硬件
普中科技10 小时前
【普中 51-Ai8051 开发攻略】-- 第 20 章 输入捕获实验
单片机·嵌入式硬件·输入捕获·pca·普中科技·ai8051u·aicube
d111111111d10 小时前
直流电机位置式 PID 控制 和 舵机的区别
笔记·stm32·单片机·嵌入式硬件·学习
d111111111d12 小时前
了解Modbus
网络·笔记·stm32·单片机·嵌入式硬件·学习
三佛科技-1341638421212 小时前
主控FT32F031便携式吸尘器方案,迷你手持吸尘器MCU控制方案开发
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
一个平凡而乐于分享的小比特13 小时前
一文读懂MCU与FPGA:核心区别、协同之道与双修秘籍
单片机·fpga开发·职场发展·mcu开发
踏着七彩祥云的小丑14 小时前
嵌入式——认识电子元器件——微动开关系列
单片机·嵌入式硬件
调光IC-小雅14 小时前
解析FP62××系列限流保护机制:为何它是DC/DC芯片的安全底线
单片机·嵌入式硬件