简介:本资源是一份基于STM32F103C8T6芯片实现USB复合HID设备的完整工程实践包,面向嵌入式初学者与STM32进阶开发者,解决USB设备枚举、键盘鼠标双功能协同、HID报告描述符定制及HAL库底层驱动适配等典型难点。压缩包含626个文件,主体为345个C源码(含USB初始化、中断处理、HID报告构建逻辑)、114个头文件(定义设备描述符、端点配置与报告结构)、43个汇编启动文件及28个IAR链接脚本,整体7.58MB,工程已适配Keil MDK环境,含.uvprojx/.uvoptx项目文件及.axf可执行镜像。内容预览显示其包含完整的USB HID固件框架、ARM CMSIS-DSP基础函数支持模块(如DCT/RFFT初始化)及多版本备份文件,结构规范、注释清晰,便于理解复合设备通信机制与中断响应流程。目前已有3418人学习下载,适合用于课程设计、毕设开发或USB协议深度实践。
1. 为什么一个 USB 接口要塞进键盘 + 鼠标?不是“多此一举”,而是 HID 复合设备的硬需求
你手头那块 STM32F103C8T6(俗称“蓝 pill”)板子,USB 口只有一路 D+/D−,但项目要求:插上电脑后,同时被识别为一个标准键盘 + 一个标准鼠标——不是模拟成“带宏键的键盘”,也不是用鼠标滚轮触发键盘事件,而是 Windows 设备管理器里明确显示两条独立设备:
HID 键盘(支持全键无冲、多媒体键、LED 状态回传)HID 鼠标(支持 X/Y 位移、左右中三键、滚轮、加速度平滑处理)
这在工业 HMI 控制面板、嵌入式调试终端、定制化游戏外设甚至某些医疗设备交互模块中是真实存在的交付要求。很多同学一上来就翻 CubeMX 的 USB Device 模板,发现只有“HID Keyboard”或“HID Mouse”单选项,立刻卡死;也有人强行拼两个 CDC+HID,结果主机只认一个。根本问题不在芯片能力,而在HID 复合设备(Composite Device)的描述符组织逻辑——它不靠硬件多路复用,而靠一套精巧的、符合 USB-IF 规范的接口/端点/报告描述符嵌套结构。本方案就是从 CubeMX 生成骨架出发,手动补全复合设备描述符、重定向报告发送通道、隔离 HID 数据流路径,最终让一块 F103C8T6 稳稳跑起双 HID 设备。适合正在做毕设/课设、需要交完整可演示工程的同学,也适合想吃透 STM32 USB HID 底层机制的工程师。
2. 总体架构设计:单 USB 控制器 + 双 HID 类接口 + 独立报告通道的三层结构
STM32F103C8T6 的 USB 全速控制器(USB_FS)本身不支持物理多设备,但 USB 协议允许一个设备通过多个接口(Interface)向主机声明不同功能。复合设备的本质,就是在一个配置(Configuration)下,定义两个并列的 HID 接口:Interface 0(Keyboard)、Interface 1(Mouse),各自拥有独立的中断端点(IN Endpoint)和完整的 HID 报告描述符。CubeMX 默认只生成单接口,我们必须在生成代码基础上,分层改造:
2.1 USB 设备描述符与配置描述符的复合化重构
CubeMX 生成的usbd_desc.c中,USBD_DeviceDesc是固定的 18 字节设备描述符,无需改动;关键在USBD_CfgDesc—— 它定义了整个配置的拓扑。原始单 HID 配置长度约 41 字节,而复合设备需扩展为约 97 字节,核心变化如下:
/* usbd_desc.c 中 USBD_CfgDesc 数组(截取关键段) */ __ALIGN_BEGIN uint8_t USBD_CfgDesc[USB_CFG_DESC_SIZ] __ALIGN_END = { /* --- 配置描述符头部(不变)--- */ 0x09, /* bLength: Configuation Descriptor size */ USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */ LOBYTE(0x0063), HIBYTE(0x0063),/* wTotalLength: 99 bytes (0x63) */ 0x02, /* bNumInterfaces: 2 interfaces */ 0x01, /* bConfigurationValue: Configuration value */ 0x00, /* iConfiguration: Index of string descriptor */ 0xC0, /* bmAttributes: Self powered */ 0x32, /* MaxPower 100 mA */ /* --- Interface 0: Keyboard --- */ 0x09, /* bLength: Interface Descriptor size */ USB_DESC_TYPE_INTERFACE, /* bDescriptorType: Interface */ 0x00, /* bInterfaceNumber: Number of Interface */ 0x00, /* bAlternateSetting: Alternate setting */ 0x01, /* bNumEndpoints: One endpoint used */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: 1 = Boot Interface Subclass */ 0x01, /* bInterfaceProtocol: 1 = Keyboard */ 0x00, /* iInterface: Index of string descriptor */ /* HID Class-Specific Descriptor for Keyboard */ 0x09, 0x21, 0x10, 0x01, 0x21, 0x01, 0x22, LOBYTE(KEYBOARD_REPORT_DESC_SIZE), HIBYTE(KEYBOARD_REPORT_DESC_SIZE), /* Interrupt IN Endpoint for Keyboard */ 0x07, 0x05, 0x81, 0x03, 0x08, 0x00, 0x0A, /* EP1 IN, 8 bytes, interval 10ms */ /* --- Interface 1: Mouse --- */ 0x09, /* bLength: Interface Descriptor size */ USB_DESC_TYPE_INTERFACE, /* bDescriptorType: Interface */ 0x01, /* bInterfaceNumber: Number of Interface */ 0x00, /* bAlternateSetting: Alternate setting */ 0x01, /* bNumEndpoints: One endpoint used */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: 1 = Boot Interface Subclass */ 0x02, /* bInterfaceProtocol: 2 = Mouse */ 0x00, /* iInterface: Index of string descriptor */ /* HID Class-Specific Descriptor for Mouse */ 0x09, 0x21, 0x10, 0x01, 0x21, 0x01, 0x22, LOBYTE(MOUSE_REPORT_DESC_SIZE), HIBYTE(MOUSE_REPORT_DESC_SIZE), /* Interrupt IN Endpoint for Mouse */ 0x07, 0x05, 0x82, 0x03, 0x08, 0x00, 0x0A /* EP2 IN, 8 bytes, interval 10ms */ };设计说明:
wTotalLength必须精确计算所有描述符字节数(含接口、HID 类描述符、端点),差 1 字节主机就会拒绝枚举;bNumInterfaces改为0x02,这是复合设备的开关;- 两个接口的
bInterfaceNumber必须严格为0x00和0x01,顺序不能颠倒,否则 Windows 驱动加载错乱;- 键盘用 EP1(地址
0x81),鼠标用 EP2(地址0x82),物理端点必须分离,这是数据通道隔离的基础;bInterfaceProtocol分别为0x01(Keyboard)和0x02(Mouse),这是 HID 类驱动识别设备类型的关键字段。
2.2 HID 报告描述符的独立编写与尺寸对齐
CubeMX 不生成报告描述符,需手写keyboard_report_desc[]和mouse_report_desc[]。二者必须严格遵循 HID Usage Tables v1.12 规范,且尺寸(KEYBOARD_REPORT_DESC_SIZE/MOUSE_REPORT_DESC_SIZE)必须与配置描述符中0x22条目后的字节数完全一致。
键盘报告描述符(支持 6 键无冲 + LED 状态):
/* report_desc.h */ #define KEYBOARD_REPORT_DESC_SIZE 63 __ALIGN_BEGIN static uint8_t keyboard_report_desc[KEYBOARD_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 (Keyboard LeftControl) 0x29, 0xE7, // USAGE_MAXIMUM (Keyboard 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, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) 0xC0 // END_COLLECTION };鼠标报告描述符(X/Y 位移 ±127、三键、滚轮):
#define MOUSE_REPORT_DESC_SIZE 50 __ALIGN_BEGIN static uint8_t mouse_report_desc[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) 0x95, 0x03, // REPORT_COUNT (3) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 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) 0x09, 0x38, // USAGE (Wheel) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7F, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x06, // INPUT (Data,Var,Rel) 0xC0, // END_COLLECTION 0xC0 // END_COLLECTION };设计说明:
- 键盘描述符末尾的
OUTPUT项(LED 状态)是可选但强烈建议保留,否则无法响应主机下发的 NumLock/CapsLock 指令;- 鼠标
USAGE (Wheel)必须紧跟在 X/Y 后,且REPORT_COUNT为 1,否则 Windows 认为报告格式错误;- 所有
REPORT_SIZE和REPORT_COUNT的乘积必须等于该 Report ID 的总字节数(键盘 8 字节,鼠标 4 字节),这是主机解析报告的依据;- 描述符必须
__ALIGN_BEGIN/__ALIGN_END对齐,否则 USB DMA 传输会因地址未对齐导致枚举失败(F103 的 USB 外设对内存对齐极其敏感)。
2.3 USB 设备类层的数据路由与状态隔离
CubeMX 生成的usbd_hid.c默认只处理一个 HID 接口,我们需要将其升级为双接口调度器。核心在于修改USBD_HID_SendReport()和USBD_HID_EventCallback():
/* usbd_hid.c */ // 全局变量,存储两个接口的当前报告缓冲区 static uint8_t keyboard_report_buf[8] = {0}; static uint8_t mouse_report_buf[4] = {0}; // 修改 USBD_HID_SendReport:根据 interface_num 选择端点和缓冲区 uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t len, uint8_t interface_num) { if (interface_num == 0) { // Keyboard if (len != 8) return USBD_FAIL; memcpy(keyboard_report_buf, report, 8); return USBD_LL_Transmit(pdev, 0x81, keyboard_report_buf, 8); // EP1 IN } else if (interface_num == 1) { // Mouse if (len != 4) return USBD_FAIL; memcpy(mouse_report_buf, report, 4); return USBD_LL_Transmit(pdev, 0x82, mouse_report_buf, 4); // EP2 IN } return USBD_FAIL; } // 修改 USBD_HID_EventCallback:区分接口处理主机请求 static uint8_t USBD_HID_EventCallback(USBD_HandleTypeDef *pdev, uint8_t event, uint8_t interface_num) { switch (event) { case USBD_HID_GET_REPORT: if (interface_num == 0) { USBD_HID_GetPollingInterval(pdev, 0x81); // 返回键盘轮询间隔 } else if (interface_num == 1) { USBD_HID_GetPollingInterval(pdev, 0x82); // 返回鼠标轮询间隔 } break; case USBD_HID_SET_IDLE: // 主机设置空闲时间,按接口保存 if (interface_num == 0) keyboard_idle_rate = ...; else if (interface_num == 1) mouse_idle_rate = ...; break; case USBD_HID_SET_PROTOCOL: // 设置启动协议(Boot/Report),按接口保存 if (interface_num == 0) keyboard_protocol = ...; else if (interface_num == 1) mouse_protocol = ...; break; } return USBD_OK; }设计说明:
USBD_HID_SendReport()的interface_num参数由上层应用传入,调用者必须明确指定发给哪个设备(如USBD_HID_SendReport(&hUsbDeviceFS, key_data, 8, 0));- 两个报告缓冲区物理隔离,避免键盘按键事件误触发鼠标移动;
USBD_LL_Transmit()的端点地址(0x81/0x82)必须与配置描述符中定义的 IN 端点严格一致,否则数据发到错误通道;USBD_HID_EventCallback中的interface_num由 USB 栈自动提取自 Setup 包的wIndex字段,我们只需按需分支处理,这是复合设备能正确响应主机控制请求的关键。
3. 核心模块实现:键盘扫描矩阵 + 鼠标AD采样 + 双通道报告组装
硬件资源有限(F103C8T6 仅 20KB SRAM、64KB Flash),必须高效复用 GPIO 和 ADC。本方案采用4×4 矩阵键盘 + 单路 ADC 读取电位器模拟鼠标 X/Y 轴 + 按键模拟鼠标三键的低成本实现,所有逻辑在main.c中闭环。
3.1 4×4 矩阵键盘扫描:行扫描 + 列消抖 + 按键编码映射
使用 GPIOA 的 PA0–PA3 作为行输出(推挽),PA4–PA7 作为列输入(上拉),通过HAL_GPIO_WritePin()和HAL_GPIO_ReadPin()实现扫描。关键在消抖与防连击:
// keyscan.c #define KEY_ROWS 4 #define KEY_COLS 4 GPIO_TypeDef* row_ports[KEY_ROWS] = {GPIOA, GPIOA, GPIOA, GPIOA}; uint16_t row_pins[KEY_ROWS] = {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3}; GPIO_TypeDef* col_ports[KEY_COLS] = {GPIOA, GPIOA, GPIOA, GPIOA}; uint16_t col_pins[KEY_COLS] = {GPIO_PIN_4, GPIO_PIN_5, GPIO_PIN_6, GPIO_PIN_7}; uint8_t key_matrix[KEY_ROWS][KEY_COLS] = {0}; // 当前扫描值 uint8_t key_debounce[KEY_ROWS][KEY_COLS] = {0}; // 消抖计数器 void KEY_Scan(void) { for (uint8_t r = 0; r < KEY_ROWS; r++) { // 拉低当前行,其余行高阻 for (uint8_t i = 0; i < KEY_ROWS; i++) { HAL_GPIO_WritePin(row_ports[i], row_pins[i], (i == r) ? GPIO_PIN_RESET : GPIO_PIN_SET); HAL_GPIO_Mode_t mode = (i == r) ? GPIO_MODE_OUTPUT_PP : GPIO_MODE_INPUT; HAL_GPIO_Init(row_ports[i], &(GPIO_InitTypeDef){.Pin = row_pins[i], .Mode = mode}); } HAL_Delay(1); // 行稳定延时 // 读取所有列 for (uint8_t c = 0; c < KEY_COLS; c++) { uint8_t val = HAL_GPIO_ReadPin(col_ports[c], col_pins[c]); if (val == GPIO_PIN_RESET) { // 按下 if (key_debounce[r][c] < 20) key_debounce[r][c]++; // 20ms 消抖 if (key_debounce[r][c] == 20) key_matrix[r][c] = 1; } else { // 弹起 if (key_debounce[r][c] > 0) key_debounce[r][c]--; if (key_debounce[r][c] == 0) key_matrix[r][c] = 0; } } } }设计说明:
- 行扫描采用动态切换 GPIO 模式(输出推挽/输入浮空),避免外接上拉电阻,节省 BOM;
- 消抖计数器
key_debounce独立于每个按键,防止某键长按影响其他键;HAL_Delay(1)是最小稳定时间,实测低于 0.8ms 会导致列读取不稳定(F103 GPIO 输入建立时间约束);- 最终
key_matrix[r][c]是干净的 0/1 状态,供后续编码使用。
3.2 键盘报告组装:6 键无冲 + LED 状态回传 + 多媒体键支持
键盘报告共 8 字节:[Modifier][Reserved][Key1..6]。Modifier 字节定义 Ctrl/Shift/Alt/Gui 键状态,Key1–6 存储扫描到的 6 个普通键(USB 标准要求最多 6 键无冲)。我们约定:
- 行 0–1:字母数字键(A–Z, 0–9)
- 行 2:功能键(F1–F12)、方向键
- 行 3:多媒体键(音量+/−、播放/暂停)
// keymap.h:定义 4×4 键盘到 USB 键码的映射 const uint8_t key_code_map[4][4] = { {0x04,0x05,0x06,0x07}, // A,B,C,D {0x08,0x09,0x0A,0x0B}, // E,F,G,H {0x3A,0x3B,0x3C,0x3D}, // F1,F2,F3,F4 {0xE9,0xEA,0xB5,0xB6} // VOL+,VOL-,PLAY/PAUSE,MUTE }; // main.c 中的报告组装逻辑 uint8_t keyboard_report[8] = {0}; void BuildKeyboardReport(void) { uint8_t key_count = 0; // 扫描所有按键,填入 Key1–Key6 for (uint8_t r = 0; r < 4 && key_count < 6; r++) { for (uint8_t c = 0; c < 4 && key_count < 6; c++) { if (key_matrix[r][c]) { keyboard_report[2 + key_count] = key_code_map[r][c]; key_count++; } } } // Modifier:检测 Ctrl/Shift/Alt/GUI(假设 PA8–PA11 为独立功能键) if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8)) keyboard_report[0] |= 0x01; // Left Ctrl if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9)) keyboard_report[0] |= 0x02; // Left Shift if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_10)) keyboard_report[0] |= 0x04; // Left Alt if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11)) keyboard_report[0] |= 0x08; // Left GUI // 发送报告(interface_num = 0) USBD_HID_SendReport(&hUsbDeviceFS, keyboard_report, 8, 0); }设计说明:
keyboard_report[0]的 Modifier 位严格按 USB HID Usage Tables 定义(bit0=Left Ctrl, bit1=Left Shift…),错一位会导致组合键失效;- Key1–Key6 填充必须从
keyboard_report[2]开始,因为前 2 字节是 Modifier 和 Reserved;- 多媒体键码(如
0xE9音量+)必须在keyboard_report_desc中已声明支持,否则主机忽略;- 报告发送频率由
USBD_HID_SendReport()的调用时机控制,通常放在HAL_TIM_PeriodElapsedCallback()中以 10ms 周期触发,匹配配置描述符中的bInterval=0x0A(10ms)。
3.3 鼠标报告组装:ADC 采样 + 滤波 + 位移量化 + 按键状态打包
使用 ADC1 通道 0(PA0)读取电位器电压,通过滑动平均滤波抑制噪声,再将 0–3.3V 映射为 −127~+127 的 X/Y 位移值。鼠标三键(左/右/中)由 PA12–PA14 的 GPIO 输入读取。
// mouse.c #define ADC_BUF_SIZE 8 uint16_t adc_buffer[ADC_BUF_SIZE]; uint8_t mouse_report[4] = {0}; // [Buttons][X][Y][Wheel] void MOUSE_Init(void) { // ADC 初始化(12-bit, 1 cycle, no DMA) hadc1.Instance = ADC1; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; HAL_ADC_Init(&hadc1); } int16_t MOUSE_ReadX(void) { int32_t sum = 0; for (int i = 0; i < ADC_BUF_SIZE; i++) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); sum += HAL_ADC_GetValue(&hadc1); } int32_t avg = sum / ADC_BUF_SIZE; // 映射 0–4095 → −127~+127,中心点为 2048 int16_t x = (avg - 2048) / 16; // 缩放因子 16,避免溢出 return (x < -127) ? -127 : (x > 127) ? 127 : x; } void BuildMouseReport(void) { // 按键:bit0=Left, bit1=Right, bit2=Middle mouse_report[0] = 0; if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_12)) mouse_report[0] |= 0x01; // Left if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_13)) mouse_report[0] |= 0x02; // Right if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_14)) mouse_report[0] |= 0x04; // Middle // X/Y 位移(调用滤波后函数) int16_t x = MOUSE_ReadX(); int16_t y = MOUSE_ReadY(); // 同理,用 PA1 通道 mouse_report[1] = (uint8_t)x; // 低字节 mouse_report[2] = (uint8_t)y; // 滚轮:固定为 0(可扩展为另一个电位器或编码器) mouse_report[3] = 0; // 发送报告(interface_num = 1) USBD_HID_SendReport(&hUsbDeviceFS, mouse_report, 4, 1); }设计说明:
- ADC 采样必须关闭连续模式(
ContinuousConvMode = DISABLE),否则HAL_ADC_PollForConversion()会永远等待;- 滑动平均用
ADC_BUF_SIZE=8是经验值,小于 4 噪声大,大于 16 响应迟钝;- 位移量化公式
(avg - 2048) / 16中,/16是缩放因子,确保 ±127 范围内不溢出(2048±127×16 = 2048±2032 = 16~4080,在 0–4095 内);- 鼠标报告
mouse_report[1]和[2]是有符号 8 位补码,直接赋值int16_t的低字节即可(高位截断),符合 HID 协议要求;- 滚轮字段
mouse_report[3]保留,即使不用也必须存在,否则报告长度不符。
4. 关键避坑指南:F103 USB 复合设备最易翻车的 5 个点
USB 枚举失败、设备管理器显示“未知设备”、键盘鼠标只能识别其一……这些问题 90% 出自以下五个细节。我带过三个基于 F103 的 USB 项目,血泪经验总结如下:
4.1 现象:Windows 设备管理器只显示“HID 键盘”,鼠标不出现;或反之
原因:配置描述符中bNumInterfaces写错(如写成0x01),或两个接口的bInterfaceNumber不是严格0x00和0x01(比如写成0x00和0x02)。Windows 驱动加载器会按bInterfaceNumber顺序查找,跳过0x01直接找0x02就失败。
解决:用 USB 协议分析仪(如 Total Phase Beagle USB 12)抓包,看主机GET_DESCRIPTOR CONFIGURATION返回的bNumInterfaces和各接口bInterfaceNumber是否匹配;若无硬件,用lsusb -v(Linux)或 USBView(Windows SDK 工具)查看枚举日志。
4.2 现象:设备能枚举,但键盘按键无反应,或鼠标移动卡顿
原因:报告描述符尺寸(KEYBOARD_REPORT_DESC_SIZE/MOUSE_REPORT_DESC_SIZE)与配置描述符中0x22条目后的字节数不一致。例如键盘描述符实际 63 字节,但配置描述符写成LOBYTE(62),主机解析报告时字节错位。
解决:用文本编辑器打开usbd_desc.c,手动计算keyboard_report_desc[]数组的元素个数(Ctrl+F 查,后加 1),再核对配置描述符中对应位置的两个字节是否完全相等。这是最隐蔽的坑,编译不会报错,但设备必瘫痪。
4.3 现象:插入 USB 后,板子反复重启,或 USB 端口供电异常
原因:USBD_CfgDesc数组未__ALIGN_BEGIN/__ALIGN_END对齐,导致 USB 外设 DMA 访问未对齐内存,触发 HardFault。F103 的 USB 模块要求所有描述符数组地址必须 32-bit 对齐(4 字节)。
解决:检查usbd_desc.c中所有__ALIGN_BEGIN uint8_t xxx[] __ALIGN_END声明,确认没有遗漏;用调试器查看USBD_CfgDesc的地址,末两位必须是0x00(如0x20000100正确,0x20000101错误)。
4.4 现象:键盘能用,但 NumLock/CapsLock 指示灯不亮;或鼠标滚轮无效
原因:键盘报告描述符中缺少OUTPUT项(LED 状态),或鼠标描述符中USAGE (Wheel)位置错误(未紧跟在 X/Y 后),或报告发送时字节数错误(如鼠标发了 5 字节而非 4 字节)。
解决:用 Wireshark + USBPcap 抓取主机SET_REPORT请求,看wValue字段是否为0x0200(LED 输出),若没收到说明描述符缺OUTPUT;鼠标滚轮问题则检查描述符中0x09,0x38(Wheel Usage)是否在0x09,0x30(X)和0x09,0x31(Y)之后,且REPORT_COUNT为 1。
4.5 现象:CubeMX 生成后编译报错undefined reference to 'USBD_HID_SendReport'
原因:CubeMX 中 USB Device 的 Class 设置为 “Custom”,但未勾选 “HID” Middleware,导致usbd_hid.c/h未生成;或勾选了 HID,但生成后手动修改了usbd_conf.c中的USBD_MAX_NUM_INTERFACES(默认为 1),未同步改为2。
解决:在 CubeMX 中 USB Device 配置页,确认 “Middleware” 下 “HID” 已打钩;生成代码后,打开usbd_conf.c,找到 `#define
本文还有配套的精品资源,点击获取