第3章:HID Report Descriptor详解

好,咱们今天来啃一块硬骨头——HID Report Descriptor。说实话,我刚入行那会儿,看到这玩意儿也是一脸懵。一堆十六进制数,完全不知道在说什么。后来做蓝牙键盘项目,被这个描述符坑了整整两周,才真正搞明白它的门道。

你想想看,你的蓝牙键盘按下一个键,电脑怎么知道该做什么?靠的就是这个Report Descriptor。它就像一份说明书,告诉主机:「我是谁,我能干什么,我的数据长什么样」。没有它,主机就是个睁眼瞎。

3.1 Report Descriptor的结构

Report Descriptor本质上是一串字节流。每个字节或者几个字节组合在一起,构成一个Item。这些Item就像搭积木一样,一块一块拼出完整的描述。

Item分三种:

  • Short Item:最常见,1-3个字节搞定
  • Long Item:很少用,我做了这么多年基本没见过
  • Main Item:核心,定义Input/Output/Feature

每个Short Item的结构是这样的:

Bit 7-4: bTag (类型标签)
Bit 3-2: bType (大类)
Bit 1-0: bSize (数据长度)

bType有四种:

bType值类型说明
00MainInput/Output/Feature等核心定义
01Global全局设置,比如Usage Page、Report Size
10Local局部设置,比如Usage ID、Physical Descriptor
11Reserved保留,别碰

举个例子,0x05 0x01 这个序列:

  • 0x05 = 0000 0101 → bTag=0000, bType=01(Global), bSize=01(1字节数据)
  • 0x01 = Usage Page = Generic Desktop Controls

说白了,就是告诉主机:「接下来我要用通用桌面设备这个Usage Page了」。

我的小技巧:刚开始学的时候,别直接看十六进制。先用HID Descriptor Tool(HID DT)打开,图形化界面一目了然。等看懂了再回去看原始字节流,会豁然开朗。

3.2 Usage Page与Usage ID

这两个概念是整个HID协议的基石。我经常跟学员说,搞懂了这个,HID就学会了一半。

Usage Page 是分类,Usage ID 是具体功能。就像图书馆里,Usage Page是「计算机类」,Usage ID是「C语言编程」这本书。

常用的Usage Page:

Page ID名称常见用途
0x01Generic Desktop Controls鼠标、键盘、游戏杆
0x07Keyboard/Keypad键盘按键定义
0x08LED键盘指示灯
0x09Button按钮
0x0CConsumer多媒体按键

举个例子,键盘的A键:

  • Usage Page = 0x07 (Keyboard/Keypad)
  • Usage ID = 0x04 (Keyboard A)

鼠标的X轴:

  • Usage Page = 0x01 (Generic Desktop Controls)
  • Usage ID = 0x30 (X轴)

重点来了:同一个Usage ID在不同Usage Page下意义完全不同。比如0x01在Generic Desktop下是「指针」,在Keyboard下就是「Esc键」。千万别搞混了,我曾经见过有人把鼠标的Usage Page写成0x07,结果鼠标动不了,排查了半天才发现是Page写错了。

3.3 Input/Output/Feature Report定义

这三个是Main Item,决定了数据的流向:

  • Input:设备→主机。比如键盘按下的键值
  • Output:主机→设备。比如键盘的Num Lock灯
  • Feature:双向。设备配置参数,比如鼠标的DPI设置

每个Report都有一些属性位,用bit mask表示:

Bit位名称说明
0Data/Constant0=数据可变,1=常量(只读)
1Array/Variable0=数组,1=变量
2Absolute/Relative0=绝对,1=相对(鼠标用)
3No Wrap/Wrap0=不循环,1=循环
4Linear/Non-linear0=线性,1=非线性
5Preferred State/No Preferred0=有默认状态,1=无
6No Null/Null State0=无空值,1=有空值
7Non-volatile/Volatile0=非易失,1=易失(仅Feature用)

来个完整的键盘Report Descriptor例子:

0x05, 0x01,        // Usage Page (Generic Desktop)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x05, 0x07,        //   Usage Page (Keyboard)
0x19, 0xE0,        //   Usage Minimum (224)
0x29, 0xE7,        //   Usage Maximum (231)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs)
0x19, 0x00,        //   Usage Minimum (0)
0x29, 0x65,        //   Usage Maximum (101)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x65,        //   Logical Maximum (101)
0x75, 0x08,        //   Report Size (8)
0x95, 0x06,        //   Report Count (6)
0x81, 0x00,        //   Input (Data,Array,Abs)
0xC0               // End Collection

这段描述符定义了标准键盘的输入格式:

  • 前8个bit:修饰键(Ctrl、Shift、Alt、Win等)
  • 后面6个字节:最多同时按6个普通键

注意:键盘的修饰键用的是Variable模式(0x02),普通键用的是Array模式(0x00)。为什么?因为修饰键可以同时按多个,用bit位表示最方便。普通键用Array模式,每个字节存一个键值。这是USB HID规范规定的,别乱改。

3.4 实战:解析一个鼠标描述符

咱们来看一个典型的鼠标Report Descriptor:

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 (1)
0x29, 0x03,        //     Usage Maximum (3)
0x15, 0x00,        //     Logical Minimum (0)
0x25, 0x01,        //     Logical Maximum (1)
0x95, 0x03,        //     Report Count (3)
0x75, 0x01,        //     Report Size (1)
0x81, 0x02,        //     Input (Data,Var,Abs)
0x95, 0x01,        //     Report Count (1)
0x75, 0x05,        //     Report Size (5)
0x81, 0x01,        //     Input (Constant)
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)
0xC0,              //   End Collection
0xC0               // End Collection

这个描述符定义了一个3键鼠标:

  • 第1个字节:低3位是按键(左、右、中),高5位填充0
  • 第2个字节:X轴位移(相对值,-127到127)
  • 第3个字节:Y轴位移(相对值,-127到127)

注意看X和Y用的是Relative模式(0x06),因为鼠标是相对位移设备。如果是触摸屏,就得用Absolute模式了。

避坑指南:我曾经做过一个项目,鼠标滚轮怎么都不工作。查了半天,发现是忘了加滚轮的Usage ID(0x38)。加上之后,再定义Report Count和Report Size,问题就解决了。所以画描述符的时候,一定要把每个功能对应的Usage ID都列全了。

3.5 调试技巧

写Report Descriptor难免出错,我分享几个调试方法:

  1. 用HID Descriptor Tool验证:微软官方工具,能解析并显示描述符结构
  2. 用USBlyzer抓包:看主机实际收到的描述符是什么
  3. 用Wireshark分析:蓝牙HID的话,Wireshark能解析HID包
  4. 看dmesg日志:Linux下,内核会打印HID描述符解析错误

嗯,说到调试,我记得有一次在Windows上调试蓝牙键盘,描述符怎么改都不对。后来用USBlyzer一看,发现是Report ID没加。蓝牙HID必须要有Report ID,而USB HID可以没有。这个坑,我踩过,你别再踩了。

好了,这一章的内容就到这里。Report Descriptor是HID开发的核心,搞懂了它,后面写驱动就轻松多了。下一章咱们讲Report Protocol,看看数据到底是怎么在设备和主机之间传输的。