从零实现一个虚拟鼠标:HID协议实战入门
你有没有想过,为什么插上一个USB鼠标,电脑就能立刻识别并控制光标?不需要安装驱动、跨平台通用、响应迅速——这一切的背后,靠的正是HID协议(Human Interface Device)。而今天,我们要做的就是:亲手用一块STM32开发板,模拟出一个能被Windows、Linux甚至Mac正常识别的“虚拟鼠标”。
这不仅是一个炫技项目,更是深入理解USB通信机制的关键一步。无论你是嵌入式新手,还是想为后续开发体感设备、远程操控装置打基础,这个实践都会让你收获满满。
为什么选HID?因为它够“懒人”
在开始前,先问自己一个问题:如果要让PC识别一个新的输入设备,最麻烦的是什么?
答案是:写驱动程序。
但如果你选择的是HID类设备,恭喜你——系统已经帮你把驱动写好了!无论是Windows的hidusb.sys,还是Linux的usbhid模块,都原生支持HID协议。只要你的设备符合规范,插入即用,热插拔无压力。
所以,与其从头造轮子,不如站在巨人的肩膀上。HID就是那块最稳的跳板。
📌 核心优势一句话总结:
不用写驱动、跨平台兼容、低延迟传输、结构标准化。
我们今天的任务,就是利用这些特性,做一个会“动”的虚拟鼠标。
HID到底是什么?别被术语吓到
很多人一听到“HID协议”,就觉得复杂高深。其实它本质上很简单:
HID = 描述你“想告诉主机什么”,然后按格式发过去。
整个过程就像两个讲不同语言的人交流,需要一本“词典”来翻译。而这本词典,就是报告描述符(Report Descriptor)。
数据怎么传?靠“报告”
HID设备和主机之间通过三种“报告”对话:
- 输入报告(Input Report):设备说给主机听的话,比如“我向右移动了5个单位”。
- 输出报告(Output Report):主机命令设备做的事,比如“点亮LED”。
- 特征报告(Feature Report):用于配置设备参数,比如设置灵敏度。
对我们做鼠标来说,重点就是输入报告—— 每隔几毫秒告诉主机一次:“我现在的位置变了”。
报告描述符:数据语义的“说明书”
如果说HID通信是一场演出,那报告描述符就是剧本。它定义了每一个字节、每一位代表什么意思。主机拿到这份剧本后,才能正确解读你发送的数据包。
下面这段代码,就是一个标准鼠标的“剧本”:
static uint8_t My_HID_ReportDesc[] = { 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 buttons) 0x75, 0x01, // REPORT_SIZE (1 bit) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x05, // REPORT_SIZE (5 bits) — 填充到8位 0x81, 0x01, // INPUT (Constant) — 空位 // X/Y轴移动 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 };别慌,我们来拆解一下它的逻辑:
| 字段 | 含义 |
|---|---|
USAGE_PAGE (0x01) | 通用桌面设备类别 |
USAGE (Mouse) | 当前用途是“鼠标” |
COLLECTION | 开始一组相关数据 |
Button 1~3 | 定义三个按键(左、右、中) |
X/Y/Wheel | 定义坐标轴与滚轮 |
Logical Min/Max | 数值范围:-127 到 +127,有符号数 |
INPUT (Rel) | 表示这是相对变化量(不是绝对位置) |
最终生成的输入报告长这样:
| 字节 | 内容说明 |
|---|---|
| [0] | bit0: 左键, bit1: 右键, bit2: 中键;其余填充值 |
| [1] | X位移(-127 ~ +127) |
| [2] | Y位移 |
| [3] | 滚轮增量(正=向上滚动) |
也就是说,只要你按照这个格式发4个字节过去,操作系统就知道:“哦,用户点了左键,并向右下角拖动了一下”。
实战:用STM32打造你的第一个虚拟鼠标
硬件我们选用最常见的STM32F103C8T6(俗称“蓝 pill”),成本不到10元,却自带USB全速接口,配合STM32CubeMX工具链,几分钟就能跑起来。
第一步:配置USB设备模式
打开 STM32CubeMX:
- 选择芯片型号;
- 在 Pinout 图中启用
USB外设,设为Device (FS)模式; - 添加中间件 →
HID Class; - 设置端点轮询间隔为
8ms(USB HID推荐值); - 生成代码。
此时工程里会自动包含usbd_hid.c/h文件,提供基本的HID框架。
第二步:替换报告描述符
找到生成的USBD_CUSTOM_HID_Desc数组,替换成我们上面写的那个鼠标版描述符。
⚠️ 注意:必须确保数组名和链接脚本中的引用一致,否则枚举失败!
第三步:编写发送函数
接下来是最关键的一步:如何把数据真正“推”给电脑。
void SendMouseMove(int8_t x, int8_t y, uint8_t buttons) { uint8_t report[4]; report[0] = buttons; // 按键状态 report[1] = x; // X方向移动 report[2] = y; // Y方向移动 report[3] = 0; // 滚轮暂不使用 USBD_HID_SendReport(&hUsbDeviceFS, report, sizeof(report)); }这个函数非阻塞,底层由USB中断或DMA处理实际传输。你只需要关心“我要发什么”。
第四步:主循环测试
现在让它动起来!
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_DEVICE_Init(); while (1) { // 向右缓慢移动 SendMouseMove(3, 0, 0); HAL_Delay(100); // 约每秒10次 } }烧录程序,拔掉ST-Link下载器(防止干扰USB通信),直接用USB线连接开发板到电脑。
几秒钟后……你会发现鼠标指针开始缓缓向右滑动!
🎉 成功了!你刚刚创造了一个没有物理外壳的“幽灵鼠标”。
调试避坑指南:那些年我们踩过的雷
虽然原理简单,但在实践中总会遇到各种“玄学”问题。以下是几个高频故障及应对方法:
❌ 问题1:设备无法识别,显示“未知USB设备”
- 可能原因:报告描述符格式错误,主机解析失败
- 解决方案:
- 使用在线工具验证: https://eleccelerator.com/usbdescreqparser/
- 检查是否有遗漏的
END_COLLECTION或非法条目 - 确保
REPORT_SIZE和REPORT_COUNT总位数是8的倍数
❌ 问题2:光标乱抖、突然飞屏
- 可能原因:发送的位移值超出范围(如 >127),导致溢出
- 解决方案:
- 对
x/y做限幅处理:CLAMP(x, -127, 127) - 避免频繁调用
SendReport,建议间隔 ≥8ms
❌ 问题3:按键无效,点击没反应
- 可能原因:按钮位没有放在第一个字节的低三位
- 解决方案:
- 检查报告描述符中是否写了
REPORT_COUNT(3)和REPORT_SIZE(1) - 发送时确保
buttons的 bit0~bit2 正确设置
✅ 设计最佳实践小贴士
| 建议 | 说明 |
|---|---|
| 固定报告长度 | 不要动态改变大小,避免主机缓存错乱 |
| 使用相对坐标 | 鼠标必须用Input(..., Rel),表示增量 |
| 控制发送频率 | 8~10ms一次足够,太高反而增加总线负担 |
| 添加去抖逻辑 | 如果接真实按键,软件滤波必不可少 |
更进一步:你能用它做什么?
你以为这只是个玩具?其实它的潜力远超想象。
应用场景举例:
- 远程控制设备:通过Wi-Fi/BLE接收指令,转成HID鼠标操作电脑
- 无障碍辅助工具:头部追踪+微动开关,帮助行动不便者操作计算机
- 自动化测试脚本:自动生成鼠标轨迹,用于UI压力测试
- 安全研究:模拟恶意HID设备进行渗透测试(仅限合法用途!)
更酷的是,一旦掌握了HID的套路,你可以轻松扩展成键盘、触摸板、游戏手柄……甚至自定义多维控制器。
🔧 提示:只需修改报告描述符中的
USAGE和数据结构,就能变身其他设备类型。
结语:掌握HID,就握住了PC外设世界的钥匙
我们从一个简单的鼠标模拟出发,走完了完整的HID开发流程:
- 理解了HID协议的核心思想;
- 学会了如何编写精准的报告描述符;
- 在STM32上实现了数据上报;
- 解决了常见调试难题;
- 展望了更多创新应用可能。
这个项目看似简单,但它涵盖的知识点却是嵌入式人机交互的基石。掌握HID,意味着你不再只是“使用者”,而是可以成为“创造者”。
下次当你看到有人用树莓派Pico伪装成键盘执行攻击,或者用STM32做一个空中鼠标,你会知道:
—— 原来,我也能做到。
如果你正在尝试这个项目,欢迎在评论区分享你的成果。遇到了问题?也尽管提出来,我们一起解决。