简介:本资源是一份基于STM32F103C8T6的USB复合HID设备完整工程实现,面向嵌入式初学者与STM32进阶开发者,解决USB协议下键盘+鼠标双设备协同枚举与数据上报的核心难点。项目采用STM32CubeMX图形化配置生成HAL底层框架,涵盖时钟树设置、USB OTG FS设备模式启用、双HID报告描述符定制、中断端点分配及固件级数据响应逻辑,适用于人机交互类毕业设计、USB协议学习与低成本HID设备原型开发。压缩包含626个文件(7.58MB),以345个C源文件和114个头文件构成主程序逻辑,43个汇编启动文件与28个IAR链接脚本支撑多工具链适配,另含Keil工程文件(uvprojx/uvoptx)、调试符号(axf/map)及HAL库依赖模块,目录结构按外设驱动、USB核心、HID报告层分层组织,便于理解USB设备栈构建流程。目前已有3418人学习下载,可直接编译烧录验证复合设备识别效果,并为自定义HID功能扩展提供可复用的代码骨架与配置范式。
1. 一个 USB 接口,同时当键盘又当鼠标:STM32F103C8T6 上跑通复合 HID 设备的真实路径
你手头那块蓝 pill(STM32F103C8T6)开发板,USB 口一直只接个串口调试器?别浪费了——它完全能在一个物理 USB 插头上,同时暴露「标准键盘」和「标准鼠标」两个 HID 设备。Windows/macOS/Linux 都认,即插即用,不用装驱动。这不是理论,是实测可运行的完整工程:按下 PA0 按键,PC 端立刻收到 Ctrl+Alt+Del 组合键;旋转编码器,光标就平滑移动;长按 PB1 还能模拟鼠标中键滚轮。整个项目基于 STM32CubeMX 图形化配置生成初始化代码,HID 报文结构、描述符定义、端点管理、状态同步全部封装在 C 文件里,没有 HAL 黑匣子陷阱,也没有 USB 协议栈玄学。适合想把单片机真正变成“人机交互终端”的嵌入式开发者——比如做定制化控制面板、带按键+旋钮的工业 HMI、或给树莓派/PC 加个硬件级快捷键层。不是 demo,是能焊进产品里的最小可行 HID 复合设备。
2. 工程骨架与 CubeMX 配置:从空芯片到双设备枚举成功的 7 步关键操作
2.1 创建工程前必须确认的 3 个硬件事实
STM32F103C8T6 的 USB 是Full-speed Device-only(不支持 Host),且仅支持 USB Device 模式,没有 OTG 功能。这意味着:
- 必须使用PA11 (DM) 和 PA12 (DP)作为 USB 差分信号线,不能改脚;
- 板载 USB 口必须直连 PA11/PA12,且DP 线需接 1.5kΩ 上拉电阻到 3.3V(这是主机识别设备速度的关键);
- 芯片内部无 USB PHY,依赖外部信号完整性——若枚举失败,90% 是 DP/DM 走线过长、未包地、或上拉电阻值偏差(实测 1.4–1.6kΩ 可用,2.2kΩ 就大概率失败)。
提示:很多蓝 pill 板已内置 1.5kΩ 上拉,但部分山寨板用的是 10kΩ,务必用万用表量 PA12 对 3.3V 电阻值。这是后续所有调试的起点,别跳过。
2.2 CubeMX 中 USB Device 的 4 项强制配置
打开 STM32CubeMX(建议 v6.12+,低版本对复合设备支持不全),选择 STM32F103C8T6,按顺序操作:
- Enable USB Device:在 Pinout 视图中找到 USB_DEVICE → Mode → Select
Device Only; - Clock Configuration:在 Clock Configuration 标签页,确保
USBCLK = 48MHz(由 PLL 输出分频得到,必须勾选USB在 RCC → USB clock source); - USB Device Class:在 Middleware → USB_DEVICE → Class For Port 1 → 选择
Custom Class(⚠️ 不要选 HID Keyboard 或 HID Mouse 单一类!复合设备必须自定义); - Descriptor & Endpoint:点击 USB_DEVICE → Configuration → Descriptor Settings →
Device Descriptor:bNumConfigurations = 1(复合设备仍为单配置);Configuration Descriptor:bNumInterfaces = 2(核心!键盘 + 鼠标 = 2 个接口);Interface Descriptor:添加两个 Interface,分别设置:- Interface 0:
bInterfaceClass = 0x03(HID),bInterfaceSubClass = 0x01(No Subclass),bInterfaceProtocol = 0x01(Keyboard); - Interface 1:
bInterfaceClass = 0x03,bInterfaceSubClass = 0x01,bInterfaceProtocol = 0x02(Mouse);
- Interface 0:
生成代码后,usbd_conf.c中会自动创建USBD_CUSTOM_HID_Desc数组,但此时它是空的——下一步要手动填描述符。
2.3 复合 HID 描述符的手动注入:为什么不能靠 CubeMX 自动生成
CubeMX 的 Custom Class 仅生成框架,真正的 HID Report Descriptor 和 Interface Association Descriptor(IAD)必须手写。原因:IAD 是 USB 2.0+ 复合设备识别多接口的必需结构,而 CubeMX v6.12 仍未原生支持 IAD 生成。
你需要在Core/Inc/usbd_custom_hid.h中补充:
// IAD: 关联 Interface 0 和 Interface 1 为同一设备 #define CUSTOM_HID_IAD_DESCRIPTOR_SIZE 8 extern uint8_t CUSTOM_HID_IAD_Desc[CUSTOM_HID_IAD_DESCRIPTOR_SIZE];并在Core/Src/usbd_custom_hid.c中实现:
// IAD 描述符:告诉主机这两个接口属于同一个逻辑设备 uint8_t CUSTOM_HID_IAD_Desc[CUSTOM_HID_IAD_DESCRIPTOR_SIZE] = { 0x08, // bLength 0x0B, // bDescriptorType = IAD 0x00, 0x01, // bFirstInterface = 0, bInterfaceCount = 2 0x03, // bFunctionClass = 0x03 (HID) 0x00, // bFunctionSubClass = 0 0x00, // bFunctionProtocol = 0 0x00 // iFunction = 0 (no string) };注意:IAD 必须放在 Configuration Descriptor 之后、第一个 Interface Descriptor 之前。否则 Windows 设备管理器会显示“未知 USB 设备”,且无法枚举出两个 HID 子设备。
2.4 生成最终 USB 描述符链:顺序决定成败
完整的描述符链顺序必须严格为:Device Desc→Configuration Desc→IAD Desc→Interface 0 Desc→HID Desc (Interface 0)→Endpoint Desc (IN for Keyboard)→Interface 1 Desc→HID Desc (Interface 1)→Endpoint Desc (IN for Mouse)
在usbd_custom_hid.c的USBD_CUSTOM_HID_Desc数组中,按此顺序拼接所有描述符段。其中键盘和鼠标的 Report Descriptor 必须符合 HID Usage Tables v1.12 规范:
- 键盘 Report Descriptor(简化版,支持 6 键 rollover + modifier):
0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xA1, 0x01, // COLLECTION (Application) 0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad) 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, 0xFF, // LOGICAL_MAXIMUM (255) 0x05, 0x07, // USAGE_PAGE (Keyboard/Keypad) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xC0 // END_COLLECTION- 鼠标 Report Descriptor(支持 X/Y 移动 + 3 键 + 滚轮):
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关键参数说明:
REPORT_SIZE和REPORT_COUNT决定每个 HID 报文的字节数。键盘报文固定 8 字节(1 字节 modifier + 1 字节 reserved + 6 字节 key codes),鼠标报文固定 4 字节(3 字节 button+X+Y + 1 字节 wheel)。USB 端点最大包长必须 ≥ 这个值(此处设为 64)。
2.5 USB 端点分配与中断处理逻辑绑定
在usbd_custom_hid.c的USBD_CUSTOM_HID_Init()中,必须显式启用两个 IN 端点:
// 键盘使用 EP1 IN,鼠标使用 EP2 IN(避免冲突) USBD_LL_OpenEP(pdev, CUSTOM_HID_EPIN_ADDR_KEYBOARD, USBD_EP_TYPE_INTR, 0x40); // 64-byte max packet USBD_LL_OpenEP(pdev, CUSTOM_HID_EPIN_ADDR_MOUSE, USBD_EP_TYPE_INTR, 0x40);对应地,在USBD_CUSTOM_HID_DataIn()回调中,根据epnum区分发送:
static uint8_t USBD_CUSTOM_HID_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) { if (epnum == CUSTOM_HID_EPNUM_KEYBOARD) { // 填充键盘 report buffer,调用 USBD_LL_Transmit() } else if (epnum == CUSTOM_HID_EPNUM_MOUSE) { // 填充鼠标 report buffer,调用 USBD_LL_Transmit() } return USBD_OK; }血泪经验:HAL 库默认只开一个 IN 端点。若忘记为鼠标单独开 EP2,Windows 会枚举出键盘,但鼠标设备永远显示“正在安装驱动”然后失败——因为主机发了 SET_INTERFACE 请求,但设备没响应。
2.6 主循环中的 HID 状态同步:非阻塞扫描与双缓冲机制
在main.c的while(1)循环中,不能直接调用USBD_CUSTOM_HID_SendReport()——它底层调用USBD_LL_Transmit(),而该函数在 USB 总线忙时会阻塞。正确做法是:
- 定义两个全局 report buffer:
uint8_t keyboard_report[8] = {0}; // modifier + reserved + 6 keys uint8_t mouse_report[4] = {0}; // buttons + X + Y + wheel- 每 10ms 扫描一次 GPIO/编码器,更新 buffer 内容(注意原子性):
// 使用 HAL_GetTick() 实现软定时器 if ((HAL_GetTick() - last_scan_tick) >= 10) { last_scan_tick = HAL_GetTick(); // 更新 keyboard_report[0](modifier)和 keyboard_report[2..7](key codes) // 更新 mouse_report[0](buttons)、mouse_report[1](X)、mouse_report[2](Y)、mouse_report[3](wheel) }- 在
USBD_CUSTOM_HID_DataIn()回调中,将 buffer 拷贝到 USB 传输缓冲区并触发发送:
if (epnum == CUSTOM_HID_EPNUM_KEYBOARD) { memcpy(hid->Report_buf, keyboard_report, 8); USBD_LL_Transmit(pdev, CUSTOM_HID_EPIN_ADDR_KEYBOARD, hid->Report_buf, 8); } else if (epnum == CUSTOM_HID_EPNUM_MOUSE) { memcpy(hid->Report_buf, mouse_report, 4); USBD_LL_Transmit(pdev, CUSTOM_HID_EPIN_ADDR_MOUSE, hid->Report_buf, 4); }这样设计保证了:即使 USB 总线暂时繁忙,按键/鼠标事件也不会丢失,只是延迟 1 帧(约 10ms)发送。比轮询
USBD_CUSTOM_HID_IsBusy()更可靠。
3. 避坑指南:5 个让开发者反复重启烧录器的致命细节
3.1 现象:设备管理器显示“未知 USB 设备”,右键更新驱动无效
原因:IAD 描述符缺失或位置错误。Windows 10+ 强制要求复合设备提供 IAD,否则拒绝枚举第二接口。
解决:检查USBD_CUSTOM_HID_Desc数组中 IAD 是否紧接在 Configuration Descriptor 之后,且bFirstInterface=0、bInterfaceCount=2。用 USBlyzer 或 Wireshark + USBPcap 抓包,确认主机是否收到 IAD(类型 0x0B)。
3.2 现象:键盘能用,鼠标在设备管理器中显示为“HID-compliant mouse”,但光标不动
原因:鼠标 Report Descriptor 中LOGICAL_MINIMUM/MAXIMUM值错误,或USBD_CUSTOM_HID_DataIn()中发送了错误长度的数据(如发了 8 字节给鼠标端点)。
解决:用hid-report-descriptor在线工具校验鼠标描述符;在USBD_CUSTOM_HID_DataIn()中加if(epnum==2) printf("Mouse send %d bytes\n", len);日志;确保USBD_LL_Transmit()第三个参数传4(鼠标报文长度)。
3.3 现象:按下按键后 PC 端收到乱码字符(如@、~),而非预期的Ctrl+Alt+Del
原因:键盘 Report Descriptor 中USAGE_MINIMUM/MAXIMUM范围与实际发送的 keycode 不匹配,或 modifier 字节(report[0])位定义错误。
解决:对照 HID Usage Tables v1.12,确认 keycode0x29是Keyboard Application,而0xE0是Left Control;modifier 字节 bit0=Left Ctrl, bit1=Left Shift, bit2=Left Alt, bit3=Left GUI, bit4=Right Ctrl... 发送0x08表示只按 Left Alt。
3.4 现象:首次插入 USB 后设备正常,拔插几次后 Windows 提示“设备无法启动(代码 10)”
原因:USB 复位后,HAL 库未正确重置端点状态,导致USBD_LL_Transmit()返回USBD_BUSY后卡死。
解决:在USBD_CUSTOM_HID_DeInit()中显式关闭端点:
USBD_LL_CloseEP(pdev, CUSTOM_HID_EPIN_ADDR_KEYBOARD); USBD_LL_CloseEP(pdev, CUSTOM_HID_EPIN_ADDR_MOUSE);并在USBD_CUSTOM_HID_Init()中重新OpenEP。
3.5 现象:使用 ST-Link V2 烧录后,USB 无法识别,但 SWD 调试正常
原因:ST-Link V2 的 3.3V 输出能力不足(仅 50mA),而 USB 枚举初期需要 >100mA 维持 PHY 稳定。
解决:改用外部 3.3V 电源供电(如 AMS1117-3.3 模块),或在main()开头加HAL_Delay(100)让电源稳定后再初始化 USB。
4. 核心 HID 数据流拆解:从 GPIO 按下到 PC 光标移动的 11 个关键节点
4.1 GPIO 输入捕获:消抖与状态机设计
PA0 按键连接到 GND,采用上拉输入。HAL 库默认无硬件消抖,必须软件实现:
// 在 main.c 全局定义 static uint8_t key_state = 0; // 0=idle, 1=pressed, 2=released static uint32_t key_down_time = 0; // 在 10ms 扫描中: if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET) { // 按键释放 if (key_state == 1) { key_state = 2; key_down_time = HAL_GetTick(); } } else { // 按键按下 if (key_state == 0) { key_state = 1; } else if (key_state == 2 && (HAL_GetTick() - key_down_time) > 500) { // 长按 500ms 触发特殊功能(如鼠标中键) keyboard_report[0] |= 0x04; // Left Alt } }注意:不要用
HAL_GPIO_EXTI_Callback()做按键中断——USB 中断优先级更高,EXTI 可能被屏蔽导致丢键。
4.2 键盘 Report Buffer 填充逻辑:modifier 与 keycode 的协同
键盘报文格式(8 字节):
| Byte | 用途 | 示例值 |
|---|---|---|
| 0 | Modifier bits | 0x08= Left Alt |
| 1 | Reserved (always 0) | 0x00 |
| 2-7 | Keycode array (最多 6 键) | 0x29, 0x00, 0x00, 0x00, 0x00, 0x00=Del |
填充代码:
// 清空 memset(keyboard_report, 0, sizeof(keyboard_report)); // 设置 modifier:Ctrl+Alt+Del = 0x01 | 0x04 = 0x05 keyboard_report[0] = 0x05; // 设置 keycode:Del 的 usage code 是 0x29 keyboard_report[2] = 0x29;关键:
keyboard_report[1]必须为0x00,否则 Windows 解析失败。很多教程漏写这行,导致键盘“有按键声但无输出”。
4.3 鼠标移动数据生成:编码器旋转到 ΔX/ΔY 的映射
PB2/PB3 接增量编码器 A/B 相。使用 HAL 的HAL_TIM_Encoder_Start()获取计数值:
// 初始化 TIM2 为编码器模式 htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFF; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Encoder.Init.EncoderMode = TIM_ENCODERMODE_TI12; htim2.Encoder.Init.IC1Polarity = TIM_ICPOLARITY_RISING; htim2.Encoder.Init.IC2Polarity = TIM_ICPOLARITY_RISING; htim2.Encoder.Init.IC1Selection = TIM_ICSELECTION_DIRECTTI; htim2.Encoder.Init.IC2Selection = TIM_ICSELECTION_DIRECTTI; HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); // 在 10ms 扫描中: int16_t delta = __HAL_TIM_GET_COUNTER(&htim2) - last_encoder_count; last_encoder_count += delta; // 映射:1 编码器脉冲 ≈ 1 像素移动(可调) mouse_report[1] = (delta > 127) ? 127 : (delta < -127) ? -127 : delta; // X mouse_report[2] = 0; // Y 暂不使用注意:
__HAL_TIM_GET_COUNTER()返回 int32_t,但鼠标 report 只接受 int8_t。必须截断,否则高位溢出导致光标狂奔。
4.4 USB IN 端点传输触发:USBD_LL_Transmit 的隐藏约束
USBD_LL_Transmit()并非立即发送,而是将数据放入 FIFO,由 USB ISR 触发 DMA 传输。其成功前提:
- 端点必须已
OpenEP且未被Stall; pdev->pClass->DataIn回调必须已注册(CubeMX 自动生成);USBD_CUSTOM_HID_DataIn()中不能修改hid->Report_buf地址(它是静态 buffer)。
典型错误写法:
// ❌ 错误:动态分配 buffer,ISR 中访问野指针 uint8_t* temp_buf = malloc(4); memcpy(temp_buf, mouse_report, 4); USBD_LL_Transmit(pdev, EP2_IN, temp_buf, 4); // 危险!正确写法:
// ✅ 正确:使用预分配的 hid->Report_buf memcpy(hid->Report_buf, mouse_report, 4); USBD_LL_Transmit(pdev, EP2_IN, hid->Report_buf, 4);4.5 主机端接收解析:Windows HID Class Driver 如何拆包
Windows 收到 IN 包后,按以下流程处理:
- 根据
bInterfaceNumber将数据路由到对应 HID minidriver(kbdclass.sys 或 mouclass.sys); - 用 Report Descriptor 中的
USAGE_PAGE/USAGE查找字段含义; - 将
keyboard_report[0]解析为 modifier 键状态; - 将
keyboard_report[2]解析为 keycode,查表转为 Unicode 字符(需配合当前键盘布局); - 将
mouse_report[1]作为相对 X 位移,叠加到当前光标坐标。
验证方法:在 Windows 设备管理器 → 键盘/鼠标属性 → “详细信息” → 选择“硬件 ID”,确认
VID_XXXX&PID_YYYY与usbd_desc.c中定义一致;用hid-nuget工具读取设备 Report Descriptor,比对是否与源码一致。
4.6 复合设备的电源管理:Suspend/Resume 事件处理
USB 主机可能在空闲时发送SET_FEATURE DEVICE_SUSPEND。若设备不响应,会被强制断开。必须实现:
// 在 usbd_custom_hid.c 中添加 static uint8_t USBD_CUSTOM_HID_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) { switch (req->bmRequest & 0x1F) { case USB_REQ_SET_FEATURE: if (req->wValue == 0x0002) { // DEVICE_SUSPEND // 进入低功耗:关闭 GPIO 时钟、降低系统时钟 __HAL_RCC_GPIOA_CLK_DISABLE(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); } break; } return USBD_OK; }注意:唤醒后需重新初始化所有外设时钟,否则 GPIO 无法读取。这是最易忽略的电源管理坑。
5. 二次开发实战:从双设备扩展到三设备(键盘+鼠标+游戏手柄)的改造清单
5.1 新增 Gamepad 接口的 Descriptor 修改清单
要在现有双设备基础上增加标准游戏手柄(HID Joystick),需修改三处:
- Configuration Descriptor:
bNumInterfaces从2改为3; - IAD Descriptor:
bInterfaceCount从2改为3; - 新增 Interface 2:
bInterfaceClass = 0x03,bInterfaceSubClass = 0x00,bInterfaceProtocol = 0x00(Joystick 无协议);- 对应 Report Descriptor(支持 32 按键 + 6 轴 + 2 个 POV 开关):
0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x04, // USAGE (Joystick) 0xA1, 0x01, // COLLECTION (Application) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x20, // USAGE_MAXIMUM (Button 32) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x20, // REPORT_COUNT (32) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x09, 0x32, // USAGE (Z) 0x09, 0x33, // USAGE (Rx) 0x09, 0x34, // USAGE (Ry) 0x09, 0x35, // USAGE (Rz) 0x15, 0x81, // LOGICAL_MINIMUM (-127) 0x25, 0x7F, // LOGICAL_MAXIMUM (127) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x06, // REPORT_COUNT (6) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x39, // USAGE (Hat switch) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x07, // LOGICAL_MAXIMUM (7) 0x35, 0x00, // PHYSICAL_MINIMUM (0) 0x45, 0x07, // PHYSICAL_MAXIMUM (7) 0x75, 0x04, // REPORT_SIZE (4) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xC0 // END_COLLECTION关键变化:游戏手柄 Report Descriptor 长度为 52 字节,因此需为 EP3 IN 分配更大 buffer(如 64 字节),并在
USBD_CUSTOM_HID_Init()中OpenEP。
5.2 新增端点与传输通道的代码补丁
在usbd_custom_hid.h中定义新端点:
#define CUSTOM_HID_EPIN_ADDR_JOYSTICK 0x83 // EP3 IN #define CUSTOM_HID_EPNUM_JOYSTICK 0x03在USBD_CUSTOM_HID_Init()中:
USBD_LL_OpenEP(pdev, CUSTOM_HID_EPIN_ADDR_JOYSTICK, USBD_EP_TYPE_INTR, 0x40);在USBD_CUSTOM_HID_DataIn()中增加分支:
else if (epnum == CUSTOM_HID_EPNUM_JOYSTICK) { memcpy(hid->Report_buf, joystick_report, 12); // 游戏手柄报文实际用 12 字节 USBD_LL_Transmit(pdev, CUSTOM_HID_EPIN_ADDR_JOYSTICK, hid->Report_buf, 12); }注意:
joystick_report需定义为uint8_t joystick_report[12],其中 byte0-3 为 32 个按钮(bit0-bit31),byte4-9 为 6 轴(各 1 字节),byte10-11 为 POV(4-bit each)。
5.3 主循环中游戏手柄状态采集:ADC 模拟摇杆接入
PA4/PA5 接电位器模拟 X/Y 轴。使用 HAL ADC 多通道扫描:
// 初始化 ADC1,通道 IN4(IN5),12-bit,右对齐 hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 2; hadc1.AdvancedInit.AutoDelay = DISABLE; hadc1.AdvancedInit.Overrun = ADC_OVR_DATA_OVERWRITTEN; HAL_ADC_Init(&hadc1); // 配置通道 sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_5; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 在 10ms 扫描中: HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint32_t x_raw = HAL_ADC_GetValue(&hadc1); HAL_ADC_Start(&hadc1); // 重新启动获取第二个通道 HAL_ADC_PollForConversion(&hadc1, 10); uint32_t y_raw = HAL_ADC_GetValue(&hadc1); // 映射到 -127 ~ +127 joystick_report[4] = (x_raw > 2048) ? (x_raw - 2048) / 16 : -(2048 - x_raw) / 16; joystick_report[5] = (y_raw > 2048) ? (y_raw - 2048) / 16 : -(2048 - y_raw) / 16;血泪经验:ADC 采样必须用
HAL_ADC_PollForConversion(),不能用中断——USB ISR 优先级高于 ADC IRQ,中断方式必然丢数据。
5.4 Windows 设备识别验证:三设备共存的注册表证据
成功枚举后,在 Windows 注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB下,会看到三个子项:
VID_XXXX&PID_YYYY&MI_00→ 键盘(Interface 0)VID_XXXX&PID_YYYY&MI_01→ 鼠标(Interface 1)
本文还有配套的精品资源,点击获取