【STM32 USB】USB CDC类

news/2025/2/23 17:51:56

简介

USB CDC(communication device class)类是usb2.0标准下的一个子类,定义了通信相关设备的抽象集合。usb2.0标准下定义了很多子类,有音频类,CDC类,HID类,打印,大容量存储类,HUB和智能卡等等。

在这里插入图片描述

CDC类下根据应用场合,又分为多个子类,官方文档主要讲的是PSTN。PSTN(Public Switched Telephone Network)是一个与电信相关的子类,这里只是将其当作一个普通的通信设备使用,并没有使用它的一些电话特性。

使用USB中的CDC类来虚拟串口 Virtual COM Port (VCP)进行通讯是一种非常好用的方式,一方面对于上位机来说显示出来的就是一个串口,所有操作都还是对串口的操作;另一方面实际数据传输是基于USB的,数据传输速度得到大大提升。并且STM32 CDC VCP对于win10和较新版本的Linux来说是免驱的。

示例

使用STM32CubeIDE生成代码。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

将代码编译烧录后,我们可以看到多了一个串口设备。

在这里插入图片描述

其他说明

从USB版本来说目前STM32系列MCU可以认为都是USB2.0的。从硬件接口功能上来说STM32系列MCU的USB分为 USB_FS 、 USB_OTG_FS 、 USB_OTG_HS 三种。其中的FS指的是全速(Full Speed),HS指的是高速(High Speed)。OTG指的是既可以作为Device(从设备)使用,也可以作为Host(主机)使用。

Full Speed 理论上速度为12Mbit/s,High Speed 理论上速度为480Mbit/s ,当然这都是理论速度,实际上通讯速度还依赖于所用通讯方式和设备性能。

对于STM32系列MCU而言,USB FS的使用只要使用 DM / D- 和 DP / D+ 这两个引脚就行了,最多也就加上ID、SOF、VBUS这三个引脚。而使用USB HS大多数还需要外接PHY芯片(比如USB3300),这样使用的引脚就多了,至少也要用到12个引脚。STM32系列MCU中目前只有STM32F723内置USB HS PHY功能,不需要外接PHY芯片。

我们在上面的示例中,是外接了PHY芯片的,所以选择配置的时候是用的USB_HS。

用户代码分析

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

#include "usbd_cdc_if.h"

// 数据收发缓存,这部分也可以完全由用户自行定义
uint8_t UserRxBufferFS[APP_RX_DATA_SIZE]; // 接收缓存
uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; // 发送缓存

extern USBD_HandleTypeDef hUsbDeviceFS;

// 初始化USB_CDC
static int8_t CDC_Init_FS(void)
{
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); // 设置发送缓存
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); // 设置接收缓存
}

// 反初始化USB_CDC
static int8_t CDC_DeInit_FS(void){}

// 来自主机的请求处理
// cmd: 命令代码
// pbuf & length: 请求数据指针与长度
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length)
{
  switch(cmd)
  {
  /***********************************************************************************************/
  /* Line Coding Structure                                                                       */
  /*---------------------------------------------------------------------------------------------*/
  /* Offset | Field       | Size | Description                                                   */
  /* 0      | dwDTERate   |   4  | Data terminal rate, in bits per second                        */
  /* 4      | bCharFormat |   1  | Stop bits: 0 - 1 Stop bit; 1 - 1.5 Stop bits; 2 - 2 Stop bits */
  /* 5      | bParityType |   1  | Parity: 0 - None; 1 - Odd; 2 - Even; 3 - Mark; 4 - Space      */
  /* 6      | bDataBits   |   1  | Data bits (5, 6, 7, 8 or 16).                                 */
  /***********************************************************************************************/
    case CDC_SET_LINE_CODING: break; // 主机设置串口参数
  }
}

// 接收回调函数
// Buf & Len: 当前收到这一包数据指针与长度
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); // 重新设置接收缓存
  // 注意默认情况下上面一行代码相当于 USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS); // 重新启动数据接收
}

// 数据发送函数
// Buf & Len: 要发送的数据指针与长度
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
  USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
  if (hcdc->TxState != 0){
    return USBD_BUSY; // 如果当前USB繁忙则返回USBD_BUSY
  }
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); // 设置要发送的数据
  result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); // 发送数据
}

// 发送完成回调函数
// Buf & Len: 所送的数据指针与长度
static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum){}

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

发送

uint8_t  CDC_Transmit_FS ( uint8_t* Buf,  uint16_t Len );
static int8_t CDC_TransmitCplt_HS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)

我们需要发送数据的时候,需要调用CDC_Transmit_FS函数,该函数的Len参数好像并不会限制在2048。然后我们在发生完成函数中修改如下:

static int8_t CDC_TransmitCplt_HS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)
{
  uint8_t result = USBD_OK;
  /* USER CODE BEGIN 14 */
  printf("CDC_TransmitCplt_HS Len=%d\r\n", *Len);
  UNUSED(Buf);
  UNUSED(Len);
  UNUSED(epnum);
  /* USER CODE END 14 */
  return result;
}

在这里插入图片描述

我们可以看到,当一次性发送的数据包大于512的时候,发送函数内部会自动分包,直至发完,就会调用CDC_TransmitCplt_HS函数。接收端好像也会自动组包然后一次性收完?

还有一点需要注意,在发送函数内部,有一个设备状态判断,这意味着如果当前正在发送一包数据,然后你又调用该函数发送一包数据,是会发送失败的。可以理解为发送函数它是一个异步函数,数据真正发完的时候会调用CDC_TransmitCplt_HS函数。所以,我们在发送下一包数据的时候,需要确保上一包数据已经发送完成了,一般可以使用一个信号量进行阻塞之类的。

关于发送函数的总结:

  • 发送函数的数据长度可以大于一包USB数据的长度,函数内部会自动分包。
  • 发送函数是一个异步函数,调用CDC_TransmitCplt_HS函数的时候,数据才真正发完。

接收

当USB CDC接收到来自USB主机的数据时,触发中断进入中断函数,继而自动调用接收回调函数:

int8_t  CDC_Receive_FS (uint8_t* Buf, uint32_t *Len);  
  • uint8_t* Buf: 指向接收缓冲区的指针,即数据缓存的地址。
  • uint16_t* Len: 当前数据包的字节数。

我们将代码改为如下

static int8_t CDC_Receive_HS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 11 */
  printf("CDC_Receive_HS Len=%d\r\n",*Len);

  USBD_CDC_SetRxBuffer(&hUsbDeviceHS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceHS);
  return (USBD_OK);
  /* USER CODE END 11 */
}

然后使用串口助手往底板发送数据的结果如下:

在这里插入图片描述

可以看到,我们串口一次性发送了6868个字节的数据,在底板是分多次接收的。这是为什么呢?这是因为我们在配置USB CDC的时候,USB协议已经规定了一个USB包的数据最大就是512字节,这并不意味着超过512字节的数据帧就不能够发送给单片机,从上例就可以看出,USB协议会自动分包,然后单片机会有多次接收,只是一次接收不能接收所有完整的数据帧。所以,如果有大于512的数据帧时,我们可以在接收端手动组包,将多次接收的一个数据包组成完成的数据帧。组包的机制可以是定义帧头帧尾或者定义包序号,这里就不展开了。

还有一点示例可能看不出来,就是接收函数其实就是一个中断服务函数。所以,我们在该函数中的处理时间应该尽可能的短,因为在接收函数中进行耗时操作时,可能正好有一包数据需要接收,这时可能就会漏收。一般建议在该函数中接收数据时,将数据转存到其他的队列或者缓存空间。

关于接收函数的总结:

  • 如果上位机一次发送的数据帧大于USB最大包长度,那么在接收端需要注意组包。
  • 在接收函数中尽量不要进行耗时的操作,如需处理数据可以先转存到其他地方。

http://www.niftyadmin.cn/n/5863640.html

相关文章

stm32常见的存储器应用

常用 STM32 存储器芯片介绍和应用 STM32 微控制器通常与多种存储器芯片一起工作,以下是几种常见的存储器类型及其应用: 1. 闪存(Flash Memory) STM32 内部的 闪存 是一种非易失性存储器,广泛用于存储程序代码和常驻…

探索 Peewee:轻量级 Python ORM 简明指南

文章目录 探索 Peewee:轻量级 Python ORM 简明指南主要特点:安装:使用示例:1. 定义模型:2. 初始化数据库:3. 数据操作(增、查、改、删):4. 查询构建器:5. 迁移…

Python----PyQt开发(PyQt高级:手搓一个音乐播放器)

一、效果展示 二、设计PyQt界面 本次ui界面设置用到了水平和垂直布局 2.1、设置ui窗口显示大小与位置 self.setWindowTitle(音乐播放器) # 设置窗口标题self.setGeometry(800, 300, 800, 800) # 设置窗口大小和位置 2.2、创建显示歌曲列表控件 # 创建显示歌曲列表的控件 …

uniapp 整合openlayers 编辑图形文件并上传到服务器

引入openlayer依赖 import Map from ol/Map.js // OpenLayers的主要类,用于创建和管理地图 import View from ol/View.js // OpenLayers的视图类,定义地图的视图属性 import TileLayer from ol/layer/Tile.js// OpenLayers的瓦片图层类 import…

verilog中等难度设计实践与ALU设计

Verilog中等难度部分设计实践(含ALU算术逻辑单元的设计) verilog的中等部分根据Deepseek给出的大纲理应包括时序逻辑verilog设计实践和组合逻辑verilog组合设计实践两个部分。 时序逻辑verilog设计和实践 在 Verilog 中,时序逻辑通常通过 …

计算机考研复试上机07

14、数据结构 1)二叉树 1.常用操作 struct TreeNode{int data;TreeNode *leftChild;TreeNode *rightChild; }; //前序遍历 void PreOrder(TreeNode *root){if(root == NULL) return;visit(root->data);PreOrder(root->leftChild);PreOrder(root->rightChild);ret…

提效10倍:基于Paimon+Dolphin湖仓一体新架构在阿里妈妈品牌业务探索实践

1. 业务背景 阿里妈妈品牌广告数据包括投放引擎、下发、曝光、点击等日志,面向运筹调控、算法特征、分析报表、诊断监控等应用场景,进行了品牌数仓能力建设。随着业务发展,基于Lambda架构的数仓开发模式,缺陷日益突出:…

Windows 上编译 mebedtls 的鸿蒙库

mebedtls 地址:https://github.com/Mbed-TLS/mbedtls 准备工作: clone mebedtls 仓库到本地(tag: mbedtls-2.26.0)鸿蒙工具链(SDK version: v5.0.5) 编译文件修改: 对 CMakeLists.txt 进行修改,主要是关闭了以下几个选项 ENABLE_P…