第4章:HID Report Descriptor实战

好,咱们今天来点真家伙。前面讲了那么多HID协议的理论,说实话,不亲手写一个Report Descriptor,你永远都觉得自己懂了,但一上手就懵。我当年就是这样,看了三遍规范,结果写第一个键盘描述符时,愣是折腾了一整天。

这一章,我们就直接动手。写两个最常用的描述符:键盘的8字节数组,鼠标的4字节数组。最后再用HID Descriptor Tool验证一下,确保我们写的没错。

4.1 键盘Report Descriptor(8字节)

先说说键盘。标准键盘报告是8个字节,这个结构大家应该不陌生:

  • 字节0:修饰键(Ctrl、Shift、Alt、Win等)
  • 字节1:保留(通常为0)
  • 字节2-7:6个按键的键值(最多同时按6个键)

嗯,这里要注意。为什么是6个键?不是8个也不是4个?这是USB HID规范里定义的"标准键盘报告长度"。我在项目中遇到过,有些客户非要改成8个键同时按下,结果发现很多操作系统根本不认,最后还是老老实实改回6个。

好,我们直接看代码。下面这个就是标准的键盘Report Descriptor:

0x05, 0x01,        // Usage Page (Generic Desktop)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x05, 0x07,        //   Usage Page (Keyboard/Keypad)
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, 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)
0x95, 0x01,        //   Report Count (1)
0x75, 0x08,        //   Report Size (8)
0x81, 0x01,        //   Input (Const,Array,Abs)
0x95, 0x06,        //   Report Count (6)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x65,        //   Logical Maximum (101)
0x05, 0x07,        //   Usage Page (Keyboard/Keypad)
0x19, 0x00,        //   Usage Minimum (0)
0x29, 0x65,        //   Usage Maximum (101)
0x81, 0x00,        //   Input (Data,Array,Abs)
0xC0               // End Collection

这段代码一共多少字节?我数一下:从0x05到0xC0,总共...嗯,67个字节。但注意,这是描述符的"定义",不是报告本身。报告本身只有8个字节。

你可能会问:为什么描述符这么长,报告却这么短?说白了,描述符是在告诉主机"我的报告长什么样",而报告是实际发送的数据。描述符写得越详细,主机解析就越准确。

关键点:描述符中的Usage Minimum和Usage Maximum定义了按键的HID Usage ID范围。0xE0到0xE7对应的是修饰键(Ctrl左、Shift左、Alt左、Win左、Ctrl右、Shift右、Alt右、Win右)。0x00到0x65对应的是标准按键(A、B、C...等101个键值)。

4.2 鼠标Report Descriptor(4字节)

鼠标就简单多了。标准鼠标报告是4个字节:

  • 字节0:按键状态(Bit0=左键,Bit1=右键,Bit2=中键)
  • 字节1:X轴位移(有符号,-127到127)
  • 字节2:Y轴位移(有符号,-127到127)
  • 字节3:滚轮(有符号,-127到127)

我个人习惯把鼠标描述符写得尽量简洁。你看下面这个:

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)
0x75, 0x01,        //     Report Size (1)
0x95, 0x03,        //     Report Count (3)
0x81, 0x02,        //     Input (Data,Var,Abs)
0x95, 0x01,        //     Report Count (1)
0x75, 0x05,        //     Report Size (5)
0x81, 0x01,        //     Input (Const,Array,Abs)
0x05, 0x01,        //     Usage Page (Generic Desktop)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x09, 0x38,        //     Usage (Wheel)
0x15, 0x81,        //     Logical Minimum (-127)
0x25, 0x7F,        //     Logical Maximum (127)
0x75, 0x08,        //     Report Size (8)
0x95, 0x03,        //     Report Count (3)
0x81, 0x06,        //     Input (Data,Var,Rel)
0xC0,              //   End Collection
0xC0               // End Collection

这段描述符定义了4字节的报告。注意看,前3个bit是按键,后面5个bit是填充(常量)。然后X、Y、Wheel各占一个字节,都是有符号的相对值。

实战技巧:鼠标的X和Y轴用的是相对值(Rel),不是绝对值(Abs)。这意味着鼠标报告的是"移动了多少",而不是"移动到了哪里"。这一点和触摸屏、手写板完全不同。我在做游戏鼠标驱动时,就见过有人把鼠标设成绝对值,结果光标满屏乱跳。

4.3 使用HID Descriptor Tool验证

写完了描述符,怎么知道对不对?别靠猜。HID Descriptor Tool是微软官方提供的工具,专门用来验证和调试HID描述符。

我记得第一次用这个工具时,发现我写的描述符有个小错误——Report Count和Report Size的顺序搞反了。工具直接报错,省了我半天调试时间。

使用步骤很简单:

  1. 打开HID Descriptor Tool
  2. 点击File → New,创建一个新描述符
  3. 在编辑区输入我们上面写的字节数组
  4. 点击Parse按钮,工具会自动解析
  5. 查看解析结果,确认每个字段都正确

工具解析后,会显示类似这样的信息:

Item Tag: Usage Page (Generic Desktop)
Item Tag: Usage (Keyboard)
Item Tag: Collection (Application)
  Item Tag: Usage Page (Keyboard/Keypad)
  Item Tag: Usage Minimum (224)
  Item Tag: Usage Maximum (231)
  ...

如果解析结果和你预期的一致,那就说明描述符写对了。如果工具报错,它会告诉你具体是哪一行有问题。比如"Invalid item size"或者"Unexpected end of descriptor"。

常见错误

  • Report Size和Report Count写反了——工具会报"Invalid report size"
  • Logical Minimum大于Logical Maximum——工具会报"Logical range error"
  • Collection没有正确关闭——工具会报"Unmatched collection"
  • Usage Page和Usage不匹配——比如在Keyboard Usage Page下用了Mouse的Usage

我曾经遇到过一个坑:写鼠标描述符时,把滚轮的Usage设成了0x39(其实是0x38)。工具没报错,但Windows就是不认滚轮。后来查了半天,才发现是Usage写错了。所以,工具能帮你检查语法错误,但语义错误还得靠你自己。

4.4 小结

这一章我们实战了两个最常用的HID Report Descriptor。键盘的8字节描述符,鼠标的4字节描述符,都是蓝牙HID设备的基础。你想想看,市面上90%的蓝牙键盘鼠标,用的都是这两个模板。

下一章,我们会把这些描述符真正烧进芯片里,配合蓝牙协议栈,让设备跑起来。到时候你会发现,描述符写对了,后面的事情就顺了。写错了...嗯,那就等着抓包调试吧。

好,今天就到这里。动手试试,把这两个描述符敲进HID Descriptor Tool里,看看解析结果。有任何问题,咱们下节课见。