一、系统概述
USB-HID(Human Interface Device) 是USB协议中用于人机交互的设备类,常见如键盘、鼠标、游戏手柄等。本设计基于STM32F103C8T6(Cortex-M3内核,72MHz)实现USB-HID下位机,模拟自定义HID设备(如4按键键盘+2轴摇杆),通过USB与PC通信,支持输入报告(下位机→PC) 和输出报告(PC→下位机),可用于工业控制、数据采集、自定义输入设备等场景。
二、硬件设计
2.1 核心组件
| 模块 | 型号/参数 | 功能说明 |
|---|---|---|
| 主控 | STM32F103C8T6(64KB Flash,20KB RAM) | USB HID协议处理、数据收发、外设控制 |
| USB接口 | Mini USB(DP/DM引脚) | 连接PC,实现USB 2.0全速通信(12Mbps) |
| 输入设备 | 4×轻触开关(GPIO输入) | 模拟HID按键(如K1-K4) |
| 输出设备 | 2×LED(GPIO输出) | 显示HID输出报告状态(如PC控制LED) |
| 电源 | 5V USB供电(或3.3V LDO) | 为STM32和USB模块供电 |
2.2 硬件连接
| 模块 | 引脚(STM32F103C8T6) | 说明 |
|---|---|---|
| USB | PA11(DM) | USB数据负线(D-) |
| PA12(DP) | USB数据正线(D+) | |
| 按键 | PA0-PA3(K1-K4) | 上拉输入,按下接地(低电平有效) |
| LED | PB0-PB1(LED1-LED2) | 推挽输出,高电平点亮 |
三、软件设计(STM32标准库3.5)
3.1 系统架构
USB HID
输入报告
输出报告
PC上位机
STM32 USB OTG
按键输入
LED输出
HID报告处理
3.2 核心原理
-
USB HID协议 :通过报告描述符定义设备功能(如按键、摇杆),上位机根据描述符解析数据。
-
端点配置:
-
端点1(IN,地址0x81):下位机→PC,发送输入报告(如按键状态)。
-
端点2(OUT,地址0x02):PC→下位机,接收输出报告(如控制LED)。
-
-
数据格式:输入/输出报告为二进制数组,按报告描述符定义的格式打包(如1字节按键状态+2字节摇杆数据)。
3.3 开发步骤
3.3.1 STM32CubeMX配置(关键步骤)
-
时钟配置:USB时钟需48MHz(STM32F103通过PLL分频:72MHz主频→PLLP=9→8MHz×6=48MHz)。
-
USB配置:
-
选择"Connectivity→USB_OTG_FS",模式设为"Device Only"。
-
勾选"Device→HID",参数设置:
-
Vendor ID:0x0483(ST官方示例,可自定义)
-
Product ID:0x5710(自定义)
-
Report Descriptor:见下文"3.3.2 报告描述符"。
-
-
-
GPIO配置:PA0-PA3(按键,上拉输入),PB0-PB1(LED,推挽输出)。
3.3.2 HID报告描述符(自定义设备)
定义4按键+2轴摇杆 的输入报告(8字节)和2LED控制的输出报告(1字节):
c
// 输入报告(8字节):1字节按键状态 + 2字节X轴 + 2字节Y轴 + 3字节保留
const uint8_t HID_ReportDescriptor[] = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Game Pad)
0xA1, 0x01, // COLLECTION (Application)
// 按键(4个,1字节,bit0-bit3对应K1-K4)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x04, // USAGE_MAXIMUM (Button 4)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x04, // REPORT_COUNT (4)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x75, 0x04, // REPORT_SIZE (4) // 保留4位
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
// 摇杆X轴(2字节,0-1023)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x16, 0x00, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x03, // LOGICAL_MAXIMUM (1023)
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// 摇杆Y轴(2字节,0-1023)
0x09, 0x31, // USAGE (Y)
0x16, 0x00, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x03, // LOGICAL_MAXIMUM (1023)
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// 输出报告(1字节):2个LED控制(bit0-LED1,bit1-LED2)
0x09, 0x00, // USAGE (Undefined)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x02, // REPORT_COUNT (2)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0xC0 // END_COLLECTION
};
3.3.3 核心代码实现
(1)USB HID初始化(标准库3.5)
c
#include "usb_lib.h"
#include "hw_config.h"
#include "usb_desc.h"
#include "usb_pwr.h"
// USB HID设备初始化
void USB_HID_Init(void) {
Set_System(); // 系统时钟、GPIO初始化
Set_USBClock(); // 配置USB时钟(48MHz)
USB_Interrupts_Config(); // 配置USB中断
USB_Init(); // 初始化USB设备
USB_SIL_Init(); // 初始化端点
}
(2)发送HID输入报告(按键+摇杆数据)
c
// 输入报告结构(8字节)
typedef struct {
uint8_t buttons; // 按键状态(bit0:K1, bit1:K2, bit2:K3, bit3:K4)
uint8_t reserved; // 保留字节
uint16_t x_axis; // X轴摇杆(0-1023)
uint16_t y_axis; // Y轴摇杆(0-1023)
} HID_InputReport;
// 发送输入报告
void HID_SendInputReport(HID_InputReport *report) {
uint8_t buf[8];
buf[0] = report->buttons; // 按键状态(1字节)
buf[1] = report->reserved; // 保留(1字节)
buf[2] = report->x_axis & 0xFF; // X轴低8位
buf[3] = report->x_axis >> 8; // X轴高8位
buf[4] = report->y_axis & 0xFF; // Y轴低8位
buf[5] = report->y_axis >> 8; // Y轴高8位
buf[6] = 0; // 保留
buf[7] = 0; // 保留
// 通过端点1发送(IN端点,地址0x81)
UserToPMABufferCopy(buf, ENDP1_TXADDR, 8);
SetEPTxCount(ENDP1, 8);
SetEPTxValid(ENDP1);
}
(3)接收HID输出报告(控制LED)
c
// 输出报告结构(1字节)
typedef struct {
uint8_t leds; // LED状态(bit0:LED1, bit1:LED2)
} HID_OutputReport;
// USB中断回调函数(接收输出报告)
void EP2_OUT_Callback(void) {
uint8_t buf[1];
HID_OutputReport report;
// 从端点2(OUT端点,地址0x02)读取数据
PMAToUserBufferCopy(buf, ENDP2_RXADDR, 1);
report.leds = buf[0];
// 控制LED
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(report.leds & 0x01)); // LED1
GPIO_WriteBit(GPIOB, GPIO_Pin_1, (BitAction)((report.leds >> 1) & 0x01)); // LED2
SetEPRxValid(ENDP2); // 使能端点2接收
}
(4)主函数(按键扫描+数据发送)
c
int main(void) {
HID_InputReport input_report = {0};
uint8_t key_state = 0;
SystemInit();
USB_HID_Init();
GPIO_Configuration(); // 配置按键(PA0-PA3)和LED(PB0-PB1)
while (1) {
// 1. 扫描按键状态(K1-K4:PA0-PA3,按下为低电平)
key_state = 0;
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) key_state |= 0x01; // K1
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 0) key_state |= 0x02; // K2
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0) key_state |= 0x04; // K3
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == 0) key_state |= 0x08; // K4
// 2. 模拟摇杆数据(固定值,可替换为ADC采集)
input_report.buttons = key_state;
input_report.x_axis = 512; // 中间位置
input_report.y_axis = 512;
// 3. 发送HID输入报告(每10ms一次)
HID_SendInputReport(&input_report);
Delay_ms(10);
}
}
参考代码 STM32实现的USB-HID下位机 www.youwenfan.com/contentcss/56294.html
四、上位机测试(Python示例)
使用pywinusb库接收HID数据,验证下位机通信:
python
import pywinusb.hid as hid
def on_data(data):
print(f"按键状态: {bin(data[0])}, X轴: {data[2]+data[3]*256}, Y轴: {data[4]+data[5]*256}")
# 查找HID设备(VID=0x0483, PID=0x5710)
filter = hid.HidDeviceFilter(vendor_id=0x0483, product_id=0x5710)
devices = filter.get_devices()
if devices:
device = devices[0]
device.open()
device.set_raw_data_handler(on_data)
print("设备已连接,按Ctrl+C退出...")
import time
while True: time.sleep(1)
else:
print("未找到HID设备")
五、关键问题与解决方案
5.1 USB枚举失败
-
原因:USB时钟未配置为48MHz,或报告描述符错误。
-
解决:
-
用示波器测量USB DP/DM引脚,确保有48MHz时钟;
-
用
USBlyzer工具分析枚举过程,检查报告描述符是否符合HID规范。
-
5.2 数据发送卡顿
-
原因:HID报告发送频率过高,或PC端处理不及时。
-
解决:
-
降低发送频率(如10ms/次);
-
使用双缓冲机制,避免数据覆盖。
-
5.3 按键抖动
-
原因:机械按键按下时产生抖动,导致误报。
-
解决:软件消抖(检测到按键变化后延时10ms再确认状态)。
六、总结
基于STM32F103实现了USB-HID下位机,通过自定义报告描述符支持4按键+2轴摇杆输入,2LED输出,可扩展为游戏手柄、工业控制器等。核心步骤包括:USB时钟配置、HID报告描述符定义、端点数据收发、按键/LED控制。