HID Report Descriptor 调试:为什么设备能枚举但主机读到错的数据
排查 HID Report Descriptor 的细节错误:设备枚举成功,但主机把数据解析错了——这是 USB 固件最常见的坑之一。
HID 之所以受欢迎,是因为很多设备都能在不写自定义驱动的情况下工作。键盘、传感器、旋钮、条码枪、控制面板、厂商自定义 HID 设备——都能受益于标准的主机协议栈。但 HID 把复杂度搬到了 Report Descriptor 里。一台设备可能枚举正常,但发出去的数据被主机解错。
这是 USB 固件最常见的坑之一:把"枚举成功"等同于"HID 正确"。
Report Descriptor 定义的是数据契约
HID Report Descriptor 告诉主机怎么解读字节。它定义了 usages、report sizes、report counts、logical ranges、physical ranges、collections、report IDs。描述符说的是一回事,固件发的是另一回事——主机只会按描述符来。
常见问题:
- 固件发 8 字节,但描述符写的是 7
- Report ID 该有的没有,或者多了一个
- 有符号值被声明成了无符号
- Logical Min / Max 跟实际范围对不上
- Usage Page 写错
- Padding bit 数错
- 多个 Report 布局混乱
- Input Report 和 Output Report 弄反
应用层看到的现象可能是数值错、按键丢失、Report 被忽略、读取时好时坏。
描述符和 Report 要一起抓
只看 Report Descriptor 调 HID 是不完整的,只看 payload 字节也是不完整的——这两者必须一起看。
一份合格的 HID 抓包包含:
- Device Descriptor
- Configuration 和 Interface Descriptor
- HID Descriptor
- Report Descriptor 请求和响应
- Interrupt IN Report
- Interrupt OUT Report(如有)
- Feature Report 的 Control Transfer
- Report ID 和 payload 长度
这样工程师才能把"声明的布局"和"实际的字节"对照起来。如果 Report Descriptor 写的是 Report Count 3,但 Interrupt payload 带的是 4 个值,抓包应该让这种不一致一目了然。
主机可能没毛病,只是你以为它坏了
固件开发者有时候会以为主机在丢数据。但实际上,主机是按它收到的描述符在解析。如果描述符声明了 padding,或者用了不同的 Report ID,数据就会看起来"错位、截断、被忽略"。
这也是为什么一份合格的支持报告必须带上原始字节。解析后的解读固然好用,但只有原始字节能终结争论。要问的是:
- 固件发了什么?
- 固件声明了什么?
- 主机请求了什么?
- 主机收到了什么?
这才是 HID 调试的正确边界。
组合 HID 设备要更小心
组合设备里可能同时有 HID 加 CDC、Storage、厂商接口。HID 部分单独看可能没问题,但接口编号、端点分配、描述符总长度出错的连带影响会把它也牵连进去。
组合 HID 调试要看:
- Interface Association(如果有)
- Interface 编号
- Endpoint 地址唯一性
- HID Descriptor 位置
- Report Descriptor 长度
- 类特定请求的路由
主机如果从错误的接口要 Report Descriptor,或者收到的长度不对,后面的 Report 流量就会被误导。
Bus Scope 在这个场景里做什么
Bus Scope 面向需要"用证据说话"的 USB 调试场景。HID Report Descriptor 这一类问题,它应该让工程师能在同一个地方看:描述符树、原始字节、端点流量、保存的 .bscope 会话。
实际跑完应该能输出一份报告:
- HID Report Descriptor 是否被请求和返回
- 描述符声明的 Report 长度
- 实际的 Interrupt payload 长度
- Report ID 的行为
- 声明和实际流量之间是否一致
- 下一步该改的是固件描述符、Report 打包,还是主机解析预期
这比"HID 设备不工作"有用得多。它把一个含糊的输入问题,变成了一个具体的 USB 契约不匹配。