DIY蓝牙键盘(2) - 理解HID报文描述符
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報文描述符的結構
| 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就可以了。
| 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
| 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數值。
| 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我們希望按如下的格式.
| 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的最小值與最大值,這有什么用?
| 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。
| 11 | Logical Maximum (0xFF), | 邏輯的最大值 |
3.6 Data Type
上表第12行用來表示報文數據的類型,鍵盤的報文數據有modifer key和qwerty key, 這兩種數據都是鍵盤要發送給主機的,因此是input報文。如果是主機發給鍵盤的,而是output報文。另外qwerty key和modifier是用無符號數來表示的,因此是absolute屬性。
3.7 End Collection
一個集合的結束必須要加上end collection, 它與上表的第3行的collection必須是成對出現的。
4. 構建鍵盤的HID報文描述符
有了上面的知識基礎,我們就可以來構建鍵盤的報文描述符了。我們希望編寫一個HID報文描述符,讓主機解析我們的報文格式如下:
| Report Id | Modifier key | Qwerty Key |
Modifier key格式如下:
| 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的描述符如下:
| 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的描述符如下:
| 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的描述符如下:
| 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的描述符組合起來如下:
| 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 0xC0U6. 總結
本篇主要講了HID報文描述符,以及如何根據報文的格式編寫報文描述符。這只是對HID報文描述符的入門,還有一些問題沒有談到。雖然這篇是入門篇,但至少看完這篇已經能自己編寫描述符了,或者說已經能達到使用報文描述符的程度了。
下篇將繼續深入報文描述符,講一些本篇還沒有講到的一些點。
歡迎大家來評論與指正,你的點贊與評論將有助于作者改善文章質量,并繼續前行
總結
以上是生活随笔為你收集整理的DIY蓝牙键盘(2) - 理解HID报文描述符的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么用python画直线_python怎
- 下一篇: 百度地图获取本地搜索(LocalSear