CSDN_USB鼠标Boot与Report协议兼容问题排查

📅 2026/7/30 8:21:44 👁️ 阅读次数 📝 编程学习
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协议
0U1Report
1U0Boot

于是原工程形成了一个不一致的组合:

主机要求鼠标使用Report Protocol + 主机按照Boot固定位置解析数据

对于Report格式刚好与Boot格式相同的鼠标,这个问题不会暴露。

一旦遇到Report格式不同的鼠标,就会发生字段错位。


四、为什么其他鼠标一直正常

使用USBPcap和Wireshark抓取一只正常鼠标的数据,得到:

HID Data: 00 FA 0C 00

共4字节,可直接解释为:

字节数值含义
data[0]00按键
data[1]FAX
data[2]0CY
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 = 0xFFD

0xFFD按12位有符号数解释为:

X = -3

2. 解析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 = 0xFFB

0xFFB按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=0

2. 纯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:Report

Windows一般会使用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字节长度猜测数据格式。


参考资料

  1. USB-IF《Device Class Definition for Human Interface Devices (HID)》

    https://www.usb.org/sites/default/files/hid1_12.pdf

  2. STMicroelectronics STM32 USB Host Middleware

    https://github.com/STMicroelectronics/stm32-mw-usb-host

  3. ST《STM32Cube USB Host Library》UM1720

    https://www.st.com/resource/en/user_manual/um1720-stm32cube-usb-host-library-stmicroelectronics.pdf

  4. Wireshark USB HID显示过滤器参考

    https://www.wireshark.org/docs/dfref/u/usbhid.html