STM32 USB CDC VPC

news2024/11/15 19:33:33

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://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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/623797.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

钉钉H5微应用基础学习

钉钉开发文档 一、使用调试工具——IDE&#xff1a; 1、先下载调试工具&#xff0c;并且新建一个企业内部应用。 如果需要管理员权限&#xff0c;可以自己创建一个企业。 &#xff08;tips&#xff1a;一定要屏蔽自己创建的企业的消息&#xff0c;不然消息很多&#xff09; 2…

Burpsuite超详细安装教程

概述 Burp Suite 是用于攻击web 应用程序的集成平台&#xff0c;包含了许多工具。Burp Suite为这些工具设计了许多接口&#xff0c;以加快攻击应用程序的过程。所有工具都共享一个请求&#xff0c;并能处理对应的HTTP 消息、持久性、认证、代理、日志、警报。 接下来我来给大…

Linux4.8Nginx Rewrite

文章目录 计算机系统5G云计算第六章 LINUX Nginx Rewrite一、Nginx Rewrite 概述1.常用的Nginx 正则表达式2.rewrite和location3.location4.实际网站使用中&#xff0c;至少有三个匹配规则定义5.rewrite6.rewrite 示例 计算机系统 5G云计算 第六章 LINUX Nginx Rewrite 一、…

看完一位毕业的拼多多“P9”级别员工以及他的四页半简历,我悟了

前几天在脉脉上看到一个热帖&#xff0c;是刚从PDD毕业的P9级别员工吴可发的&#xff0c;同时附上了他的简历&#xff0c;这个简历很有意思&#xff0c;基本上和国内互联网这十多年来的发展步骤重叠&#xff0c;能够反映出&#xff0c;在这样一个跌宕起伏的时代里&#xff0c;个…

一次有关 DNS 解析导致 APP 慢的问题探究

一、业务背景 HTTTPDNS AWS Router53 APP 使用 HTTPDNS&#xff0c; 为解决 DNS 解析生效慢&#xff0c; DNS 劫持等问题。 我们 IOS 和安卓都是使用了 HTTPDNS。 域名托管在 AWS Router53。 域名有多个解析(基于延迟)&#xff0c;为了解决就近接入。 示例配置 ai.baidu.c…

网易Java后端面经(一面)

这是网易的Java一面&#xff0c;问的都很基础。 1.session过期怎么处理&#xff1f; session过期通常指用户在一段时间内没有进行任何操作而导致session失效。针对这种情况&#xff0c;可以采取以下措施&#xff1a; 1. 前端提示用户session即将过期&#xff0c;提醒其重新登录…

JavaScript对象的增强知识

Object.defineProperty ◼ 在前面我们的属性都是直接定义在对象内部&#xff0c;或者直接添加到对象内部的&#xff1a;  但是这样来做的时候我们就不能对这个属性进行一些限制&#xff1a;比如这个属性是否是可以通过delete删除的&#xff1f;这个属性是否在for-in遍历的时候…

微信能取代对讲机吗?区别在哪?

对讲机和微信的区别在哪&#xff1f;为什么大家在通讯方面选择对讲机而不是微信&#xff1f; 微信作为社交软件在多个领域都有着广泛的应用&#xff0c;不过在对讲机行业也在讨论一个话题&#xff1a;微信能否取代对讲机&#xff1f;下面河南宝蓝小编就和大家聊聊这个话题。 …

基于redis实现秒杀并防止超卖

基于redis实现秒杀并防止超卖 为什么基于redis针对秒杀商品库存为一个的情况setnx代码实现测试 针对有多个库存的商品实现测试 为什么基于redis 因为所有redis的操作&#xff08;这里指的是key的操作&#xff0c;像备份落盘之类的另算&#xff09;都是单线程的&#xff0c;所以…

一文读懂:LoRA实现大模型LLM微调

LoRA大模型LLM微调 为什么要进行微调&#xff1f;LoRA思路提高权重更新效率选择低的秩 实现LoRALoRA在LLaMA实现 为什么要进行微调&#xff1f; 在快速发展的人工智能领域中&#xff0c;以高效和有效的方式使用大型语言模型变得越来越重要。 预训练的大型语言模型通常被称为优…

02-启动 Vue 项目

一. 学习目标 掌握 Vue 项目的启动 二. 学习内容 掌握 Vue 项目的启动 三. 学习过程 项目的启动也有两种方式&#xff0c;一种是通过图形界面启动&#xff0c;另一种是通过命令行启动。 1.图形界面 打开vscode编辑器&#xff0c;点击 1.文件 ——>打开文件夹&#xff0c…

springboot实现支付宝支付(沙箱环境)

springboot实现支付宝支付 1. 获取应用id,应用私钥和支付宝公钥2. 开始开发3. 内网穿透4. 测试支付功能 1. 获取应用id,应用私钥和支付宝公钥 进入支付宝控制台:https://open.alipay.com/develop/manage 找到沙箱 这里可以看到应用id 可以看到应用私钥和支付宝公钥,获取这…

Zoho:集成ChatGPT、开发大型语言模型,加紧布局AI+SaaS

在企业的数字化转型进程中&#xff0c;管理层和员工的数字化意识会随着建设的推进而不断提高&#xff0c;对于办公场景的数字化应用需求也不断产生。传统的办公系统建设中&#xff0c;系统的应用能力需要支撑越来越丰富的场景需求。 《今日人工智能》采访到Zoho中国VP兼SaaS事业…

【编程语言 · C语言 · for语句】

for 语句 C语言中&#xff0c;使用for语句也可以控制一个循环&#xff0c;并且在每一次循环时修改循环变量。在循环语句中&#xff0c;for语句的应用最为灵活&#xff0c;不仅可以用循环次数已经确定的情况&#xff0c;而且可以用于循环次数不确定而只给出循环结束条件的情况。…

jenkins构建pipline无法执行shell命令原因

问题表现 新的服务器上&#xff0c;新安装的jenkins&#xff0c;在上面创建了一个pipline项目&#xff0c;脚本里有shell命令&#xff0c;但是jenkins每次执行都卡住&#xff0c;经过尝试&#xff0c;无论多简单的命令都执行不了&#xff0c;cp&#xff0c;mv等都不行&#xf…

华为路由器:ospf协议三张表及邻居建立过程

说明&#xff1a;本篇接上一篇继续讲解 拓扑图 为了方便&#xff0c;我把R1/2/3/4/5的router id改成了回环网卡的IP。 ospf协议三张表 邻居表&#xff08;neighbortable&#xff09; OSPF用邻居机制来发现和维持路由的存在&#xff0c;邻居表存储了双向通信的邻居关系OSPF路…

矩形图:数据之美在图形中展现

在当今信息爆炸的时代&#xff0c;数据已经成为决策和洞察的重要基石&#xff0c;但海量的数据如果不经过整理和呈现&#xff0c;往往难以得出有意义的结论。这时候&#xff0c;可视化工具的作用就变得尤为重要了。在众多可视化形式中&#xff0c;矩形图以其简洁直观的特点受到…

团队管理之性能实施团队日志9

最近项目进入胶着状态。 混合场景在有些项目组里已经可以开始了&#xff0c;但还是有两三个项目组现在是完全没办法混合起来的。 本周计划是把基准测试、容量测试跑完&#xff0c;稳定性测试每个项目组至少能跑一遍。 但是从进度上来看&#xff0c;容量测试至少有四个系统不能跑…

各类动态路由协议汇总简介

目录 一、前言 二、OSPF协议 &#xff08;一&#xff09;OSPF是什么 &#xff08;二&#xff09;OSPF的工作原理 &#xff08;三&#xff09;OSPF的特点 &#xff08;四&#xff09;OSPF的使用 &#xff08;五&#xff09;OSPF的优点 &#xff08;六&#xff09;总结 …