参考教程
- 【STM32 + CubeMX】 USB 虚拟串口通信
主要参考这个,写的非常的详细,可以直接看这边文章,本文为指路 - stm32-USB使用记录(一)
- 嵌入式STM32 USB通信
步骤



编译无错
1、包含 USB接口 的头文件
打开 main.c文件,大约第26行,配对的 /* USER CODE ... Includes */ 注释之间,
添加:#include "usbd_cdc_if.h"
2、增加 USB模拟插拔
为了避免调试期间频繁地手动操作,我们可以在程序开跑后、USB初始化前,用代码把PA12置低,使D+线为低电平,持续一段时间,模拟USB拔出动作,令主机认为设备已断开连接,释放端口;
然后,当程序运行到后面的USB初始化函数时,PA12会被正常配置(DP线电平被置高),USB主机就会"发现"有设备插入,开始尝试枚举、配置;
具体操作:
打开 usbd_conf.c 文件,大约第70行附近,找到HAL_PCD_MspInit ( )函数;
在两对 /* USER ... 0 */ 注释之间,约75行,添加PA12引脚置低电平操作, 如下(可复制):
c
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA端口
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 声明结构体; 如果与文中位置相同,这行可不写
GPIO_InitStruct.Pin = GPIO_PIN_12; // 引脚PA12, 即D+
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 引脚工作模式
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 引脚反转速度
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化
HAL_Delay(5); // 持续片刻
3. 发送数据的函数
c
uint8_t CDC_Transmit_FS ( uint8_t* Buf, uint16_t Len );
函数接受两个参数:数据缓冲区的地址、字节数。
如果USB设备正忙,它会返回USBD_BUSY状态。
这个函数的作用是设置传输数据的缓冲区,并标记数据包为待发送。数据并非立刻发出,而是被存储在USB外设的缓冲区中,等待主机轮询请求传输。
1、接收方式的简述
当USB CDC接收到来自USB主机的数据时,触发中断进入中断函数,继而自动调用接收回调函数:
int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len);
uint8_t* Buf: 指向接收缓冲区的指针,即数据缓存的地址。
uint16_t* Len: 当前数据包的字节数。
我们就在这个回调函数里,处理接收到的数据!
它在 usbd_cdc_if.c 文件,位于发送函数的正上方;
2、接收示范
通过串口助手,发送字符串
STM32(设备端)收到数据后,把收到的字节数、字符串,发回串口助手显示(主机端)
在函数内的注释行 /* USER CODE BEGIN */ 下方,添加4行自定义代码(可复制)。
c
char myStr[64] = {0}; // 定义一个数组,用于存放要输出的字符串
sprintf(myStr, "\r\r收到 %d 个字节;\r内容是:%s\r\r", *Len, (char *)Buf); // 格式化字符串
CDC_Transmit_FS((uint8_t *)myStr, strlen(myStr)); // 发送
memset(Buf, 0, 64); // 处理完数据,清0接收缓存;
问题解决
1. 优化发送,能连续发送
先打开 CDC_Transmit_FS ( ) 函数,看看函数原型。
修改:
注释掉刚才 if 体的3行代码;
增加等待发送空闲、判断超时,如下6行;
整个CDC_Transmit_FS ( ) 函数,如下:
c
uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 7 */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData; // 获得设备的状态信息结构体
// if (hcdc->TxState != 0){
// return USBD_BUSY;
// }
uint32_t timeStart = HAL_GetTick();
while (hcdc->TxState)
{
if (HAL_GetTick() - timeStart > 20)
return USBD_BUSY;
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
/* USER CODE END 7 */
return result;
}
2.接收优化(在外部处理数据)
操作共4个步骤,具体如下:
1、增加全局变量
在usbd_cdc_if.c文件大约97行,配对的注释内,定义两个变量:
c
/* USER CODE BEGIN PRIVATE_VARIABLES */
uint8_t myUsbRxData[64] = { 0 }; // 接收到的数据
uint16_t myUsbRxNum = 0; // 接收到的字节数
/* USER CODE END PRIVATE_VARIABLES */
现在,它俩只是本地变量,等会要在main中用extern再声明一次,才能被外部调用。
2、修改接收回调函数
在CDC_Receive_FS() 里,删除我们上节增加的测试代码;
把Buf和*Len的数据,复制到我们刚才的两个变量里。函数修改成:
c
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
// 把Buf里面的数据,复制到外部缓存
memset(myUsbRxData, 0, 64); // 清0缓存区
memcpy(myUsbRxData, Buf, *Len); // 把接收到的数据,复制到自己的缓存区中
myUsbRxNum = *Len; // 复制字节数
memset(Buf, 0, 64); // 处理完数据,清0接收缓存;
// CubeMX生成的代码,保留
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); // 设置下-个接收缓冲区
USBD_CDC_ReceivePacket(&hUsbDeviceFS); // 启动下一个数据包的接收
return (USBD_OK);
/* USER CODE END 6 */
}
现在,数据接收部分,已处理好了。
以后回调函数运行时,只复制数据至外部缓存(备用),中断时间占用极短,不会影响下包接收。
3、 在外部用extern声明变量,令外部可调用数据
外部,哪个文件里要使用CDC接收的数据,就在这个文件里,用extern声明那俩变量。
如,可以在LCD文件,也可以在SD卡的文件中,都行。
建议在main.h文件中声明,其它文件再#include "main.h",这样,可以令变量全局可用。
打开 main.c,右击空白,点击"Toggle Header/Code File",可以跳转到头文件:main.h
在main.h中,大约38行,找到 配对的注释行 /* USER CODE BEGIN ET */
用 extern 再次声明刚才两个变量。如下(可复制):
注意,是只声明,不要赋值,否则编译错误。
c
/* USER CODE BEGIN ET */
extern uint8_t myUsbRxData[ ] ;
extern uint16_t myUsbRxNum ;
/* USER CODE END ET */
4、使用接收到数据
在main.c的while循环中,通过判断myUsbRxNum的值,只要大于0,就表示收到数据了
记得每次处理完数据,把myUsbRxNum置0,以便于下一轮的判断。

再次烧录,烧录程序。
打开串口助手,发送测试文本,可以发现,能成功收到STM32发过来的回传数据。
小结
本文主要记录本人使用usb的过程,具体直接看原博主的文章,更详细。