在嵌入式开发中,设备与外界通信的方式多种多样(UART、SPI、I2C等),但USB凭借"即插即用"、"高速传输"和"供电能力"三大优势,成为设备与PC/手机通信的首选方案。STM32大部分中高端型号(如F103C8T6、F407IGH6、L431RCT6等)都集成了USB外设,支持从机、主机或OTG模式,可实现虚拟串口、键盘、鼠标等多种功能。
本文聚焦STM32 USB的两种最常用类型:CDC类(虚拟串口) 和HID类(键盘/鼠标),基于ST官方的CubeUSB库(即STM32CubeUSB middleware),从硬件原理、CubeMX配置、代码解析到实战案例,手把手教你实现STM32与PC的USB通信。无论你是需要通过USB传输数据(CDC),还是模拟输入设备(HID),本文都能提供完整的解决方案。
在开始实战前,我们需要先了解STM32 USB的硬件基础和开发工具,为后续开发铺路。
STM32的USB外设(以最常用的USB 2.0 Full-Speed为例)具有以下特性:
硬件电路注意:STM32的USB引脚(通常为PA11(USB_DM)、PA12(USB_DP))需要外接1.5kΩ上拉电阻(DP引脚),部分型号(如F103)内置上拉电阻,可通过软件控制使能。典型电路如下:
PA12(USB_DP)→ 1.5kΩ电阻 → 3.3V(上拉)
PA11(USB_DM)→ 直接连接USB母座
USB母座外壳接地,VCC引脚可接5V(用于给外设供电)
传统USB开发需要手动编写设备描述符、配置描述符和状态机,门槛极高。ST推出的CubeUSB库(集成在STM32Cube生态中)通过以下方式简化开发:
本文将基于STM32CubeMX 6.6.0和CubeUSB库 2.9.0,以STM32F103C8T6(最小系统板)为例,讲解CDC和HID类设备的开发。
CDC(Communications Device Class)是USB的一种标准设备类,其核心功能是将USB设备模拟为串口(即"虚拟串口"),使PC通过USB线与设备通信,就像使用传统UART串口一样。
CDC虚拟串口因其"无需额外驱动(Windows自带)"和"即插即用"的特性,广泛应用于:
打开STM32CubeMX,搜索并选择STM32F103C8T6
,点击"Start Project"。
USB外设需要48MHz的专用时钟(USB clock),配置步骤:
点击"Project Manager",设置工程名称(如"STM32_CDC")和路径,选择IDE(如"MDK-ARM V5"),最后点击"Generate Code"生成工程。
生成的代码中,与CDC相关的核心文件和函数如下:
文件路径 | 功能描述 |
---|---|
Core/Src/usbd_conf.c |
USB设备配置(端点、缓冲区) |
Core/Src/usbd_cdc_if.c |
CDC接口实现(收发函数) |
Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c |
CDC类核心驱动 |
初始化函数:MX_USB_DEVICE_Init()
位于main.c
,用于初始化USB外设和CDC类,自动生成无需修改:
void MX_USB_DEVICE_Init(void)
{
/* 初始化USB设备库 */
USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
/* 添加CDC接口 */
USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
/* 初始化CDC接口 */
USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
/* 启动USB设备 */
USBD_Start(&hUsbDeviceFS);
}
发送数据函数:CDC_Transmit_FS()
位于usbd_cdc_if.c
,用于向PC发送数据(封装了USB端点发送逻辑):
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* 检查发送状态 */
if(USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData)
{
if(hcdc->TxState != 0) return USBD_BUSY;
/* 复制数据到发送缓冲区 */
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
/* 启动发送 */
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
}
return result;
}
接收回调函数:CDC_Receive_FS()
当PC通过虚拟串口发送数据时,该函数会被自动调用(需用户实现数据处理逻辑):
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* 用户添加数据处理逻辑 */
printf("收到PC数据:");
for(uint32_t i=0; i<*Len; i++)
{
printf("%02X ", Buf[i]);
}
printf("\r\n");
/* 重新使能接收(必须调用,否则只能接收一次) */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
}
目标:设备收到PC发送的字符串后,自动回传该字符串(即"回声"功能)。
在usbd_cdc_if.c
的CDC_Receive_FS
中添加回传逻辑:
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* 回传收到的数据 */
CDC_Transmit_FS(Buf, *Len);
/* 重新使能接收 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
}
在main.c
的主循环中添加周期性发送逻辑:
int main(void)
{
/* 初始化 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USB_DEVICE_Init();
/* 测试字符串 */
uint8_t test_buf[] = "Hello, CDC Virtual COM!\r\n";
uint32_t send_cnt = 0;
while (1)
{
/* 每1秒发送一次测试数据 */
if(send_cnt % 1000 == 0)
{
CDC_Transmit_FS(test_buf, sizeof(test_buf)-1);
}
HAL_Delay(1);
send_cnt++;
}
}
设备无法识别:
发送数据失败:
CDC_Transmit_FS
前检查返回值,若为USBD_BUSY
,说明上一次发送未完成,需等待;接收数据不完整:
CDC_Receive_FS
中调用了USBD_CDC_ReceivePacket
(重新使能接收);HID(Human Interface Device)是USB的另一重要设备类,专为人机交互设备设计,如键盘、鼠标、游戏手柄等。HID设备通过"报告描述符"定义数据格式,PC无需额外驱动即可识别。
HID设备与PC通信的核心是报告描述符(Report Descriptor),它告诉PC:
例如,简化的键盘报告描述符(8字节):
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xE0, // Usage Minimum (KB LeftControl)
0x29, 0xE7, // Usage Maximum (KB Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x03, // Input (Cnst,Var,Abs)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0xFF, // Logical Maximum (255)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0x00, // Usage Minimum (Reserved)
0x29, 0xFF, // Usage Maximum (FF)
0x81, 0x00, // Input (Data,Var,Abs)
0xC0, // End Collection
以"键盘"为例,配置步骤与CDC类似,核心差异在"USB_DEVICE"配置:
确保USB时钟为48MHz,参考2.2节步骤2。
同CDC,生成工程后,HID核心文件为usbd_hid.c
和usbd_hid_if.c
。
目标:STM32模拟键盘,按开发板上的按键(如PA0)时,向PC发送"Hello World!"。
在usbd_hid_if.c
中,替换默认报告描述符为键盘描述符:
__ALIGN_BEGIN uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xE0, // Usage Minimum (KB LeftControl)
0x29, 0xE7, // Usage Maximum (KB Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x03, // Input (Cnst,Var,Abs)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0xFF, // Logical Maximum (255)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0x00, // Usage Minimum (Reserved)
0x29, 0xFF, // Usage Maximum (FF)
0x81, 0x00, // Input (Data,Var,Abs)
0xC0 // End Collection
};
USB键盘通过"扫描码"(Scan Code)表示按键,常见字符的扫描码如下(完整表参考USB HID规范):
#define KEY_A 0x04
#define KEY_B 0x05
#define KEY_C 0x06
#define KEY_D 0x07
#define KEY_E 0x08
#define KEY_F 0x09
#define KEY_G 0x0A
#define KEY_H 0x0B
#define KEY_I 0x0C
#define KEY_J 0x0D
#define KEY_K 0x0E
#define KEY_L 0x0F
#define KEY_M 0x10
#define KEY_N 0x11
#define KEY_O 0x12
#define KEY_P 0x13
#define KEY_Q 0x14
#define KEY_R 0x15
#define KEY_S 0x16
#define KEY_T 0x17
#define KEY_U 0x18
#define KEY_V 0x19
#define KEY_W 0x1A
#define KEY_X 0x1B
#define KEY_Y 0x1C
#define KEY_Z 0x1D
#define KEY_SPACE 0x2C
#define KEY_ENTER 0x28
在usbd_hid_if.c
中添加发送键盘报告的函数:
/* 发送键盘报告(8字节):modifier(1字节) + reserved(1字节) + 6个扫描码 */
uint8_t HID_Keyboard_Send(uint8_t modifier, uint8_t* keys, uint8_t len)
{
uint8_t report[8] = {0};
report[0] = modifier; // modifier(如0x02表示Shift)
/* 填充扫描码(最多6个) */
for(uint8_t i=0; i<len && i<6; i++)
{
report[2+i] = keys[i];
}
/* 发送HID报告 */
return USBD_HID_SendReport(&hUsbDeviceFS, report, 8);
}
配置PA0为输入(上拉),检测按键按下时发送"Hello World!":
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USB_DEVICE_Init();
/* 按键引脚配置(PA0上拉输入) */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* 要发送的字符串:"Hello World!" */
uint8_t hello_keys[] = {
KEY_H, KEY_E, KEY_L, KEY_L, KEY_O, // "Hello"
KEY_SPACE, // 空格
KEY_W, KEY_O, KEY_R, KEY_L, KEY_D, // "World"
KEY_1, // "!"(Shift+1)
KEY_ENTER // 回车
};
uint8_t key_idx = 0;
uint8_t last_state = 1; // 按键初始状态(高电平)
while (1)
{
/* 检测按键(PA0)状态 */
uint8_t current_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
/* 按键按下(下降沿) */
if(current_state == 0 && last_state == 1)
{
if(key_idx < sizeof(hello_keys))
{
uint8_t modifier = 0;
/* 处理Shift键(如"!"需要Shift+1) */
if(hello_keys[key_idx] == KEY_1)
{
modifier = 0x02; // Left Shift
}
/* 发送当前字符 */
HID_Keyboard_Send(modifier, &hello_keys[key_idx], 1);
HAL_Delay(50); // 按键间隔
/* 发送释放状态(所有按键抬起) */
HID_Keyboard_Send(0, NULL, 0);
HAL_Delay(50);
key_idx++;
}
else
{
key_idx = 0; // 循环发送
}
}
last_state = current_state;
HAL_Delay(10); // 消抖
}
}
鼠标与键盘的开发流程类似,核心差异在报告描述符和数据格式(鼠标通常为3字节:X位移、Y位移、按键状态)。
__ALIGN_BEGIN uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (Button 1)
0x29, 0x03, // Usage Maximum (Button 3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x03, // Report Count (3)
0x81, 0x02, // Input (Data,Var,Abs)
0x75, 0x05, // Report Size (5)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Cnst)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data,Var,Rel)
0xC0, // End Collection
0xC0 // End Collection
};
/* 鼠标报告:3字节(X位移, Y位移, 按键) */
uint8_t HID_Mouse_Send(int8_t x, int8_t y, uint8_t buttons)
{
uint8_t report[3] = {x, y, buttons};
return USBD_HID_SendReport(&hUsbDeviceFS, report, 3);
}
int main(void)
{
// 初始化代码(略)
while (1)
{
/* 鼠标向右移动(X=10) */
HID_Mouse_Send(10, 0, 0);
HAL_Delay(50);
/* 鼠标向下移动(Y=10) */
HID_Mouse_Send(0, 10, 0);
HAL_Delay(50);
/* 鼠标向左移动(X=-10) */
HID_Mouse_Send(-10, 0, 0);
HAL_Delay(50);
/* 鼠标向上移动(Y=-10) */
HID_Mouse_Send(0, -10, 0);
HAL_Delay(50);
/* 左键点击(按下+释放) */
HID_Mouse_Send(0, 0, 0x01); // 左键按下
HAL_Delay(200);
HID_Mouse_Send(0, 0, 0x00); // 左键释放
HAL_Delay(1000);
}
}
测试时,PC会识别鼠标,鼠标指针会按上述逻辑移动并点击。
无论是CDC还是HID,CubeUSB库的核心API都围绕"USB设备状态机"和"端点数据传输"设计,掌握这些API可灵活扩展功能。
函数名 | 功能描述 |
---|---|
USBD_Init() |
初始化USB设备(设置描述符) |
USBD_RegisterClass() |
注册USB设备类(CDC/HID等) |
USBD_Start() |
启动USB设备(开始枚举) |
USBD_Stop() |
停止USB设备 |
USBD_DeInit() |
反初始化USB设备 |
函数名 | 功能描述 |
---|---|
USBD_CDC_TransmitPacket() |
CDC类发送数据 |
USBD_CDC_ReceivePacket() |
CDC类接收数据(使能端点) |
USBD_HID_SendReport() |
HID类发送报告 |
USBD_LL_Transmit() |
底层端点发送函数(通用) |
USBD_LL_Receive() |
底层端点接收函数(通用) |
CubeUSB库通过回调函数通知应用层USB事件,常见回调包括:
USBD_ResetCallback()
:USB复位事件;USBD_SuspendCallback()
:USB挂起事件;USBD_ResumeCallback()
:USB唤醒事件;USBD_CDC_ReceiveCallback()
:CDC接收数据事件(自定义实现)。特性 | CDC类(虚拟串口) | HID类(键盘/鼠标) |
---|---|---|
传输类型 | 批量传输(Bulk) | 中断传输(Interrupt) |
数据长度 | 最大512字节(可配置) | 最大64字节(HID规范限制) |
延迟 | 中等(依赖PC调度) | 低(1~10ms,中断传输保障) |
驱动需求 | Windows自带(无需额外驱动) | 全平台自带驱动 |
典型应用 | 数据透传、调试输出 | 人机交互设备(键盘、鼠标、手柄) |
开发复杂度 | 低(无需关心报告描述符) | 中(需正确定义报告描述符) |
选型建议:
USB开发调试较复杂,推荐以下工具辅助定位问题:
USB设备树查看器(USB Device Tree Viewer):
HID调试工具(HID Terminal):
串口助手(如XCOM、Putty):
示波器/逻辑分析仪:
本文详细讲解了STM32 USB的两种核心应用:
扩展学习方向:
CubeUSB库极大降低了USB开发的门槛,开发者无需深入理解USB协议细节,即可快速实现各类USB设备。建议从简单的CDC虚拟串口入手,熟悉后再挑战HID设备,逐步掌握STM32 USB开发的精髓。