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 | 报文描述符 | 描述 |
---|---|---|
1 | Usage Page (0x01), | Generic Desktop Usage Page |
2 | Usage (0x06), | Keyboard Usage |
3 | Collection (Application), | 集合开始 |
4 | Report Id(1), | Report Id, 集合的报文都必须有一个report id |
5 | Report Size (8), | 报文的大小,它的单位是一个bit。这里为8表示1个字节 |
6 | Report Count (4), | 报文的个数,上面的报文大小为一个字节,这里个数为4,就表示整个报文的长度为4个字节 |
7 | Usage Page (7), | 下面的报文usage在哪个usage page |
8 | Usage Minimum (0), | usage的最小值 |
9 | Usage Maximum (0xDD), | usage的最大值 |
10 | Logical Minimum (0), | 逻辑的最小值 |
11 | Logical Maximum (0xFF), | 逻辑的最大值 |
12 | Input (Data, Variable, Absolute), | 报文的类型,键盘是把数据上报给主机,当然是input报文。 |
13 | End 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 | 报文描述符 | 描述 |
---|---|---|
1 | Usage Page (0x01), | Generic Desktop Usage Page |
2 | Usage (0x02), | Mouse Usage |
3 | Collection (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 | 报文描述符 | 描述 |
---|---|---|
1 | Usage Page (0x01), | Generic Desktop Usage Page |
2 | Usage (0x07), | KeypadUsage |
3 | Collection (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 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 |
---|---|---|---|---|---|
0x01 | 0x00 | 0x04 | 0x00 | 0x00 | 0x00 |
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 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|
R Win | R Shift | R Alt | R Ctrl | L Win | L Shift | L Alt | L 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的最小值与最大值,这有什么用?
7 | Usage Page (7), | 下面的报文usage在哪个usage page |
---|---|---|
8 | Usage Minimum (0), | usage的最小值 |
9 | Usage 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。
10 | Logical Minimum (0), | 逻辑的最小值 |
---|---|---|
11 | Logical Maximum (0xFF), | 逻辑的最大值 |
3.6 Data Type
上表第12行用来表示报文数据的类型,键盘的报文数据有modifer key和qwerty key, 这两种数据都是键盘要发送给主机的,因此是input报文。如果是主机发给键盘的,而是output报文。另外qwerty key和modifier是用无符号数来表示的,因此是absolute属性。
12 | Input (Data, Variable, Absolute), | 报文的类型,键盘是把数据上报给主机,当然是input报文。 |
---|
3.7 End Collection
一个集合的结束必须要加上end collection, 它与上表的第3行的collection必须是成对出现的。
4. 构建键盘的HID报文描述符
有了上面的知识基础,我们就可以来构建键盘的报文描述符了。我们希望编写一个HID报文描述符,让主机解析我们的报文格式如下:
Byte 0 | Byte 1 | Byte 2 - 5 |
---|---|---|
Report Id | Modifier key | Qwerty Key |
Modifier key格式如下:
bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|
R Win | R Shift | R Alt | R Ctrl | L Win | L Shift | L Alt | L Ctrl |
4.1 编写一个键盘的TLC
通过文档(https://usb.org/sites/default/files/hut1_22.pdf)查找到键盘的usage page为0x01, usage为0x06, 于是TLC的描述符如下:
0 | 报文描述符 | 描述 |
---|---|---|
1 | Usage Page (0x01), | Generic Desktop Usage Page |
2 | Usage (0x06), | Keyboard Usage |
3 | Collection (Application), | 集合开始 |
4 | Report Id(1) | 键盘报文的report id |
5 | End 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 | 报文描述符 | 描述 |
---|---|---|
1 | Report Size (1), | 一个报文元素的大小,它的单位是一个bit |
2 | Report Count (8), | 报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit |
3 | Usage Page (7), | 下面的报文usage在哪个usage page |
4 | Usage Minimum (0xE0), | usage的最小值 |
5 | Usage Maximum (0xE7), | usage的最大值 |
6 | Logical Minimum (0), | 逻辑的最小值 |
7 | Logical Maximum (1), | 逻辑的最大值 |
8 | Input (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 | 报文描述符 | 描述 |
---|---|---|
1 | Report Size (8), | 一个报文元素的大小,它的单位是一个bit |
2 | Report Count (4), | 报文的个数,上面的报文大小为一个字节,这里个数为4,就表示整个报文的长度为4个字节 |
3 | Usage Page (7), | 下面的报文usage在哪个usage page |
4 | Usage Minimum (0x04), | usage的最小值 |
5 | Usage Maximum (0xDD), | usage的最大值 |
6 | Logical Minimum (0), | 逻辑的最小值 |
7 | Logical Maximum (0xFF), | 逻辑的最大值 |
8 | Input (Data, Variable, Absolute), | 数据类型为输入 |
4.4 整合所有的描述符
把上面的TLC 描述符, modifer key描述符, qwerty的描述符组合起来如下:
0 | 报文描述符 | 描述 |
---|---|---|
1 | Usage Page (0x01), | Generic Desktop Usage Page |
2 | Usage (0x06), | Keyboard Usage |
3 | Collection (Application), | 集合开始 |
4 | Report Id(1) | 键盘报文的report id |
5 | Report Size (1), | 一个报文元素的大小,它的单位是一个bit |
6 | Report Count (8), | 报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit |
7 | Usage Page (7), | 下面的报文usage在哪个usage page |
8 | Usage Minimum (0xE0), | usage的最小值 |
9 | Usage Maximum (0xE7), | usage的最大值 |
10 | Logical Minimum (0), | 逻辑的最小值 |
11 | Logical Maximum (1), | 逻辑的最大值 |
12 | Input (Data, Variable, Absolute), | 数据类型为输入 |
13 | Report Size (8), | 一个报文元素的大小,它的单位是一个bit |
14 | Report Count (4), | 报文的个数,上面的报文大小为一个字节,这里个数为8,就表示整个报文的长度为8个bit |
15 | Usage Page (7), | 下面的报文usage在哪个usage page |
16 | Usage Minimum (0x04), | usage的最小值 |
17 | Usage Maximum (0xDD), | usage的最大值 |
18 | Logical Minimum (0), | 逻辑的最小值 |
19 | Logical Maximum (0xFF), | 逻辑的最大值 |
20 | Input (Data, Variable, Absolute), | 数据类型为输入 |
21 | End 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报文描述符的入门,还有一些问题没有谈到。虽然这篇是入门篇,但至少看完这篇已经能自己编写描述符了,或者说已经能达到使用报文描述符的程度了。
下篇将继续深入报文描述符,讲一些本篇还没有讲到的一些点。