STM32F407IGT6电机控制板 USB CDC虚拟串口收发

1.STM32cubeMX工程配置

时钟配置
使用USB推荐使用外部时钟,对于USB_FS而言其总线时钟一般为48MHz;
启用USB接口
接口中启用USB,这里仅作为从设备使用(Device_Only);
使用USB设备库
中间件中启用USB_DEVICE库,使用CDC类(Communication Device Class Virtual Port Com);

1.时钟配置

开启外部高速时钟,如果使用高速外部时钟HSE,则需要单片机PH0-OSC_IN和PH0-OSC_OUT两个引脚接入一4MHz~16MHz的陶瓷晶振,笔者开发板的高速晶振为8MHZ。

image-20250205174502232

SYS中Debug模式表示MCU使用的下载调试模式,一般烧录程序会使STLINK/DAP等,按照自己的模式选择即可,笔者这里开发板使用DAPLink下载器,因此选择Serial Wire即可

image-20250205174743959

时钟树配置如下

image-20250205180022966

2.启用USB接口

配置USB FS从机全速模式,最大速率12MBit/s

image-20250205175004868

3.使用USB设备库

IP Audio Device Class(IP音频设备类):这个类别定义了一种用于在IP网络上传输音频数据的设备。它允许音频设备通过IP协议与计算机或其他设备进行通信,从而实现音频数据的传输和控制。
IP Communication Device Class(IP通信设备类):这个类别定义了一种用于在IP网络上进行通信的设备。它提供了一套标准接口和协议,使设备能够与计算机或其他设备进行数据交换、实时通信和控制。
IP Download Firmware Update Class(IP固件下载更新类):这个类别定义了一种用于通过IP网络下载和更新设备固件的设备。它提供了一套标准协议和接口,使设备能够通过IP协议接收、存储和安装固件更新。
IP Human Interface Device Class(IP人机界面设备类):这个类别定义了一种用于通过IP网络连接人机界面设备的设备。它允许用户通过IP协议与设备进行交互和控制,例如通过远程访问控制计算机的鼠标、键盘或触摸屏等输入设备。
IP Custom Human Interface Device Class(IP自定义人机界面设备类):这个类别定义了一种用于通过IP网络连接自定义人机界面设备的设备。它提供了灵活的接口和协议,使设备能够与计算机或其他设备进行自定义的人机界面交互和控制。
IP Mass Storage Device Class(IP大容量存储设备类):这个类别定义了一种用于通过IP网络连接大容量存储设备的设备。它允许用户通过IP协议访问、传输和管理存储设备上的文件和数据,类似于传统的USB大容量存储设备(如U盘或移动硬盘)的功能。

此处选择IP Communication Device Class(IP通信设备类),用于实时数据交互。

image-20250205175338951

其他默认设置,生成代码。

2.Keil工程修改

上述配置生成的代码中,对于用户来说USB使用相关的代码都在 USB_DEVICE > App 中,这其中最重要的就是 usbd_cdc_if.c 文件,大多数时候我们只要改写这个文件就可以实现相关需求了,该文件主要结构与说明如下:

image-20250205182219525

上面代码中最常处理的只有下面四个函数:
CDC_Control_FS() 来自主机请求的回调函数
CDC_Receive_FS() 接收数据回调函数;
CDC_Transmit_FS() 用来发送数据;
CDC_TransmitCplt_FS() 发送完成回调函数

image-20250205182402129

在 CDC_Receive_FS 中添加了一行 CDC_Transmit_FS(Buf, *Len); 代码,实现了回环效果;

3.串口环回测试

不管上位机软件中波特率设置为多少都可以正常通讯,因为使用USB虚拟串口的时候真正数据传输用的是USB,串口本身参数这些已经无所谓了。

image-20250205182857209

4.通信延迟测试

在windows平台使用C++构建串口一收一发的延时测试程序,调用微软提供的串口类。测试请修改L"\\.\COM31"串口为自己的串口号。

代码如下:

#include <windows.h>
#include <stdio.h>
#include <string.h>

#define TEST_DATA "test"
#define TEST_COUNT 100

long long get_time_in_us() {
    LARGE_INTEGER frequency, counter;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&counter);
    return (counter.QuadPart * 1000000LL) / frequency.QuadPart;
}

int main() {
    HANDLE hSerial = CreateFile(L"\\\\.\\COM31", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (hSerial == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "打开串口失败\n");
        return 1;
    }

    // 配置串口
    DCB dcbSerialParams = { 0 };
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
    if (!GetCommState(hSerial, &dcbSerialParams)) {
        fprintf(stderr, "获取串口属性失败\n");
        CloseHandle(hSerial);
        return 1;
    }

    dcbSerialParams.BaudRate = CBR_115200;
    dcbSerialParams.ByteSize = 8;
    dcbSerialParams.StopBits = ONESTOPBIT;
    dcbSerialParams.Parity = NOPARITY;

    if (!SetCommState(hSerial, &dcbSerialParams)) {
        fprintf(stderr, "设置串口属性失败\n");
        CloseHandle(hSerial);
        return 1;
    }

    COMMTIMEOUTS timeouts = { 0 };
    timeouts.ReadIntervalTimeout = 50;
    timeouts.ReadTotalTimeoutConstant = 50;
    timeouts.ReadTotalTimeoutMultiplier = 10;
    timeouts.WriteTotalTimeoutConstant = 50;
    timeouts.WriteTotalTimeoutMultiplier = 10;
    SetCommTimeouts(hSerial, &timeouts);

    long long delays[TEST_COUNT] = { 0 };
    long long total_delay = 0, min_delay = 99999999, max_delay = 0;

    for (int i = 0; i < TEST_COUNT; i++) {
        DWORD bytesWritten, bytesRead;
        char buffer[16] = { 0 };

        long long start_time = get_time_in_us();
        WriteFile(hSerial, TEST_DATA, strlen(TEST_DATA), &bytesWritten, NULL);  // 发送数据
        ReadFile(hSerial, buffer, strlen(TEST_DATA), &bytesRead, NULL);         // 接收数据
        long long end_time = get_time_in_us();

        if (bytesRead > 0 && strncmp(buffer, TEST_DATA, strlen(TEST_DATA)) == 0) {
            long long delay = end_time - start_time;
            delays[i] = delay;
            total_delay += delay;
            if (delay < min_delay) min_delay = delay;
            if (delay > max_delay) max_delay = delay;
        }
        else {
            printf("第 %d 次测试接收数据失败\n", i + 1);
        }
    }

    CloseHandle(hSerial);

    double avg_delay = total_delay / (double)TEST_COUNT;
    printf("延时统计 (单位: us):\n");
    printf("最大延时: %lld us\n", max_delay);
    printf("最小延时: %lld us\n", min_delay);
    printf("平均延时: %.2f us\n", avg_delay);

    return 0;
}

测试100组收发数据,统计最大最小延时和平均延时:

image-20250205183548553

平均延时在200us以下,可以初步满足5KHZ的实时控制周期需求,后续将使用C++和simulink混合编程的方式实现simulink C mex s-function与STM32低延时实时通信。

参考:

【1】STM32 USB使用记录:使用CDC类虚拟串口(VCP)进行通讯https://blog.csdn.net/Naisu_kun/article/details/118192032

【2】Visual C++串口编程全解https://blog.csdn.net/weixin_29476595/article/details/144328738

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐