STM32虚拟串口usb_printf函数及接收函数

官方库例程:

..\STM32Cube\Repository\STM32Cube_FW_F4_V1.23.0\Projects\STM324xG_EVAL\Applications\USB_Device\CDC_Standalone\MDK-ARM

环境:

STM32CubeMX   STM32F429IGT6  STlink

首先要确保硬件电路USB部分没问题;USB相关的概念知识大概需要了解一下,网上挺多这类文章的,自行百度。

STM32CubeMX 配置:

点击USB_OTG_FS,模式选择Device_Only,其他保持默认。

点击USB_DEVICE,选择IP 为VPC(虚拟串口),其他保持默认。

我使用的芯片是F429IGT6,最大时钟180MHz,但是USB时钟必须为48MHz(详情看STM32中文参考手册930页),180MHz是分频不出来48MHz的USB时钟,所以把系统配置成168MHz就能分频出48MHz的USB时钟。

堆空间需要改大一点,不然在USB插入电脑的时候,设备管理器会显示虚拟串口设备黄色感叹号

因为在USB插入电脑,STM32会创建一个实例,malloc申请内存,但内存不足的时候就失败了,驱动工作不正常了。

造成这一现象的原因:源码在usbd_cdc.c中: pdev->pClassData = USBD_malloc(sizeof (USBD_CDC_HandleTypeDef));

如果pdev->pClassData = NULL 即出现设备黄色感叹号。

最后生成代码即可,打开工程。

图中Application/User文件夹中多了几个文件:

usb_device.czhi只有一个USB设备函数初始化函数 MX_USB_DEVICE_Init()。

usb_conf.c是USB协议参数、IO初始化、中断回调函数、端点打开关闭停止操作等等函数。

usbd_cdc_if.c有虚拟串口的接收和发送等函数。

usb_desc.c有USB的描述符和USB枚举处理等。

文件夹Middlewares/USB_Device_Library是STM32Cube库。

检测VCP连接状态

在中断函数OTG_FS_IRQHandler

                         HAL_PCD_IRQHandler(&hpcd_USB_OTG_FS);

                                       HAL_PCD_ConnectCallback(hpcd);    //连接事件回调

                                       HAL_PCD_DisconnectCallback(hpcd);    //断开事件回调

发现两个连接状态事件回调函数,但是经过测试发现这两个回调函数根本不会发生。具体往下就没去深究了。

USBD_StatusTypeDef USBD_LL_DevDisconnected(USBD_HandleTypeDef  *pdev)
{
  /* Free Class Resources */
  pdev->dev_state = USBD_STATE_DEFAULT;
  pdev->pClass->DeInit(pdev, pdev->dev_config);  
  return USBD_OK;
}



void HAL_PCD_DisconnectCallback(PCD_HandleTypeDef *hpcd)
{
    USBD_LL_DevDisconnected((USBD_HandleTypeDef*)hpcd->pData);
}

但是偶然发现断开事件有改变一个变量pdev->dev_state = USBD_STATE_DEFAULT;

所以发现hUsbDeviceFS.dev_state的状态才是真正的连接状态标志位。

/*  Device Status */
#define USBD_STATE_DEFAULT                                1              //初始化状态
#define USBD_STATE_ADDRESSED                              2              //建立地址
#define USBD_STATE_CONFIGURED                             3              //配置完成,连接成功
#define USBD_STATE_SUSPENDED                              4              //usb挂起,断开成功

检测USB状态的函数

void VCP_Status(void)
{
    static uint8_t old_status = 0;

    if(hUsbDeviceFS.dev_state != old_status)
    {
        if(hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED)
            printf("连接成功\r\n");
        else if (hUsbDeviceFS.dev_state == USBD_STATE_SUSPENDED)
            printf("断开成功\r\n");
        old_status = hUsbDeviceFS.dev_state;
    }    
}

打印函数

写一个usb_printf打印函数,在usbd_cdc_if.c里面末尾USER CODE BEGIN及USER CODE END之间添加

/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */

#include <stdarg.h>

void usb_printf(const char *format, ...)
{
    va_list args;
    uint32_t length;
 
    va_start(args, format);
    length = vsnprintf((char *)UserTxBufferFS, APP_TX_DATA_SIZE, (char *)format, args);
    va_end(args);
    CDC_Transmit_FS(UserTxBufferFS, length);
}

/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */

关于接收有几点注意事项,认真看看。

1、usb虚拟串口每次接收最大的数据包ReceivePacket是64个字节;且每包数据以末尾追加  \r\n 表示一包数据接收完整。

当包长小于64个字节的时候

修改USB接收打印函数,在usbd_cdc_if.c里先找到static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)这个函数,

把它改成下图类似:

PC的usb虚拟串口收到MCU的数据通过UARTx发送回PC。演示一下发现什么问题:

//Len是每包数据的有效数据长度。值不会超过64
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    printf("%s\r\n", Buf);
    memset(Buf, 0, APP_RX_DATA_SIZE);
    return (USBD_OK);
}

可以看到第一次发送内容  01                  >>>     MUC收到的数据是:01 81 7F 04 0D 0A 。数据异常,多了中间的 81 7F 04。

可以看到第二次发送内容  01 02 03 04   >>>     MUC收到的数据是:01 02 03 04 0D 0A 。数据正常。

注意static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)的  *Len 是每包数据真实有效长度;

第一次发送的内容,*Len是等于1的;

第二次发送的内容,*Len是等于4的;

*Len最大不会超过64。

出现第一种情况的原因是:当包长小于64个字节的时候,数据会强制32bit对齐。也就是发的字节必须要是4的倍数,不够补够4的倍数,最后末尾加上\r\n表示包的完整。

为什么会32位对齐,下面是STM32的源码:

typedef struct
{
  uint32_t data[CDC_DATA_HS_MAX_PACKET_SIZE/4];      /* Force 32bits alignment */
  ...
  ...
}
USBD_CDC_HandleTypeDef;

当包长大于64个字节的时候

发现一共发送了306个字节。但是MCU收到了318个字节。318怎么来的?

其实每64个字节加\r\n2个字节,最后一包数据不够4倍数再补充够4的倍数。

306 / 64 = 4......50

4 * 13 >= 50

也就是4 * (64 + \r\n) + 52个字节 + \r\n = 318。

小结一下:

1、当数据小于等于64字节,那么进行4字节对齐,故真实长度以(*Len)为准。评论说加'\0'其实是使用%s打印知道结束符。

2、当数据大于64字节,由于VCP每包数据最大只能接收64字节,超过64字节部分会重新清空刷新缓存区接收。故需新建用户缓存所有数据,直至收到数据中的标记位才表示该帧数据结束。过程中的(*Len)数据长度也要累加起来。

2019.07.24改进,上传的源码没更新。

下面这个函数作用是避免我们每次下载复位后需要拨出USB再插上才能用。如果不行的话可以把HAL_Delay()延时加大一些

原理:和usb硬件相关。PC的usb内部两根数据线都接着下拉电阻,当检测任一个任一根数据线有高电平代表有设备接入初始化。下面代码就是模拟,上电把两个STM32的USB IO拉低,相当于手动断开USB线,然后进行MX_USB_DEVICE_Init()初始化的时候会正确初始化这两个IO。

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);

    /*Configure GPIO pin : W25Q256_CS_Pin */
    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(2);
}

使用方法:

在main()函数中的 SystemClock_Config()函数调用后使用USB_Status_Init();

VCP通讯走的是pack,故和串口调试助手的波特率无关。无论波特率多少,对数据无影响。

接收函数:(更新2020.10.09)

故修改代码:以接收到\r\n为结束

typedef struct{
  uint32_t rxlen;
  uint32_t flag;
}VcpRx_t;

VcpRx_t temp = {
    .rxlen =0,
    .flag = 0
};

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */   
    temp.rxlen = temp.rxlen + (*Len);
    
    if(temp.rxlen < APP_RX_DATA_SIZE && UserRxBufferFS[temp.rxlen - 2] != 0x0d  \
        &&  UserRxBufferFS[temp.rxlen - 1] != 0x0a)
    {
       //---继续接收---------------
       USBD_CDC_SetRxBuffer(&hUsbDeviceFS,UserRxBufferFS  + temp.rxlen); 
       USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    }
    else 
    {
       temp.flag = 1;    //接收完成
       UserRxBufferFS[temp.rxlen] = 0;   //20201009新增,添加/0,方便打印
    }

    return (USBD_OK);
  /* USER CODE END 6 */
}




//接收完成后通过UART打印出来
void rxdata_printf(void)
{
  if(temp.flag)
  {
    printf("%s\r\n", UserRxBufferFS);
    temp.flag = 0;
    temp.rxlen = 0;
    memset(UserRxBufferFS, 0, APP_RX_DATA_SIZE);
    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); 
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  }
}

main函数

int main(void)
{
    HAL_Init();

    SystemClock_Config();

    MX_GPIO_Init();
    MX_USB_DEVICE_Init();
    MX_USART2_UART_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */
    printf("sudaroot\r\n");

    while (1)
    {
        rxdata_printf();       
    }
}

附上测试代码:STM32VCP虚拟串口_stm32接收printf-其它代码类资源-CSDN下载

  全篇完。

本人是一个嵌入式未入门小白,博客仅仅代表我个人主观见解,记录成长笔记。
笔记是以最简单的方式,只展示最核心的原理。
若有与 大神大大 见解有歧义,我绝对坚信 大神大大 见解是对的,我的是错的。
若无积分等无法下载源码,可加入QQ群657407920下载交流经验。感谢~!

 

 

  • 30
    点赞
  • 124
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值