STM32 USB开发详解:CDC虚拟串口与HID键盘鼠标(基于CubeUSB库)_基于stm32,usb键盘快速开发
前言:STM32的USB功能为何重要?
在嵌入式开发中,设备与外界通信的方式多种多样(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硬件基础与CubeUSB库简介
在开始实战前,我们需要先了解STM32 USB的硬件基础和开发工具,为后续开发铺路。
1.1 STM32 USB外设核心特性
STM32的USB外设(以最常用的USB 2.0 Full-Speed为例)具有以下特性:
- 速度:支持Full-Speed(12Mbps),部分型号(如F4、H7)支持High-Speed(480Mbps);
- 模式:支持从机(Device)、主机(Host)和OTG(On-The-Go)模式,本文聚焦从机模式;
- 端点:最多支持8个双向端点(Endpoint),用于数据传输(控制、批量、中断、同步传输);
- 低功耗:支持USB suspend模式,适合电池供电设备。
硬件电路注意: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(用于给外设供电)
1.2 CubeUSB库:简化USB开发的利器
传统USB开发需要手动编写设备描述符、配置描述符和状态机,门槛极高。ST推出的CubeUSB库(集成在STM32Cube生态中)通过以下方式简化开发:
- 封装底层协议:自动处理USB枚举、数据收发等复杂流程;
- 支持多种USB类:内置CDC、HID、MSC(存储)等标准类驱动;
- 与CubeMX无缝集成:通过图形化配置生成初始化代码,无需手动修改寄存器;
- 跨系列兼容:同一套API支持F1、F4、L4等多个STM32系列。
本文将基于STM32CubeMX 6.6.0和CubeUSB库 2.9.0,以STM32F103C8T6(最小系统板)为例,讲解CDC和HID类设备的开发。
二、USB CDC类:虚拟串口(Virtual COM Port)
CDC(Communications Device Class)是USB的一种标准设备类,其核心功能是将USB设备模拟为串口(即\"虚拟串口\"),使PC通过USB线与设备通信,就像使用传统UART串口一样。
2.1 CDC类的应用场景
CDC虚拟串口因其\"无需额外驱动(Windows自带)\"和\"即插即用\"的特性,广泛应用于:
- 调试信息输出(替代传统UART串口);
- 设备参数配置(如传感器校准、WiFi配置);
- 数据透传(如蓝牙模块与PC的USB桥接)。
2.2 CubeMX配置CDC设备步骤
步骤1:新建工程并选择芯片
打开STM32CubeMX,搜索并选择STM32F103C8T6
,点击\"Start Project\"。
步骤2:配置USB时钟
USB外设需要48MHz的专用时钟(USB clock),配置步骤:
- 点击\"RCC\",设置HSE为\"Crystal/Ceramic Resonator\"(外部晶振);
- 点击\"Clock Configuration\",配置系统时钟树:
- HSE = 8MHz;
- PLLMUL = ×9(PLL输出 = 72MHz);
- USB Prescaler = /1.5(使USB时钟 = 72MHz / 1.5 = 48MHz);
- 确保\"USB\"时钟分支显示为48MHz。
步骤3:配置USB从机模式
- 点击\"Connectivity\"→\"USB\",设置\"Mode\"为\"Device Only\"(从机模式);
- 勾选\"Activate the Full Speed Interface\",此时PA11(USB_DM)和PA12(USB_DP)会自动配置为USB功能。
步骤4:配置CDC类
- 点击\"Middleware\"→\"USB_DEVICE\",在\"Class For FS IP\"中选择\"Communication Device Class (CDC)\";
- 点击\"Configuration\",可修改CDC的默认参数(如厂商名称、产品名称):
- “Manufacturer String”:改为\"STM32\"(可选);
- “Product String”:改为\"STM32 CDC Virtual Port\"(可选);
- “Serial Number String”:改为\"0001\"(可选)。
步骤5:生成代码
点击\"Project Manager\",设置工程名称(如\"STM32_CDC\")和路径,选择IDE(如\"MDK-ARM V5\"),最后点击\"Generate Code\"生成工程。
2.3 CDC代码结构解析
生成的代码中,与CDC相关的核心文件和函数如下:
核心文件
Core/Src/usbd_conf.c
Core/Src/usbd_cdc_if.c
Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c
关键函数
-
初始化函数:
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);}
2.4 CDC实战:实现\"回声\"功能
目标:设备收到PC发送的字符串后,自动回传该字符串(即\"回声\"功能)。
步骤1:修改接收回调函数
在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);}
步骤2:主循环发送测试数据
在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++; }}
步骤3:编译下载与测试
- 编译工程并下载到STM32F103C8T6开发板;
- 用USB线连接开发板的USB口(注意:需连接到STM32的USB引脚,而非UART转USB);
- PC会自动识别\"STM32 CDC Virtual Port\",在设备管理器中查看虚拟串口编号(如COM3);
- 打开串口助手(如XCOM),设置波特率115200(CDC虚拟串口波特率无实际意义,任意值均可),可看到设备发送的测试字符串;
- 在串口助手发送任意字符串,设备会回传该字符串,实现\"回声\"功能。
2.5 CDC常见问题与解决方案
-
设备无法识别:
- 检查USB线是否为\"数据传输线\"(部分充电线无数据引脚);
- 确认PA11/PA12引脚连接正确,上拉电阻是否焊接;
- 检查USB时钟是否为48MHz(最常见问题)。
-
发送数据失败:
- 调用
CDC_Transmit_FS
前检查返回值,若为USBD_BUSY
,说明上一次发送未完成,需等待; - 单次发送数据长度不超过CDC端点缓冲区大小(默认512字节)。
- 调用
-
接收数据不完整:
- 确保
CDC_Receive_FS
中调用了USBD_CDC_ReceivePacket
(重新使能接收); - 长数据需在应用层处理分包(USB底层可能分多次接收)。
- 确保
三、USB HID类:键盘与鼠标
HID(Human Interface Device)是USB的另一重要设备类,专为人机交互设备设计,如键盘、鼠标、游戏手柄等。HID设备通过\"报告描述符\"定义数据格式,PC无需额外驱动即可识别。
3.1 HID类的核心特性
- 即插即用:Windows、Linux、MacOS均内置HID驱动;
- 低延迟:采用中断传输(Interrupt Transfer),确保实时响应;
- 灵活的数据格式:通过报告描述符自定义数据结构(如键盘扫描码、鼠标坐标)。
3.2 HID报告描述符:定义设备\"语言\"
HID设备与PC通信的核心是报告描述符(Report Descriptor),它告诉PC:
- 设备类型(键盘/鼠标/其他);
- 数据格式(如键盘有8个字节,包含 modifier 和扫描码);
- 数据范围(如鼠标X坐标范围-127~127)。
例如,简化的键盘报告描述符(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
3.3 CubeMX配置HID设备步骤
以\"键盘\"为例,配置步骤与CDC类似,核心差异在\"USB_DEVICE\"配置:
步骤1:配置USB时钟(同CDC)
确保USB时钟为48MHz,参考2.2节步骤2。
步骤2:配置HID类
- 点击\"Middleware\"→\"USB_DEVICE\",在\"Class For FS IP\"中选择\"HID Class\";
- 点击\"Configuration\",设置HID参数:
- “HID Usage Page”:选择\"Generic Desktop\"(通用桌面设备);
- “HID Usage”:选择\"Keyboard\"(键盘)或\"Mouse\"(鼠标);
- “HID Report Descriptor Size”:设置为报告描述符长度(如键盘为63字节);
- “HID Report Size”:设置单包报告大小(键盘8字节,鼠标3字节)。
步骤3:生成代码
同CDC,生成工程后,HID核心文件为usbd_hid.c
和usbd_hid_if.c
。
3.4 HID键盘开发实战
目标:STM32模拟键盘,按开发板上的按键(如PA0)时,向PC发送\"Hello World!\"。
步骤1:修改HID报告描述符
在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};
步骤2:定义键盘扫描码映射表
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
步骤3:实现键盘数据发送函数
在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);}
步骤4:主循环检测按键并发送数据
配置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); // 消抖 }}
步骤5:测试效果
- 下载程序到开发板,连接USB线到PC;
- PC会识别\"USB Input Device\"(键盘);
- 打开记事本,按下开发板上的PA0按键,会依次输入\"Hello World!\"并换行。
3.5 HID鼠标开发实战
鼠标与键盘的开发流程类似,核心差异在报告描述符和数据格式(鼠标通常为3字节:X位移、Y位移、按键状态)。
步骤1:鼠标报告描述符
__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};
步骤2:鼠标数据发送函数
/* 鼠标报告: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);}
步骤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会识别鼠标,鼠标指针会按上述逻辑移动并点击。
四、CubeUSB库核心API解析
无论是CDC还是HID,CubeUSB库的核心API都围绕\"USB设备状态机\"和\"端点数据传输\"设计,掌握这些API可灵活扩展功能。
4.1 USB设备状态机相关函数
USBD_Init()
USBD_RegisterClass()
USBD_Start()
USBD_Stop()
USBD_DeInit()
4.2 数据传输相关函数
USBD_CDC_TransmitPacket()
USBD_CDC_ReceivePacket()
USBD_HID_SendReport()
USBD_LL_Transmit()
USBD_LL_Receive()
4.3 回调函数
CubeUSB库通过回调函数通知应用层USB事件,常见回调包括:
USBD_ResetCallback()
:USB复位事件;USBD_SuspendCallback()
:USB挂起事件;USBD_ResumeCallback()
:USB唤醒事件;USBD_CDC_ReceiveCallback()
:CDC接收数据事件(自定义实现)。
五、HID与CDC的对比及选型建议
选型建议:
- 需传输大量数据(如日志、传感器数据) → 选CDC;
- 需模拟输入设备(如远程控制、自动化测试) → 选HID;
- 需跨平台兼容性(Windows/Linux/Mac) → 优先HID。
六、调试USB设备的实用工具
USB开发调试较复杂,推荐以下工具辅助定位问题:
-
USB设备树查看器(USB Device Tree Viewer):
- 功能:查看USB设备枚举过程、描述符、端点配置;
- 下载:https://www.uwe-sieber.de/usbtreeview_e.html。
-
HID调试工具(HID Terminal):
- 功能:监控HID设备发送的报告数据,手动发送HID报告;
- 下载:https://github.com/abcminiuser/hidterm。
-
串口助手(如XCOM、Putty):
- 功能:测试CDC虚拟串口的收发功能。
-
示波器/逻辑分析仪:
- 功能:测量USB_DP/USB_DM引脚的信号,判断硬件连接是否正常。
七、总结与扩展
本文详细讲解了STM32 USB的两种核心应用:
- CDC类:通过虚拟串口实现PC与设备的双向数据传输,配置简单,适合数据透传;
- HID类:通过报告描述符定义数据格式,模拟键盘、鼠标等输入设备,延迟低,适合人机交互。
扩展学习方向:
- 复合设备:同时支持CDC和HID(如带虚拟串口的游戏手柄);
- USB主机模式:STM32作为主机连接U盘、键盘(需用USB Host库);
- USB OTG模式:动态切换主机/从机模式(如手机与设备互传数据);
- 自定义HID设备:如旋钮、滑块等特殊输入设备,需编写对应的报告描述符。
CubeUSB库极大降低了USB开发的门槛,开发者无需深入理解USB协议细节,即可快速实现各类USB设备。建议从简单的CDC虚拟串口入手,熟悉后再挑战HID设备,逐步掌握STM32 USB开发的精髓。