第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值 | 类型 | 说明 |
|---|---|---|
| 00 | Main | Input/Output/Feature等核心定义 |
| 01 | Global | 全局设置,比如Usage Page、Report Size |
| 10 | Local | 局部设置,比如Usage ID、Physical Descriptor |
| 11 | Reserved | 保留,别碰 |
举个例子,0x05 0x01 这个序列:
- 0x05 = 0000 0101 → bTag=0000, bType=01(Global), bSize=01(1字节数据)
- 0x01 = Usage Page = Generic Desktop Controls
说白了,就是告诉主机:「接下来我要用通用桌面设备这个Usage Page了」。
3.2 Usage Page与Usage ID
这两个概念是整个HID协议的基石。我经常跟学员说,搞懂了这个,HID就学会了一半。
Usage Page 是分类,Usage ID 是具体功能。就像图书馆里,Usage Page是「计算机类」,Usage ID是「C语言编程」这本书。
常用的Usage Page:
| Page ID | 名称 | 常见用途 |
|---|---|---|
| 0x01 | Generic Desktop Controls | 鼠标、键盘、游戏杆 |
| 0x07 | Keyboard/Keypad | 键盘按键定义 |
| 0x08 | LED | 键盘指示灯 |
| 0x09 | Button | 按钮 |
| 0x0C | Consumer | 多媒体按键 |
举个例子,键盘的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位 | 名称 | 说明 |
|---|---|---|
| 0 | Data/Constant | 0=数据可变,1=常量(只读) |
| 1 | Array/Variable | 0=数组,1=变量 |
| 2 | Absolute/Relative | 0=绝对,1=相对(鼠标用) |
| 3 | No Wrap/Wrap | 0=不循环,1=循环 |
| 4 | Linear/Non-linear | 0=线性,1=非线性 |
| 5 | Preferred State/No Preferred | 0=有默认状态,1=无 |
| 6 | No Null/Null State | 0=无空值,1=有空值 |
| 7 | Non-volatile/Volatile | 0=非易失,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难免出错,我分享几个调试方法:
- 用HID Descriptor Tool验证:微软官方工具,能解析并显示描述符结构
- 用USBlyzer抓包:看主机实际收到的描述符是什么
- 用Wireshark分析:蓝牙HID的话,Wireshark能解析HID包
- 看dmesg日志:Linux下,内核会打印HID描述符解析错误
嗯,说到调试,我记得有一次在Windows上调试蓝牙键盘,描述符怎么改都不对。后来用USBlyzer一看,发现是Report ID没加。蓝牙HID必须要有Report ID,而USB HID可以没有。这个坑,我踩过,你别再踩了。
好了,这一章的内容就到这里。Report Descriptor是HID开发的核心,搞懂了它,后面写驱动就轻松多了。下一章咱们讲Report Protocol,看看数据到底是怎么在设备和主机之间传输的。