DIY蓝牙键盘(2) - 理解HID报文描述符

本文详细介绍了如何构建蓝牙键盘的HID报文描述符,包括HID报文描述符的结构、TLC描述、Report Id、Report Size/Count等关键要素。通过理解这些概念,读者能够根据键盘报文格式编写对应的HID描述符,实现与主机的有效通信。
摘要由CSDN通过智能技术生成

1. 前情回顾

上篇主要讲了键盘报文的分类与格式,并留下了一个问题:那主机为什么知道我这些报文的格式?那肯定是主机要提前知道我们发的报文的格式,那么问题就变成了:在发送报文前我们要怎么通知主机,让它知道我们报文的格式。

这篇将回答这个问题,主机如何知道键盘报文的格式。答案就是键盘将发送HID report descriptor(HID报文描述符)给主机,主机根据HID描述符就知道键盘的报文格式。

2. HID 报文描述符简介

我们先理解一下逻辑:

(1) 键盘通过发送报文描述符给主机,告诉主机它后面发出来的报文的格式

(2) 主机通过解析报文描述符,从而知道了键盘后面要发的报文的格式

(3) 键盘发送报文给主机

(4) 主机已经知道了报文的格式,收到报文后根据报文的内容做出相应的动作

这里隐含了一个问题:键盘发送的报文描述符,主机为什么能解析?

因为这个报文描述符是USB协会定义的,所有的主机都支持USB协会定义的这个描述符。从这也能感受到USB协会的强大,所有的主机都支持USB定义的HID报文描述符!!!这里的主机包括:Windows主机,macOS主机,Android主机等,总之是所有主机都支持。

于是我们的问题变成,怎么按照我们的报文格式写出对应的HID报文描述符?那自然就是要仔细阅读下面这两篇HID报文描述符相关的文档:

(1) HID描述符介绍: Microsoft Word - HID1_11.doc (usb.org)

(2) HID Usage表:https://usb.org/sites/default/files/hut1_22.pdf

对于初学者来说,直接上手这两篇文档很不友好,因为这两篇文档看起来比较吃力。当然功力扎实的朋友,直接从这两篇文档入手也是可以的。

为了让初学都抓住主干,我决定按我的理解给各位来个简单的入门。

3. HID报文描述符的结构

0报文描述符描述
1Usage Page (0x01),Generic Desktop Usage Page
2Usage (0x06),Keyboard Usage
3Collection (Application),集合开始
4Report Id(1),Report Id, 集合的报文都必须有一个report id
5Report Size (8),报文的大小,它的单位是一个bit。这里为8表示1个字节
6Report Count (4),报文的个数,上面的报文大小为一个字节,这里个数为4,就表示整个报文的长度为4个字节
7Usage Page (7),下面的报文usage在哪个usage page
8Usage Minimum (0),usage的最小值
9Usage Maximum (0xDD),usage的最大值
10Logical Minimum (0),逻辑的最小值
11Logical Maximum (0xFF),逻辑的最大值
12Input (Data, Variable, Absolute),报文的类型,键盘是把数据上报给主机,当然是input报文。
13End Collection集合结束

以上就是HID报文描述符的结构,基本上一个HID报文描述符都必须包含以上的这些元素。但我估计初学都看完还是一头雾水,下面我将带大家自上而下理解这个报文。

3.1 HID TLC 描述

第1、2、3行组成一个TLC(Top Level Collection), 这个TLC是用来让主机加载相应的驱动的。也就是说主机收到前3行的数据后,它会在设备管理器里生成一个设备结点,并把键盘的驱动挂到这个结点上。于是我们在主机上会看到下面中红框圈起来的结点,而且结点上已经加载了一个键盘驱动。是不是很简单,用3行描述符就可以让主机生成一个设备结点,并加载一个驱动到这个结点上。

以这个例子为例,主机已经创建了键盘结点并加载了键盘驱动,那么以后收到report id为1的报文,都由这个结点的键盘驱动来进行解析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KPpJPJFx-1635581808785)(C:\Users\86180\AppData\Roaming\Typora\typora-user-images\image-20211030115435849.png)]

稍微拓展一下,如果我要让主机生成一个鼠标的设备结点,那要怎么写描述符。根据(https://usb.org/sites/default/files/hut1_22.pdf)第30页的描述。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-00mJZ9Xy-1635581808787)(C:\Users\86180\AppData\Roaming\Typora\typora-user-images\image-20211030132857916.png)]

鼠标的HID TLC描述如下表,我们只需要把usage page的值改为1, usage改为2就可以了。

0报文描述符描述
1Usage Page (0x01),Generic Desktop Usage Page
2Usage (0x02),Mouse Usage
3Collection (Application),集合开始

所以如果我们要写一个写一个TLC, 重点就是要学会查表。 所有的HID usage page/usage都可以在(https://usb.org/sites/default/files/hut1_22.pdf)查到。比如我们要查一个keypad的TLC, 按下面的步骤来:

(1) 查找到"usage name"为keypad, 然后"usage types" 为CA的

(2) 然后我们找到Generic Desktop Page (0x01), Keypad(0x07)

(3) 写TLC

0报文描述符描述
1Usage Page (0x01),Generic Desktop Usage Page
2Usage (0x07),KeypadUsage
3Collection (Application),集合开始

3.2 HID Report Id

在报文描述符的第4行有一个report id, 这个report id就代表这一组的报文。 上面的报文report id为1, 主机如果收到report id是1的话,它就知道它收到的报文是一个键盘的report. 总之一句话,report id跟这个TLC(或是说这个报文)是绑定在一起的。

所以键盘发给主机的内容除了报文,还需要发给主机一个report id. 以上一篇介绍的报文为例,按下按键A, 键盘发给主机的报文应该为:

其中Byte 0表示report id, Byte 2表示字母A的usage数值。

Byte 0Byte 1Byte 2Byte 3Byte 4Byte 5
0x010x000x040x000x000x00

3.3 Report Size / Report Count

一个报文里面可以包括多种数据类型,比如上篇说听qwerty key和modifier key. qwerty key我们用4个字节来表示,用4个字节就是表示说同一个报文里可以向主机发送4个qwerty key. 那怎么与report size 与 report count对应起来呢?

(1) Report Size: 一个qwerty key我们想用一个字节来表示,于是report size要设为8. 因为report size的单位是1 bit.

(2) Report Count: 一个report里有4个qwerty key, 因此report count就是4.

我们可以类比为, report size就是数组的元素的大小,report count就是数组元素的个数。

我们来举一反三: 如何设置modifer key的 report size与 report count?

先来回顾一下上篇的内容, modifer key我们希望按如下的格式.

bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0
R WinR ShiftR AltR CtrlL WinL ShiftL AltL Ctrl

因为一个modifier key使用一个bit来表示,因此report size设置为1。 总共有8个modifier key, 因此report count设置为8.

3.4 Usage Min / Usage Max

在说usage之前,需要搞清楚usage与usage page的关系。 一个usage page里面有很多个usage, 不同的usage page里面的usage的值是可以相同的。因此在谈到usage的时候,要先指定usage page, 不然主机无法解析这个usage.

上表的第7-9行,就是用来表示usage. 首先它先指定usage page(0x07). 通过查表知道,它属于keyboard/keypad的page. (https://usb.org/sites/default/files/hut1_22.pdf)第82页。

然后第8-9行指定了usage的最小值与最大值,这有什么用?

7Usage Page (7),下面的报文usage在哪个usage page
8Usage Minimum (0),usage的最小值
9Usage Maximum (0xDD),usage的最大值

在上一篇里提到,我们用4个字节来表示4个qwerty key, 每个字节可用来表示一个qwerty key, 也就是每一个字节的值就是一个qwerty key的usage的值。 这里的Usage Minimum/Usage Maximum就表示这每一个字节的值的范围,即它的有效值是 0 - 0xDD.

为什么把值设定为0 - 0xDD呢? 查表知道, 0x00 - 0xDD已经能表示所以键盘的qwerty key的usage值了。

3.5 Logic Min / Logic Max

上表的第10-11行,表示每个字节的范围,这个可以和Usage Minimum/Usage Maximum设置成一样,即0 - 0xDD。但设置为0 - 0xFF, 因为一个字节可以表示的最大范围就是0 - 0xFF。

10Logical Minimum (0),逻辑的最小值
11Logical Maximum (0xFF),逻辑的最大值

3.6 Data Type

上表第12行用来表示报文数据的类型,键盘的报文数据有modifer key和qwerty key, 这两种数据都是键盘要发送给主机的,因此是input报文。如果是主机发给键盘的,而是output报文。另外qwerty key和modifier是用无符号数来表示的,因此是absolute属性。

12Input (Data, Variable, Absolute),报文的类型,键盘是把数据上报给主机,当然是input报文。

3.7 End Collection

一个集合的结束必须要加上end collection, 它与上表的第3行的collection必须是成对出现的。

4. 构建键盘的HID报文描述符

有了上面的知识基础,我们就可以来构建键盘的报文描述符了。我们希望编写一个HID报文描述符,让主机解析我们的报文格式如下:

Byte 0Byte 1Byte 2 - 5
Report IdModifier keyQwerty Key

Modifier key格式如下:

bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0
R WinR ShiftR AltR CtrlL WinL ShiftL AltL Ctrl

4.1 编写一个键盘的TLC

通过文档(https://usb.org/sites/default/files/hut1_22.pdf)查找到键盘的usage page为0x01, usage为0x06, 于是TLC的描述符如下:

0报文描述符描述
1Usage Page (0x01),Generic Desktop Usage Page
2Usage (0x06),Keyboard Usage
3Collection (Application),集合开始
4Report Id(1)键盘报文的report id
5End Collection集合结束

4.2 编写Modifier key描述符

(1) Report Size: 每个modifier key用一个bit来表示,因此report size为1.

(2) Report Count: 总共有8个modifier key,因此report count为8

(3) Usage Page: 0x07, Usage Minimum: 0xE0, Usage Maximum: 0xE7

通过文档(https://usb.org/sites/default/files/hut1_22.pdf) 搜索“LeftControl” 可以查到8个modifer key在usage page 7, usage 范围0xE0 - 0xE7.

(4) Logic Minimum: 0, Logic Maximum: 1

因为每个元素为1个bit, 最小值是0, 最大值是1。

于是得到modifier key的描述符如下:

0报文描述符描述
1Report Size (1),一个报文元素的大小,它的单位是一个bit
2Report Count (8),报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit
3Usage Page (7),下面的报文usage在哪个usage page
4Usage Minimum (0xE0),usage的最小值
5Usage Maximum (0xE7),usage的最大值
6Logical Minimum (0),逻辑的最小值
7Logical Maximum (1),逻辑的最大值
8Input (Data, Variable, Absolute),数据类型为输入

4.3编写Qwerty key描述符

(1) Report Size: 每个qwerty key用一个byte来表示,因此report size为8.

(2) Report Count: 一个report里面总共有4个qwerty key,因此report count为4

(3) Usage Page: 0x07, Usage Minimum: 0x04, Usage Maximum: 0xDD

通过文档(https://usb.org/sites/default/files/hut1_22.pdf) 搜索“Keyboard a and A” 可以查到键盘qwerty key在usage page 7, usage 范围0x04 - 0xDD.

(4) Logic Minimum: 0, Logic Maximum: 0xFF

因为每个元素为1个byte, 最小值是0, 最大值是0xFF。

于是得到modifier key的描述符如下:

0报文描述符描述
1Report Size (8),一个报文元素的大小,它的单位是一个bit
2Report Count (4),报文的个数,上面的报文大小为一个字节,这里个数为4,就表示整个报文的长度为4个字节
3Usage Page (7),下面的报文usage在哪个usage page
4Usage Minimum (0x04),usage的最小值
5Usage Maximum (0xDD),usage的最大值
6Logical Minimum (0),逻辑的最小值
7Logical Maximum (0xFF),逻辑的最大值
8Input (Data, Variable, Absolute),数据类型为输入

4.4 整合所有的描述符

把上面的TLC 描述符, modifer key描述符, qwerty的描述符组合起来如下:

0报文描述符描述
1Usage Page (0x01),Generic Desktop Usage Page
2Usage (0x06),Keyboard Usage
3Collection (Application),集合开始
4Report Id(1)键盘报文的report id
5Report Size (1),一个报文元素的大小,它的单位是一个bit
6Report Count (8),报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit
7Usage Page (7),下面的报文usage在哪个usage page
8Usage Minimum (0xE0),usage的最小值
9Usage Maximum (0xE7),usage的最大值
10Logical Minimum (0),逻辑的最小值
11Logical Maximum (1),逻辑的最大值
12Input (Data, Variable, Absolute),数据类型为输入
13Report Size (8),一个报文元素的大小,它的单位是一个bit
14Report Count (4),报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit
15Usage Page (7),下面的报文usage在哪个usage page
16Usage Minimum (0x04),usage的最小值
17Usage Maximum (0xDD),usage的最大值
18Logical Minimum (0),逻辑的最小值
19Logical Maximum (0xFF),逻辑的最大值
20Input (Data, Variable, Absolute),数据类型为输入
21End Collection集合结束

5. 描述符宏

键盘要发给主机的描述符是应该是一些整形数据才对,怎么上面的描述符是一些字符串加数字呢?

这主要是为了让大家好容易读与理解,比如说看到Report Id(1),就知道希望表示report id为1.其实Report Id是一个宏,最终发给主机的时候需要把这些宏展开的。

描述符宏展开宏后的值
Report Id(1)0x85, 0x01,
Report Size (1),0x75, 0x01
Report Count (8),0x95, 0x08
Usage Page (7),0x06, 0x07
Usage Minimum (0xE0),0x19, 0xE0
Usage Maximum (0xE7),0x29, 0xE8
Logical Minimum (0),0x15, 0x00
Logical Maximum (1),0x25, 0x01
Input (Data, Variable, Absolute),0x81, 0x00

我们在这篇最前面提到要看两篇文档,其中一篇 Microsoft Word - HID1_11.doc (usb.org)。 里面主要就是跟你讲report id, report count这些表要怎么表示。对于我们初学都来说,只要有像“Report Id, Report Count”这些宏可以使用,那就撇开这篇HID文档的介绍了。当然如果你要深入的话,肯定是要把这个文档啃几遍的。

这里我把一些常用的HID描述符的宏列在这里方便大家使用。

#define Physical                        (0x00U)
#define Undefined                       (0x00U)
#define Application                     (0x01U)
#define Logical                         (0x02U)

#define Data_Arr_Abs                    (0x00U)
#define Const_Arr_Abs                   (0x01U)
#define Data_Var_Abs                    (0x02U)
#define Const_Var_Abs                   (0x03U)
#define Data_Var_Rel                    (0x06U)
#define Data_Var_Abs_Null               (0x42U)
#define BuffBytes                       (0x01U)

#define HID_REPORT_ID(a)                0x85U,(a)
#define HID_USAGE(a)                    0x09U,(a)
#define HID_USAGE_SENSOR_DATA(a,b)      (a)|(b)     

#define HID_COLLECTION(a)               0xA1U,(a)
#define HID_REPORT_SIZE(a)              0x75U,(a)
#define HID_REPORT_COUNT(a)             0x95U,(a)
#define HID_REPORT_COUNT_16(a,b)        0x96U,(a),(b)
#define HID_UNIT_EXPONENT(a)            0x55U,(a)
#define HID_UNIT(a)                     0x65U,(a)

#define HID_USAGE_8(a)                  0x09U,(a)
#define HID_USAGE_16(a,b)               0x0AU,(a),(b)

#define HID_USAGE_PAGE_8(a)             0x05U,(a)
#define HID_USAGE_PAGE_16(a,b)          0x06U,(a),(b)

#define HID_USAGE_MIN_8(a)              0x19U,(a)
#define HID_USAGE_MIN_16(a,b)           0x1AU,(a),(b)

#define HID_USAGE_MAX_8(a)              0x29U,(a)
#define HID_USAGE_MAX_16(a,b)           0x2AU,(a),(b)

#define HID_LOGICAL_MIN_8(a)            0x15U,(a)
#define HID_LOGICAL_MIN_16(a,b)         0x16U,(a),(b)
#define HID_LOGICAL_MIN_32(a,b,c,d)     0x17U,(a),(b),(c),(d)

#define HID_LOGICAL_MAX_8(a)            0x25U,(a)
#define HID_LOGICAL_MAX_16(a,b)         0x26U,(a),(b)
#define HID_LOGICAL_MAX_32(a,b,c,d)     0x27U,(a),(b),(c),(d)

#define HID_PHYSICAL_MIN_8(a)           0x35U,(a)
#define HID_PHYSICAL_MIN_16(a,b)        0x36U,(a),(b)
#define HID_PHYSICAL_MIN_32(a,b,c,d)    0x37U,(a),(b),(c),(d)

#define HID_PHYSICAL_MAX_8(a)           0x45U,(a)
#define HID_PHYSICAL_MAX_16(a,b)        0x46U,(a),(b)
#define HID_PHYSICAL_MAX_32(a,b,c,d)    0x47U,(a),(b),(c),(d)

#define HID_INPUT_8(a)                  0x81U,(a)
#define HID_INPUT_16(a,b)               0x82U,(a),(b)
#define HID_INPUT_32(a,b,c,d)           0x83U,(a),(b),(c),(d)

#define HID_OUTPUT_8(a)                 0x91U,(a)
#define HID_OUTPUT_16(a,b)              0x92U,(a),(b)
#define HID_OUTPUT_32(a,b,c,d)          0x93U,(a),(b),(c),(d)

#define HID_FEATURE_8(a)                0xB1U,(a)
#define HID_FEATURE_16(a,b)             0xB2U,(a),(b)
#define HID_FEATURE_32(a,b,c,d)         0xB3U,(a),(b),(c),(d)

#define HID_END_COLLECTION              0xC0U

6. 总结

本篇主要讲了HID报文描述符,以及如何根据报文的格式编写报文描述符。这只是对HID报文描述符的入门,还有一些问题没有谈到。虽然这篇是入门篇,但至少看完这篇已经能自己编写描述符了,或者说已经能达到使用报文描述符的程度了。

下篇将继续深入报文描述符,讲一些本篇还没有讲到的一些点。


欢迎大家来评论与指正,你的点赞与评论将有助于作者改善文章质量,并继续前行

  • 12
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值