CSDN_USB鼠标Boot与Report协议兼容问题排查
STM32/GD32 USB Host鼠标横向移动却出现明显Y轴漂移:Boot与Report协议不匹配问题排查
前言
最近在一个GD32嵌入式仪器项目中遇到了一个比较隐蔽的USB鼠标兼容问题:
- 鼠标接到Windows电脑上使用正常;
- 接到嵌入式设备后,光标明显“起飞”;
- 缓慢水平移动鼠标时,光标虽然会横向移动,但同时伴随非常明显的Y轴变化;
- 轨迹看起来像上下波动、蛇形移动,甚至有点像正弦波;
- 其他普通鼠标连接同一台设备却基本正常。
一开始很容易把问题归因于:
- 鼠标DPI过高;
- 480×272屏幕分辨率太低;
- 鼠标传感器质量差;
- USB丢包;
- GUI坐标更新异常;
- 缺少鼠标加速度或滤波。
但经过USBPcap/Wireshark抓包、源码分析和Boot/Report协议切换测试后,最终确认:
真正的问题不是软件DPI,也不是USB丢包,而是主机要求鼠标使用Report Protocol,但工程仍然按照固定的Boot Mouse字节位置解析数据。
异常鼠标在Report模式下使用了5字节、12位X/Y打包格式,而工程将其中的混合字节直接当成8位Y坐标,导致很小的Y位移被解析成很大的数值。
本文记录完整排查过程,供使用STM32、GD32以及早期ST USB Host HID库的开发者参考。
一、项目中的鼠标处理流程
项目使用USB Host HID类接收鼠标数据,鼠标数据经过两层处理。
第一层负责从USB报告中提取按键和X/Y:
usbh_statususbh_hid_mouse_decode(uint8_t*data){mouse_info.buttons[0]=data[0]&MOUSE_BUTTON_1;mouse_info.buttons[1]=data[0]&MOUSE_BUTTON_2;mouse_info.buttons[2]=data[0]&MOUSE_BUTTON_3;mouse_info.x=data[1];mouse_info.y=data[2];usr_mouse_process_data(&mouse_info);returnUSBH_OK;}第二层负责把相对位移累加到屏幕坐标:
voidusr_mouse_process_data(hid_mouse_info*data){GUI_PID_STATE StateNew;GUI_PID_GetState(&StateNew);StateNew.Pressed=data->buttons[0]|data->buttons[1]|data->buttons[2];StateNew.x+=(signedchar)data->x;StateNew.y+=(signedchar)data->y;if(StateNew.x<0){StateNew.x=0;}if(StateNew.x>479){StateNew.x=479;}if(StateNew.y<0){StateNew.y=0;}if(StateNew.y>271){StateNew.y=271;}GUI_PID_StoreState(&StateNew);}从这段代码可以看出,工程默认认为鼠标报告格式固定为:
data[0]:按键 data[1]:X相对位移 data[2]:Y相对位移 data[3]:滚轮(代码未使用)这实际上是一种典型的Boot Mouse固定格式解析方式。
二、Boot Protocol和Report Protocol的区别
USB HID鼠标通常涉及两种协议模式。
1. Boot Protocol
Boot Protocol是USB HID为启动型键盘和鼠标定义的简化标准格式。
典型Boot鼠标格式:
字节0:按键 字节1:X相对位移 字节2:Y相对位移 字节3:滚轮(部分鼠标提供)主机不需要解析复杂的Report Descriptor,只需要固定读取相应字节。
适用场景包括:
- BIOS;
- Bootloader;
- MCU;
- 仪器;
- 只需要基本鼠标移动和按键功能的嵌入式设备。
2. Report Protocol
Report Protocol的数据格式由鼠标自己的Report Descriptor定义。
不同鼠标可能使用完全不同的格式,例如:
[按键][X][Y][滚轮]或者:
[Report ID][按键][X][Y]还可能是:
[按键][12位X和Y打包数据][滚轮]游戏鼠标或高分辨率鼠标还可能包含:
- 多个Report ID;
- 12位或16位X/Y;
- 侧键;
- 水平滚轮;
- 高精度滚轮;
- 厂商自定义字段;
- DPI、RGB和宏相关Feature Report。
Report模式下,主机应根据Report Descriptor动态计算每个字段的位置,而不能固定认为data[1]一定是X、data[2]一定是Y。
3. SET_PROTOCOL的标准值
USB HID通过类请求SET_PROTOCOL切换协议:
wValue = 0:Boot Protocol wValue = 1:Report Protocol需要注意的是,这里说的是USB Setup包中最终发出去的wValue,不一定等于某个厂商库函数的输入参数。
三、工程中容易误导的协议设置代码
项目状态机中原来的调用为:
caseHID_REQ_SET_PROTOCOL:if(USBH_OK==usbh_set_protocol(uhost,0U)){hid->ctl_state=HID_REQ_IDLE;status=USBH_OK;}break;看到这里的0U,很容易按照USB标准理解为:
0 = Boot Protocol但是库函数内部还有一次取反:
staticusbh_statususbh_set_protocol(usbh_host*uhost,uint8_tprotocol){usbh_status status=USBH_BUSY;if(CTL_IDLE==uhost->control.ctl_state){uhost->control.setup.req=(usb_req){.bmRequestType=USB_TRX_OUT|USB_RECPTYPE_ITF|USB_REQTYPE_CLASS,.bRequest=SET_PROTOCOL,.wValue=!protocol,.wIndex=0U,.wLength=0U};usbh_ctlstate_config(uhost,NULL,0U);}status=usbh_ctl_handler(uhost);returnstatus;}因此实际计算为:
protocol = 0 ↓ !protocol = 1 ↓ 最终USB wValue = 1 ↓ 鼠标进入Report Protocol这个厂商函数的参数关系实际上是:
| 函数参数 | 最终USBwValue | 协议 |
|---|---|---|
0U | 1 | Report |
1U | 0 | Boot |
于是原工程形成了一个不一致的组合:
主机要求鼠标使用Report Protocol + 主机按照Boot固定位置解析数据对于Report格式刚好与Boot格式相同的鼠标,这个问题不会暴露。
一旦遇到Report格式不同的鼠标,就会发生字段错位。
四、为什么其他鼠标一直正常
使用USBPcap和Wireshark抓取一只正常鼠标的数据,得到:
HID Data: 00 FA 0C 00共4字节,可直接解释为:
| 字节 | 数值 | 含义 |
|---|---|---|
data[0] | 00 | 按键 |
data[1] | FA | X |
data[2] | 0C | Y |
data[3] | 00 | 滚轮 |
转换为8位有符号数:
X = (int8_t)0xFA = -6 Y = (int8_t)0x0C = 12项目原来的解析:
mouse_info.x=data[1];mouse_info.y=data[2];对于这只鼠标完全正确。
虽然鼠标当前可能处于Report Protocol,但它的Report布局正好是:
[按键][X][Y][滚轮]与Boot格式前三个字节兼容,因此长期没有暴露问题。
这并不表示鼠标偷偷返回了Boot数据,更准确地说:
鼠标处于Report模式,但它的Report格式恰好与Boot固定布局兼容。
五、异常鼠标的Wireshark数据
异常鼠标抓到的数据为:
HID Data: 00 FD BF FF 00共5字节,比正常鼠标多一个字节。
这组数据非常符合12位X/Y打包格式:
| 字节 | 含义 |
|---|---|
data[0] | 按键 |
data[1] | X低8位 |
data[2] | X高4位与Y低4位的混合字节 |
data[3] | Y高8位 |
data[4] | 滚轮 |
为什么比普通鼠标多一个字节?
普通鼠标的X/Y各占8位:
8位X + 8位Y = 16位 = 2字节这只鼠标的X/Y各占12位:
12位X + 12位Y = 24位 = 3字节因此轴数据刚好多出一个字节:
普通格式: 按键1字节 + 坐标2字节 + 滚轮1字节 = 4字节 12位格式: 按键1字节 + 坐标3字节 + 滚轮1字节 = 5字节六、正确解析异常鼠标的12位坐标
1. 解析X
X由data[1]和data[2]的低4位组成:
x=data[1]|((data[2]&0x0F)<<8);代入数据:
data[1] = 0xFD data[2] & 0x0F = 0x0F X = 0xFD | 0xF00 = 0xFFD0xFFD按12位有符号数解释为:
X = -32. 解析Y
Y由data[2]的高4位和data[3]组成:
y=(data[2]>>4)|(data[3]<<4);代入数据:
data[2] >> 4 = 0x0B data[3] << 4 = 0xFF0 Y = 0xFF0 | 0x00B = 0xFFB0xFFB按12位有符号数解释为:
Y = -5所以这包数据的真实含义大约为:
按键 = 0 X = -3 Y = -5 滚轮 = 0七、原工程为什么会把Y轴放大
原工程直接执行:
mouse_info.x=data[1];mouse_info.y=data[2];然后在应用层转换成8位有符号数:
StateNew.x+=(signedchar)data->x;StateNew.y+=(signedchar)data->y;因此原工程得到:
X = (signed char)0xFD = -3 Y = (signed char)0xBF = -65对比真实值:
| 坐标 | 正确解析 | 原工程解析 |
|---|---|---|
| X | -3 | -3 |
| Y | -5 | -65 |
这就精确解释了实际现象:
- X低8位仍在
data[1],所以水平移动没有完全失效; data[2]不是完整的Y,而是X/Y的混合字节;- 工程把
0xBF直接当成8位Y,得到-65; - 真实的轻微Y变化被解析成很大的Y变化;
data[2]同时受X和Y影响,因此轨迹会出现上下波动、蛇形或类似正弦变化。
完整错误链路:
异常鼠标发送5字节、12位X/Y打包Report ↓ 工程把data[2]直接当成8位Y ↓ 真实Y=-5被解析成Y=-65 ↓ 水平移动时出现明显纵向漂移八、为什么这不是软件DPI问题
项目屏幕只有480×272,而鼠标可能有800、1000甚至更高DPI。
当前代码采用:
鼠标1个计数 = 屏幕1个像素因此鼠标过于灵敏确实可能存在,也可以在应用层使用1/2、1/3或1/4定点缩放。
但软件缩放只能解决:
移动速度过快不能解决:
X/Y字段解析错误本问题中,真实Y为-5,却被解析成-65。即使再除以3:
-65 / 3 ≈ -21仍然是明显错误。
所以正确顺序应该是:
第一步:修复协议模式和数据格式不匹配 第二步:确认X/Y方向正确 第三步:再根据屏幕大小调整鼠标灵敏度不能使用缩放或滤波掩盖协议解析错误。
九、推荐修复:强制使用Boot Protocol
当前仪器只需要:
- 基本鼠标移动;
- 左键;
- 右键;
- 可能使用中键;
- 不需要游戏鼠标的RGB、宏和高精度扩展功能。
因此最简单、稳定的修复方式是:
对声明支持Boot的鼠标,发送
SET_PROTOCOL wValue=0,要求鼠标自己切换成标准Boot格式。
切换后流程:
鼠标连接 ↓ 读取HID接口描述符 ↓ 确认是Boot Mouse接口 ↓ 主机发送SET_PROTOCOL,最终wValue=0 ↓ 鼠标切换成标准Boot输出 ↓ 鼠标发送[按键][X][Y] ↓ 现有固定解析正确本项目实测结果:
- 修改后,原来正常的鼠标仍然正常;
- 原来5字节、12位报告的异常鼠标恢复正常;
- 水平移动时明显的Y轴异常消失。
这构成了比较完整的A/B验证。
十、两种等效修改方法
方法一:修改库函数,让参数直接对应USB标准值
调用保持:
usbh_set_protocol(uhost,0U);将:
.wValue=!protocol;改为:
.wValue=protocol;最终:
传入0 ↓ wValue=0 ↓ Boot Protocol优点:
- 参数语义直观;
- 与USB标准一致;
0=Boot,1=Report。
缺点:
- 修改了第三方官方USB库;
- 后续升级或重新覆盖库文件时可能丢失;
- 与厂商原API约定不同。
方法二:保留厂商库,只修改调用参数
保留:
.wValue=!protocol;将调用:
usbh_set_protocol(uhost,0U);改为:
usbh_set_protocol(uhost,1U);最终:
传入1 ↓ !1 = 0 ↓ wValue=0 ↓ Boot Protocol考虑到这是第三方厂商USB库,更推荐方法二。
建议添加明确注释:
/* * Vendor HID API uses an inverted protocol argument: * argument 1 produces SET_PROTOCOL wValue = 0, * which selects Boot Protocol. * * The current mouse decoder uses the fixed Boot layout: * data[0] = buttons, data[1] = X, data[2] = Y. */if(USBH_OK==usbh_set_protocol(uhost,1U)){hid->ctl_state=HID_REQ_IDLE;status=USBH_OK;}也可以定义宏避免魔法数字:
#defineUSBH_VENDOR_SELECT_BOOT_PROTOCOL1U调用:
usbh_set_protocol(uhost,USBH_VENDOR_SELECT_BOOT_PROTOCOL);注意:两种方法不能同时使用
如果已经把调用改成:
usbh_set_protocol(uhost,1U);就必须保留:
.wValue=!protocol;如果同时改成:
.wValue=protocol;最终又会发送:
wValue=1重新回到Report Protocol。
十一、是否可以直接移植某些例程的“6字节鼠标解析”
一些STM32教学例程中存在类似处理:
if(HID_Machine.length==6){HID_MOUSE_Data.button=data[0];HID_MOUSE_Data.x=data[1];HID_MOUSE_Data.y=data[3]<<4|data[2]>>4;HID_MOUSE_Data.z=data[4];}其中:
data[3]<<4|data[2]>>4确实是在处理类似的12位Y坐标打包格式。
对本文抓到的:
00 FD BF FF 00它能得到Y的低8位:
Y = 0xFFB 低8位 = 0xFB 转换为int8_t后为-5因此针对这一只鼠标,它可能变相解决问题。
但不建议直接照搬,原因包括:
- 它只针对某一种固定格式;
- 通过端点最大包长猜测报告布局不严谨;
- 没有完整保存12位X/Y;
uint8_t会截断高位;- 其他5字节或6字节鼠标不一定使用相同布局;
- 换鼠标后仍可能出现新问题。
如果产品只需要基本鼠标功能,切换Boot比增加多套猜测式解析更可靠。
十二、如果必须保留Report Protocol
如果产品需要完整支持Report模式,就应真正解析Report Descriptor。
至少需要处理:
- Usage Page;
- Usage;
- Report ID;
- Report Size;
- Report Count;
- Input;
- Logical Minimum和Maximum;
- 字段位偏移;
- 8位、12位和16位有符号数;
- 多个Report ID;
- Constant/Padding;
- 数据长度校验。
针对本文12位格式,至少需要类似:
int16_tx;int16_ty;x=data[1]|((data[2]&0x0F)<<8);y=(data[2]>>4)|(data[3]<<4);if(x&0x0800){x|=0xF000;}if(y&0x0800){y|=0xF000;}但是当前工程中的:
typedefstruct{uint8_tx;uint8_ty;uint8_tbuttons[3];}hid_mouse_info;只能保存8位坐标。
要完整支持12位坐标,还需要调整:
hid_mouse_info.x/y的数据类型;- 后续坐标累加;
- 位移缩放;
- 大位移限幅;
- 不同Report ID的分发;
- 接收缓冲区长度。
这已经不是几行代码的修改,而是一套Report解析功能。
十三、Boot方案的兼容性边界
Boot模式适合普通办公鼠标和只需要基本输入的仪器,但也有边界。
1. 设备必须支持Boot
接口描述符通常应满足:
bInterfaceClass = 3 // HID bInterfaceSubClass = 1 // Boot Interface bInterfaceProtocol = 2 // Mouse只有支持Boot的鼠标才能响应:
SET_PROTOCOL wValue=02. 纯Report设备可能失败
部分特殊设备可能不支持Boot:
- 触控板;
- 特殊工业输入设备;
- 某些复合设备;
- 只有厂商自定义HID接口的设备。
它们可能对SET_PROTOCOL返回:
STALL USBH_NOT_SUPPORTED USBH_FAIL状态机应处理失败,不能永远停留在:
HID_REQ_SET_PROTOCOL在没有Report动态解析器时,更合理的行为是明确提示设备不支持,而不是继续错误解析。
3. wIndex不能永远假设为0
原代码写死:
.wIndex=0U;wIndex应表示当前HID接口号。
普通单接口鼠标的接口通常是0,但复合设备可能是:
接口0:键盘 接口1:鼠标 接口2:扩展功能扩大兼容范围时,应使用当前接口描述符中的:
bInterfaceNumber这是另一个潜在兼容性问题,与本次12位坐标问题不同。
十四、如何使用Wireshark验证
Windows下可以安装USBPcap并配合Wireshark抓取鼠标USB数据。
1. 从插入前开始抓包
先启动USBPcap捕获,再插入鼠标,确保捕获完整枚举过程。
2. 找到鼠标设备地址
设备地址每次插拔可能变化,确认地址后再过滤:
usb.device_address == 5不要永久假设地址一定是5。
3. 观察中断IN数据
分别完成:
- 静止;
- 缓慢向右;
- 缓慢向左;
- 缓慢向上;
- 缓慢向下;
- 左右键点击。
对比每个字节随动作的变化。
4. 对比报告长度
本文正常鼠标:
00 FA 0C 00长度为4字节。
异常鼠标:
00 FD BF FF 00长度为5字节。
这个差异是定位12位坐标打包的重要线索。
5. 查看SET_PROTOCOL
查找:
bRequest = 0x0B确认最终:
wValue=0:Boot wValue=1:ReportWindows一般会使用Report Protocol并根据Report Descriptor动态解析,因此鼠标在Windows上正常,并不能证明数据采用Boot格式。
Windows鼠标速度、指针加速度等设置发生在HID数据进入操作系统之后,不会修改USBPcap抓到的原始USB报告。
十五、为什么这个问题很少被发现
这个问题之所以长期隐藏,主要有以下原因。
1. 多数普通鼠标的Report格式兼容Boot布局
即使主机选择了Report:
[按键][X][Y][滚轮]仍然可以被固定Boot解析正确处理。
2. 12位打包鼠标相对少见
只有遇到5字节、高分辨率或特殊Report布局的鼠标,问题才明显暴露。
3. 现象很像DPI或传感器问题
水平移动伴随Y抖动,很容易被认为是:
- 鼠标太差;
- DPI太高;
- 小屏幕放大;
- 传感器抖动。
4. 厂商API参数具有迷惑性
调用:
USBH_HID_SetProtocol(phost,0U);看起来像在设置Boot,但函数内部又将它映射为Report。
如果不一直跟到USB Setup包中的最终wValue,很难发现。
5. 开发板例程通常只测试少量鼠标
很多USB Host HID例程的目标只是演示:
鼠标能枚举 光标能移动 按键能响应并不会针对大量不同鼠标做兼容性回归。
十六、修改前后流程对比
修改前
usbh_set_protocol(uhost, 0U) ↓ 库函数内部取反 ↓ 最终wValue=1 ↓ 鼠标使用Report Protocol ↓ 异常鼠标发送5字节12位坐标 ↓ 工程固定读取data[1]/data[2] ↓ 真实Y=-5被解析成-65 ↓ 光标Y轴明显起飞修改后
usbh_set_protocol(uhost, 1U) ↓ 库函数内部取反 ↓ 最终wValue=0 ↓ 鼠标切换Boot Protocol ↓ 鼠标输出标准[按键][X][Y] ↓ 工程固定解析正确 ↓ 新旧鼠标均正常核心区别:
修改前: Report输出 + Boot固定解析 = 存在兼容问题 修改后: Boot输出 + Boot固定解析 = 协议与解析一致十七、建议测试项目
修改后至少测试:
- 鼠标向右移动时,光标只向右;
- 鼠标向左移动时,光标只向左;
- 鼠标向上移动时,光标只向上;
- 鼠标向下移动时,光标只向下;
- 缓慢移动;
- 快速移动;
- 左键、右键和中键;
- 开机前插入鼠标;
- 开机后插入鼠标;
- 反复拔插;
- 原来正常的鼠标;
- 原来异常的5字节鼠标;
- 不同品牌办公鼠标;
- 同型号不同批次鼠标;
- 带侧键或DPI键的鼠标。
协议问题解决以后,再单独评估是否需要增加1/2、1/3或1/4的软件灵敏度缩放。
十八、最终结论
本次问题的根因可以总结为:
厂商USB Host库通过反向参数选择了Report Protocol ↓ 工程却只实现了固定Boot式鼠标解析 ↓ 普通4字节Report鼠标碰巧兼容,所以正常 ↓ 异常鼠标使用5字节、12位X/Y打包 ↓ 工程把X/Y混合字节data[2]直接当成8位Y ↓ 真实Y=-5被错误解析成Y=-65 ↓ 水平移动时出现明显Y轴漂移对于只需要基本鼠标功能的嵌入式仪器,最实用的方案是:
明确要求支持Boot的鼠标进入Boot Protocol + 继续使用固定Boot Mouse格式解析如果保留厂商库中的:
.wValue=!protocol;则调用参数应改为:
usbh_set_protocol(uhost,1U);确保最终USB请求为:
SET_PROTOCOL wValue=0该方案已经通过正常鼠标和异常鼠标的A/B测试验证。
如果产品需要支持所有Report鼠标、游戏鼠标、触控板和复合HID设备,则应实现真正的Report Descriptor动态解析,而不是根据4字节、5字节或6字节长度猜测数据格式。
参考资料
USB-IF《Device Class Definition for Human Interface Devices (HID)》
https://www.usb.org/sites/default/files/hid1_12.pdf
STMicroelectronics STM32 USB Host Middleware
https://github.com/STMicroelectronics/stm32-mw-usb-host
ST《STM32Cube USB Host Library》UM1720
https://www.st.com/resource/en/user_manual/um1720-stm32cube-usb-host-library-stmicroelectronics.pdf
Wireshark USB HID显示过滤器参考
https://www.wireshark.org/docs/dfref/u/usbhid.html
