STM32 USB CDC VPC

2023-11-13

STM32 USB CDC VPC

关键字

STM32,STM32CubeMX,HAL库,USB,虚拟串口,串口不定长接收

1.简介

通过使用stm32cubemx,实现USB CDC虚拟串口,并与硬件串口进行数据传输,实现了硬件串口数据的不定长接收,以及USB虚拟串口超过64字节的数据接收,最终实现了一个简单的USB转串口功能。

使用USB的CDC类来虚拟出一个串口与电脑进行通信,可以省去硬件转换电路,同时由于通信使用USB,速度比硬件串口快。ST针对使用CDC虚拟串口有非常完备的代码支持,几乎是到手即用,本文简单介绍一下如何快速使用USB CDC虚拟串口。

2.使用CubeMX生成工程

本次使用的芯片为STM32F407VET6

首先配置好时钟,debug接口,然后使能测试用的硬件串口,使能串口的接收和发送dma,全部默认即可。

image注意一定要打开串口的全局中断,否则串口收发会出问题。

image然后开启USB

imageUSB分为全速和高速两种模式,一般芯片只支持全速模式,高速模式需要外接PHY芯片才能实现,全速模式的最高理论速度是12Mbit/s,高速模式的最高理论速度是480Mbit/s,这里只使用了全速模式。

然后打开中间件的选项,选择USB驱动,在USB类选项里面选择CDC类,虚拟串口。

image此时直接生成工程运行,连接电脑就可以检测到虚拟出来的串口(我使用的是win11,旧版本的系统可能需要装驱动才能检测到)。

image

这个串口就是USB虚拟出来的串口。

3.代码分析

简单使用虚拟串口需要更改的只有这一个文件

image

主要只涉及这4个函数

image

int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length)​是主机进行一些控制的回调函数

int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)​是完成一个USB包接收的回调函数

uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)​是进行USB发送的函数

static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)​是USB发送完成的回调函数

需要注意的是CDC_Receive_FS​是一个回调函数,从机接收USB数据不需要启动,在USB初始化之后就会自动接收,一个包接收完成就会自动调用这个函数,因此需要在这个函数中实现对接收到的数据的处理。

3.1 USB接收代码

进行USB数据的接收需要改写CDC_Receive_FS​函数。

定义的全局变量

uint8_t Rx_Buffer[Rx_Buffer_Len];
__IO uint8_t DataReceive_Flag;
uint32_t SinglePackLength;
uint8_t *p_TempBuf;
uint8_t Num_Packet;
uint32_t Rx_Data_Len;

/**
 * @brief  Data received over USB OUT endpoint are sent over CDC interface
 *         through this function.
 *
 *         @note
 *         This function will issue a NAK packet on any OUT packet received on
 *         USB endpoint until exiting this function. If you exit this function
 *         before transfer is complete on CDC interface (ie. using DMA controller)
 *         it will result in receiving more data while previous ones are still
 *         not sent.
 *
 * @param  Buf: Buffer of data to be received
 * @param  Len: Number of data received (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
 */
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  p_TempBuf = Buf;
  SinglePackLength = *Len;
  if (SinglePackLength == CDC_DATA_FS_MAX_PACKET_SIZE)
  {
    memcpy((Rx_Buffer + (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet)), Buf, SinglePackLength);
    Rx_Data_Len = (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet) + SinglePackLength;
    Num_Packet++;
  }
  else
  {
    memcpy((Rx_Buffer + (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet)), Buf, SinglePackLength);
    Rx_Data_Len = (CDC_DATA_FS_MAX_PACKET_SIZE * Num_Packet) + SinglePackLength;
    DataReceive_Flag = 1;
    Num_Packet = 0;
  }

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

进行USB接收数据的处理需要了解到一个USB通信的特性,全速USB一个包最大只能有64字节数据,而高速USB一个包可以有512字节。可以从usbd_cdc.h​文件中查看到定义。

code在接收到一个数据包之后就会调用一下CDC_Receive_FS​函数,如果主机发送过来的数据没有超过64字节,那么一切正常,可以在回调函数中获得数据的缓存位置以及数据长度,但是如果主机发送的数据长度大于64字节,就会分成多个包发送过来,而回调函数还是一个包调用一次,如果每次回调都认为传输结束,就会丢掉后面的数据,所以必须进行处理。

如果接收到的包长度正好是64字节,那么认为后面还会有包传输过来,不产生接收完成标志,同时将接收到的数据复制到数据缓存,如果接收到的数据包长度不是64,那么认为数据接收完成,产生完成标志,复制数据到缓存区。主程序判断接收完成后,进行下一步的处理。

上述的操作基本可以完成数据的接收,但是有一个问题,如果主机发送的数据刚刚好就是64字节,那么就会一直等待下一个不满64字节的包,无法产生接收完成标志。解决这个问题可以加一个接收延时判断,例如,超过1ms没有数据包接收就认为数据接收结束了,在每次满包的状态进入到回调函数中就重新设置定时器的值,重新开始计时,在计时器中断中进行接收完成标志设定,这样在超时之后就可以实现数据的处理。上述思路提供一个解决办法,但是我并没有实现,单独为了整64字节进行这样的操作有些浪费资源,如果用途不是如此特殊,完全可以在主机发数据时注意一下,不发整64字节倍数的数据就好了。

我实现的小demo是简单的USB转串口,因此在判断接收数据完成之后,主程序利用DMA讲接收到的数据通过串口发送出去即可。

3.2 串口接收不定长数据

在已知通信协议的情况下,每次串口接收的数据长度是已知的,给发送的数据报增加特定的包头包尾进行数据包识别就可以实现串口数据的接收。但是做USB转串口的话,需要接收的数据长度是未知的,需要进行一些特殊处理。这里选择了一种办法是利用串口的空闲中断进行数据的接收。

在接收空闲时,可以产生空闲中断,从而可以判断数据接收完毕。

image

开启空闲中断接收HAL库没有像正常接收一样有个函数,HAL库只提供了宏定义的方式。

通过下面的定义开启空闲中断。


/** @brief  Enable the specified UART interrupt.
  * @param  __HANDLE__ specifies the UART Handle.
  *         UART Handle selects the USARTx or UARTy peripheral
  *         (USART,UART availability and x,y values depending on device).
  * @param  __INTERRUPT__ specifies the UART interrupt source to enable.
  *          This parameter can be one of the following values:
  *            @arg UART_IT_CTS:  CTS change interrupt
  *            @arg UART_IT_LBD:  LIN Break detection interrupt
  *            @arg UART_IT_TXE:  Transmit Data Register empty interrupt
  *            @arg UART_IT_TC:   Transmission complete interrupt
  *            @arg UART_IT_RXNE: Receive Data register not empty interrupt
  *            @arg UART_IT_IDLE: Idle line detection interrupt
  *            @arg UART_IT_PE:   Parity Error interrupt
  *            @arg UART_IT_ERR:  Error interrupt(Frame error, noise error, overrun error)
  * @retval None
  */
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)   ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)): \
                                                           (((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)): \
                                                           ((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))

通过下面的方式清除中断标志位

/** @brief  Clears the UART IDLE pending flag.
  * @param  __HANDLE__ specifies the UART Handle.
  *         UART Handle selects the USARTx or UARTy peripheral
  *         (USART,UART availability and x,y values depending on device).
  * @retval None
  */
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) __HAL_UART_CLEAR_PEFLAG(__HANDLE__)

实现过程首先在主函数开启串口接收和空闲中断。

	HAL_UART_Receive_DMA(&huart1,uart_data,max_num); 
	__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);

之后在中断服务函数中增加对空闲中断的判断。


/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
	{
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);
		Usart1_Rec_Cnt = max_num - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
		HAL_UART_DMAStop(&huart1);
		HAL_UART_Receive_DMA(&huart1,uart_data,max_num);
		RECE_OK = 1;
	}
  /* USER CODE END USART1_IRQn 1 */
}

如果进入空闲中断,清除标志位,计算一下接收数据的长度,产生接收完成标志,重新开启串口接收。DMA接收的缓冲区设置可以大一些,这样在一般情况下就不会进行到串口DMA接收完成。因为只是一个小demo,没有考虑持续性大量接收数据的情况,只考虑几百字节一次的传输。如果是持续性的大量输出传输,可能还需要考虑缓冲区的设置等处理方式才可以。

在主程序中,判断串口接收标志,然后通过CDC_Transmit_FS​函数将数据通过USB发送出去。

3.3通过主机USB设置串口通信参数

在USB虚拟串口的通信中,主机和STM32是通过USB协议进行数据传输的,因此不需要设置波特率等数据即可进行通信,电脑的串口助手无需设置波特率即可正常通信,但是要实现USB转串口的功能,就必须将电脑串口助手设置的波特率信息同步到硬件串口上,此时需要利用CDC_Control_FS​函数。

/**
 * @brief  Manage the CDC class requests
 * @param  cmd: Command code
 * @param  pbuf: Buffer containing command data (request parameters)
 * @param  length: Number of data to be sent (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
 */
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length)
{
  /* USER CODE BEGIN 5 */
  switch (cmd)
  {
  case CDC_SEND_ENCAPSULATED_COMMAND:

    break;

  case CDC_GET_ENCAPSULATED_RESPONSE:

    break;

  case CDC_SET_COMM_FEATURE:

    break;

  case CDC_GET_COMM_FEATURE:

    break;

  case CDC_CLEAR_COMM_FEATURE:

    break;

    /*******************************************************************************/
    /* Line Coding Structure                                                       */
    /*-----------------------------------------------------------------------------*/
    /* Offset | Field       | Size | Value  | Description                          */
    /* 0      | dwDTERate   |   4  | Number |Data terminal rate, in bits per second*/
    /* 4      | bCharFormat |   1  | Number | Stop bits                            */
    /*                                        0 - 1 Stop bit                       */
    /*                                        1 - 1.5 Stop bits                    */
    /*                                        2 - 2 Stop bits                      */
    /* 5      | bParityType |  1   | Number | Parity                               */
    /*                                        0 - None                             */
    /*                                        1 - Odd                              */
    /*                                        2 - Even                             */
    /*                                        3 - Mark                             */
    /*                                        4 - Space                            */
    /* 6      | bDataBits  |   1   | Number Data bits (5, 6, 7, 8 or 16).          */
    /*******************************************************************************/
  case CDC_SET_LINE_CODING:
        VCP_Parameters.bitrate = (uint32_t)(pbuf[0] | (pbuf[1] << 8) | (pbuf[2] << 16) | (pbuf[3] << 24));
      VCP_Parameters.format = pbuf[4];
      VCP_Parameters.paritytype = pbuf[5];
      VCP_Parameters.datatype = pbuf[6];
      huart1.Init.BaudRate = VCP_Parameters.bitrate;
      HAL_UART_Init(&huart1);

    break;

  case CDC_GET_LINE_CODING:

    break;

  case CDC_SET_CONTROL_LINE_STATE:

    break;

  case CDC_SEND_BREAK:

    break;

  default:
    break;
  }

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

在电脑设置串口参数时会调用这个函数,根据cmd判断主机发送的命令,CDC_SET_LINE_CODING就是主机在进行通信参数的设置,上面的注释已经写明了pbuf​中的数据结构,前4个字节是波特率,然后是停止位,奇偶校验位和数据位数。获取到这个信息即可设置硬件串口的通信参数,这里只设置了波特率。

4.实验

左侧连接硬件串口,右侧连接USB虚拟串口

image

首先验证通过串口助手设置通信参数。

在这里插入图片描述

验证USB发送

在这里插入图片描述

可以观察右侧发送的字节数和左侧接收的字节数,没有丢包。

再验证串口发送

​[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttiU0zh4-1686212378666)(https://assets.b3logfile.com/siyuan/1675829124924/assets/串口发送-20230608161207-4c9us9i.gif)]​

左右对比没有丢包,此时验证USB转串口小demo基本实现。

5.总结和疑问

USB通信非常复杂,如需深入了解还需要多多学习。

此外,在我将这个例程移植到H750上时,却工作不正常,整个代码的原理完全一样,在测试硬件串口接收时,只要进行了USB的初始化,没有其他操作,串口就无法进行正常的接收,不进行USB的初始化,串口就一切正常,目前还没有找到原因。

工程已上传
‍https://download.csdn.net/download/Master_0_/87890600


参考链接

https://shequ.stmicroelectronics.cn/thread-638862-1-1.html

https://blog.csdn.net/Naisu_kun/article/details/118192032

https://359303267.github.io/STM32_UART_TxRx_noDMA

https://blog.csdn.net/qq_33160790/article/details/78668950


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STM32 USB CDC VPC 的相关文章

  • Linux删除文件夹命令

    Linux删除文件夹命令 linux删除目录很简单 很多人还是习惯用rmdir 不过一旦目录非空 就陷入深深的苦恼之中 现在使用rm rf命令即可 直接rm就可以了 不过要加两个参数 rf 即 rm rf 目录名字 r 就是向下递归 不管有
  • milvus笔记01--部署测试版本 milvus

    milvus笔记01 部署测试版本milvus 1 milvus 简介 2 milvus cpu 部署 2 1 基于sqlite部署milvus 2 2 基于mysql部署milvus 3 常见命令 3 1 api 案例 3 2 RESTf
  • 使用 Github Action 将 github 仓库同步到 gitee

    背景 最近将 CI CD 流程改造了一波 使用 ArgoCD 做 gitops 这样所有的集群 Yaml 文件就都存放在了 github 上的一次仓库里 但是小服务器放在家里 从 github 上拉代码时总是时不时有网络问题 导致集群资源无

随机推荐

  • -1. HTML&CSS 基础总结

    HTML CSS Favorite 1 基础知识 1 HTML 1 1基本结构标签 1 骨架 2 排版标签 标题标签 h1 标题文本 h1 h1 gt h1 h6 段落标签 p 段落文本内容 p 水平线标签 hr 水平线 换行标签 br 换
  • Android 解析软件包时出现问题 -- Error staging apk from content URI

    Android Version 8 1 使用场景 在Rk3288w Android 8 1 的测试设备上安装 文件管理器 应用程序 若打开 apk文件 会出现 解析包错误 提示 即安装失败 影响使用 如下为ActivityManagerSe
  • 华为OD机试真题-找出重复代码【2023.Q1】

    题目描述 小明负责维护项目下的代码 需要查找出重复代码 用以支撑后续的代码优化 请你帮助小明找出重复的代码 重复代码查找方法 以字符串形式给出两行代码 字符审长度1 lt length lt 100 由英文字母 数字和空格组成 找出两行代码
  • 扩展磁盘大小

    各个系统可能会有些差异 主要存在于文件系统和卷组名上 一定要注意 如果要进行扩展大小的话 一定要先把原来的那个卷的数据进行保存好 数据 bin bash 使用这个脚本时 只需将第一个参数设置为想扩展多大即可 但是需要注意的是 若移植到位置的
  • java char长度_Java中char的字节数

    以前一直以为char占一个字节 后来发现远没这么简单 Java中char的字节数 和编码有关 使用UTF 8 英文字符占1个字节 中文占3个字节 下面在是在Ubuntu中测试的结果 public static void main Strin
  • 方差

    方差在概率统计中有很重要的作用 2公式 方差 方差是实际值与 期望值之差 平方的期望值 而 标准差是方差算术 平方根 1 在实际计算中 我们用以下公式计算方差 方差是各个数据与 平均数之差的平方的和的平均数 即 其中 x 表示 样本的平均数
  • 前缀树

    前缀树的结构 Trie树 又叫字典树 前缀树 Prefix Tree 单词查找树或键树 是一种多叉树结构 如下图 上图是一棵Trie树 表示了关键字集合 a to tea ted ten i in inn Trie树的基本性质 根节点不包含
  • 活动报名

    活动议程 日期 8月25日 周五 时间 主题 10 00 10 05 开场简介 马恺声 清华大学交叉信息研究院助理教授 青源会会员 10 05 10 50 基于商用硬件的同态加密加速 张明喆 中国科学院信息工程研究所信息安全国家重点实验室副
  • layui table直接编辑

    修改lay modules table js 在TPL HEADER中 if item2 type checkbox 复选框 修改为不显示标题行复选框 if item2 type checkbox item2 header undefine
  • 移动开发期末大作业-备忘录app

    备忘录app 资源链接在文末 前言 2022年软件工程专业上学期的一个安卓的课设 开发工具 androidStudio 开发语言 Java 介绍 这是一个备忘录APP 具有基本的备忘录功能和云端同步功能 实现备忘录功能的部分借鉴了xmenw
  • python接收易语言数据中文乱码

    易语言代码 book name 发送到发 txt 提交信息 引号 book name 引号 引号 编辑框 下载 内容 引号 到文本 网页 访问 对象 http 127 0 0 1 8000 download 1 提交信息 Content T
  • bootstrap click事件自动刷新页面问题

    1 将按钮的type类型改为button
  • Linux线程编程

    参考 Linux多线程编程初探 作者 峰子 仰望阳光 网址 https www cnblogs com xiehongfeng100 p 4620852 html 目录 线程概述 线程概念 线程与进程区别 为何用线程 线程开发api概要 线
  • 存储过程与控制结构

    存储过程与函数的区别 存储过程是没有返回值的函数 函数是有返回值的存储过程 创建存储过程 delimiter create procedure procedureName begin sql 语句 end delimiter 查看已有存储过
  • VUE.js

    VUE 1 1 概述 Vue 是一套前端框架 免除原生JavaScript中的DOM操作 简化书写 之前也学习过后端的框架 Mybatis Mybatis 是用来简化 jdbc 代码编写的 而 VUE 是前端的框架 是用来简化 JavaSc
  • STM32(HAL库)驱动st7789LCD屏幕(7引脚240*240)

    目录 1 简介 2 CubeMX初始化配置 2 1 基础配置 2 1 1 SYS配置 2 1 2 RCC配置 2 2 屏幕引脚配置 2 3 项目生成 3 KEIL端程序整合 3 1 LCD驱动添加 3 2 函数修改 3 2 1 lcd h修
  • pyqt5_tools下找不到designer.exe的问题

    pyqt tools 5 15 版本 designer exe在路径 Lib site packages qt5 applications Qt bin下
  • 第11讲:vue脚手架集成ElementUI

    一 创建vue路由项目并添加ElementUI支持 ElementUI官方网站 ElementUI组件 创建路由项目请参考 路由开发 使用如下命令集成ElementUI npm i element ui S 在src main js文件中引
  • MySQL日期函数

    MySQL日期函数 1 adddate 语法 adddate date interval expr unit 或 adddate expr days 用于给时间类型增加时间间隔 默认为天 unit year month day day ho
  • STM32 USB CDC VPC

    STM32 USB CDC VPC 关键字 STM32 STM32CubeMX HAL库 USB 虚拟串口 串口不定长接收 1 简介 通过使用stm32cubemx 实现USB CDC虚拟串口 并与硬件串口进行数据传输 实现了硬件串口数据的