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意法半导体中文论坛

相关推荐
学嵌入式的小杨同学3 小时前
STM32 进阶封神之路(四十一)FreeRTOS 中断管理、软件定时器、内存管理与低功耗模式|工业级实战完整版
vscode·stm32·单片机·嵌入式硬件·mcu·智能硬件·嵌入式实时数据库
蜕变的小白3 小时前
☆嵌入式硬件的学习 :51单片机 知识总结
单片机·嵌入式硬件·51单片机
yu85939584 小时前
基于MSP430 LaunchPad的蔬菜基地分布式无线低功耗温湿度监测系统
分布式·stm32·嵌入式硬件
学嵌入式的小杨同学4 小时前
STM32 进阶封神之路(四十)FreeRTOS 队列、信号量、互斥锁精讲|任务通信、同步、资源保护(超详细图文版)
c++·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
fie88891 天前
基于51单片机的航模遥控器6通道接收机程序
单片机·嵌入式硬件·51单片机
bu_shuo1 天前
嵌入式硬件工程师VS单板硬件工程师
嵌入式硬件·电子工程师·单板硬件
llilian_161 天前
选择北斗导航卫星信号模拟器注意事项总结 北斗导航卫星模拟器 北斗导航信号模拟器
功能测试·单片机·嵌入式硬件·测试工具·51单片机·硬件工程
Yyq130208696821 天前
MH2457,‌国产 32 位屏驱 MCU‌芯片,支持‌1080P 高清显示‌与‌以太网通信‌,广泛应用于两轮车仪表盘及工控屏等领域
单片机·嵌入式硬件
爱分享的阿Q1 天前
STM32现代化AI开发环境搭建:从Keil到VSCode+AI的范式转移
人工智能·vscode·stm32