HID 報告描述符簡介

本章旨在概述 HID 報告描述符是什麼,以及非核心程式設計師如何處理與 Linux 配合不佳的 HID 裝置。

簡介

HID 代表人機介面裝置,可以是您用於與計算機互動的任何裝置,無論是滑鼠、觸控板、平板電腦還是麥克風。

許多 HID 裝置都可以開箱即用,即使它們的硬體不同。例如,滑鼠可以有任意數量的按鈕;它們可能有一個滾輪;不同型號之間的移動靈敏度不同,等等。儘管如此,大多數時候一切都正常工作,無需在核心中為自 1970 年以來開發的每個滑鼠型號編寫專用程式碼。

這是因為現代 HID 裝置確實透過 *HID 報告描述符* 公告了它們的功能,這是一組固定的位元組,描述了裝置和主機之間可能傳送的 *HID 報告* 以及這些報告中每個單獨位的含義。例如,HID 報告描述符可以指定“在 ID 為 3 的報告中,第 8 到 15 位是滑鼠的 delta x 座標”。

然後,HID 報告本身僅攜帶實際資料值,而沒有任何額外的元資訊。請注意,HID 報告可以從裝置傳送(“輸入報告”,即輸入事件),傳送到裝置(“輸出報告”,例如更改 LED),或者用於裝置配置(“特性報告”)。裝置可以支援一個或多個 HID 報告。

HID 子系統負責解析 HID 報告描述符,並將 HID 事件轉換為正常的輸入裝置介面(請參閱HID I/O 傳輸驅動程式)。裝置可能會出現故障,因為裝置提供的 HID 報告描述符是錯誤的,或者因為需要以特殊方式處理它,或者因為預設程式碼未處理某些特殊裝置或互動模式。

HID 報告描述符的格式由兩份文件描述,可從 USB Implementers Forum HID 網頁地址獲取

HID 子系統可以處理不同的傳輸驅動程式(USB、I2C、藍牙等)。請參閱HID I/O 傳輸驅動程式

解析 HID 報告描述符

當前 HID 裝置列表可以在 /sys/bus/hid/devices/ 中找到。對於每個裝置,例如 /sys/bus/hid/devices/0003\:093A\:2510.0002/,可以讀取相應的報告描述符

$ hexdump -C /sys/bus/hid/devices/0003\:093A\:2510.0002/report_descriptor
00000000  05 01 09 02 a1 01 09 01  a1 00 05 09 19 01 29 03  |..............).|
00000010  15 00 25 01 75 01 95 03  81 02 75 05 95 01 81 01  |..%.u.....u.....|
00000020  05 01 09 30 09 31 09 38  15 81 25 7f 75 08 95 03  |...0.1.8..%.u...|
00000030  81 06 c0 c0                                       |....|
00000034

可選:也可以透過直接訪問 hidraw 驅動程式 [1] 來讀取 HID 報告描述符。

HID 報告描述符的基本結構在 HID 規範中定義,而 HUT “定義了應用程式可以解釋的常量,以識別 HID 報告中資料欄位的用途和含義”。每個條目至少由兩個位元組定義,其中第一個位元組定義了後面值的型別,並在 HID 規範中描述,而第二個位元組攜帶實際值,並在 HUT 中描述。

原則上,可以手動逐位元組地仔細解析 HID 報告描述符。

如何在 手動解析 HID 報告描述符 中簡要介紹。如果您需要修補 HID 報告描述符,則只需理解它。

實際上,您不應該手動解析 HID 報告描述符;相反,您應該使用現有的解析器。在所有可用的解析器中

  • 線上 USB 描述符和請求解析器

  • hidrdd,它提供非常詳細且有些冗長的描述(如果您不熟悉 HID 報告描述符,則冗長可能很有用);

  • hid-tools,這是一個完整的實用程式集,允許您記錄和重放原始 HID 報告,以及除錯和重放 HID 裝置。它正在由 Linux HID 子系統維護者積極開發。

使用 hid-tools 解析滑鼠 HID 報告描述符會導致(解釋穿插)

$ ./hid-decode /sys/bus/hid/devices/0003\:093A\:2510.0002/report_descriptor
# device 0:0
# 0x05, 0x01,                    // Usage Page (Generic Desktop)        0
# 0x09, 0x02,                    // Usage (Mouse)                       2
# 0xa1, 0x01,                    // Collection (Application)            4
# 0x09, 0x01,                    // Usage (Pointer)                     6
# 0xa1, 0x00,                    // Collection (Physical)               8
# 0x05, 0x09,                    // Usage Page (Button)                10

以下是一個按鈕

# 0x19, 0x01,                    // Usage Minimum (1)                  12
# 0x29, 0x03,                    // Usage Maximum (3)                  14

第一個按鈕是按鈕 1,最後一個按鈕是按鈕 3

# 0x15, 0x00,                    // Logical Minimum (0)                16
# 0x25, 0x01,                    // Logical Maximum (1)                18

每個按鈕可以傳送從 0 到包括 1 的值(即它們是二進位制按鈕)

# 0x75, 0x01,                    // Report Size (1)                    20

每個按鈕都以一個位傳送

# 0x95, 0x03,                    // Report Count (3)                   22

並且有三個位(與三個按鈕匹配)

# 0x81, 0x02,                    // Input (Data,Var,Abs)               24

它是實際資料(不是常量填充),它們表示單個變數 (Var) 並且它們的值是絕對的(不是相對的);請參閱 HID 規範第 6.2.2.5 節“輸入、輸出和特性項”

# 0x75, 0x05,                    // Report Size (5)                    26

五個額外的填充位,需要達到一個位元組

# 0x95, 0x01,                    // Report Count (1)                   28

這五個位僅重複一次

# 0x81, 0x01,                    // Input (Cnst,Arr,Abs)               30

並採用常量 (Cnst) 值,即它們可以被忽略。

# 0x05, 0x01,                    // Usage Page (Generic Desktop)       32
# 0x09, 0x30,                    // Usage (X)                          34
# 0x09, 0x31,                    // Usage (Y)                          36
# 0x09, 0x38,                    // Usage (Wheel)                      38

滑鼠還有兩個物理位置(用法 (X)、用法 (Y))和一個滾輪(用法 (Wheel))

# 0x15, 0x81,                    // Logical Minimum (-127)             40
# 0x25, 0x7f,                    // Logical Maximum (127)              42

它們中的每一個都可以傳送從 -127 到包括 127 的值

# 0x75, 0x08,                    // Report Size (8)                    44

這由八位表示

# 0x95, 0x03,                    // Report Count (3)                   46

並且有三個八位,與 X、Y 和 Wheel 匹配。

# 0x81, 0x06,                    // Input (Data,Var,Rel)               48

這次資料值是相對的 (Rel),即它們表示與先前傳送的報告(事件)的更改

# 0xc0,                          // End Collection                     50
# 0xc0,                          // End Collection                     51
#
R: 52 05 01 09 02 a1 01 09 01 a1 00 05 09 19 01 29 03 15 00 25 01 75 01 95 03 81 02 75 05 95 01 81 01 05 01 09 30 09 31 09 38 15 81 25 7f 75 08 95 03 81 06 c0 c0
N: device 0:0
I: 3 0001 0001

此報告描述符告訴我們,滑鼠輸入將使用四個位元組傳輸:第一個位元組用於按鈕(使用三個位,五個用於填充),最後三個位元組分別用於滑鼠 X、Y 和滾輪更改。

實際上,對於任何事件,滑鼠都會發送一個四個位元組的 *報告*。我們可以透過例如 hid-recorder 工具檢查傳送的值,來自 hid-tools:單擊並釋放按鈕 1,然後單擊並釋放按鈕 2,然後單擊並釋放按鈕 3 傳送的位元組序列是

$ sudo ./hid-recorder /dev/hidraw1

....
output of hid-decode
....

#  Button: 1  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000000.000000 4 01 00 00 00
#  Button: 0  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000000.183949 4 00 00 00 00
#  Button: 0  1  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000001.959698 4 02 00 00 00
#  Button: 0  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000002.103899 4 00 00 00 00
#  Button: 0  0  1 | # | X:    0 | Y:    0 | Wheel:    0
E: 000004.855799 4 04 00 00 00
#  Button: 0  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000005.103864 4 00 00 00 00

此示例顯示,當單擊按鈕 2 時,將傳送位元組 02 00 00 00,並且緊隨其後的事件 (00 00 00 00) 是釋放按鈕 2(未按下任何按鈕,記住資料值是 *絕對的*)。

如果改為單擊並按住按鈕 1,然後單擊並按住按鈕 2,釋放按鈕 1,最後釋放按鈕 2,則報告為

#  Button: 1  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000044.175830 4 01 00 00 00
#  Button: 1  1  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000045.975997 4 03 00 00 00
#  Button: 0  1  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000047.407930 4 02 00 00 00
#  Button: 0  0  0 | # | X:    0 | Y:    0 | Wheel:    0
E: 000049.199919 4 00 00 00 00

其中 03 00 00 00 表示兩個按鈕都被按下,而隨後的 02 00 00 00 表示釋放按鈕 1,而按鈕 2 仍然有效。

輸出、輸入和特性報告

HID 裝置可以具有輸入報告(如滑鼠示例中)、輸出報告和特性報告。“輸出”表示資訊已傳送到裝置。例如,具有力反饋的操縱桿將具有一些輸出;鍵盤的 LED 也需要輸出。“輸入”表示資料來自裝置。

“特性”並非旨在供終端使用者使用,而是定義裝置的配置選項。可以從主機查詢它們;當宣告為 *Volatile* 時,應由主機更改它們。

集合、報告 ID 和 Evdev 事件

單個裝置可以將資料邏輯地分組到不同的獨立集合中,稱為 *集合*。集合可以巢狀,並且有不同型別的集合(有關詳細資訊,請參閱 HID 規範 6.2.2.6 節“集合、結束集合項”)。

不同的報告透過不同的 *報告 ID* 欄位來標識,即標識緊隨其後的報告結構的數字。只要需要報告 ID,它就會作為任何報告的第一個位元組傳輸。僅支援一個 HID 報告的裝置(如上面的滑鼠示例)可以省略報告 ID。

考慮以下 HID 報告描述符

05 01 09 02 A1 01 85 01 05 09 19 01 29 05 15 00
25 01 95 05 75 01 81 02 95 01 75 03 81 01 05 01
09 30 09 31 16 00 F8 26 FF 07 75 0C 95 02 81 06
09 38 15 80 25 7F 75 08 95 01 81 06 05 0C 0A 38
02 15 80 25 7F 75 08 95 01 81 06 C0 05 01 09 02
A1 01 85 02 05 09 19 01 29 05 15 00 25 01 95 05
75 01 81 02 95 01 75 03 81 01 05 01 09 30 09 31
16 00 F8 26 FF 07 75 0C 95 02 81 06 09 38 15 80
25 7F 75 08 95 01 81 06 05 0C 0A 38 02 15 80 25
7F 75 08 95 01 81 06 C0 05 01 09 07 A1 01 85 05
05 07 15 00 25 01 09 29 09 3E 09 4B 09 4E 09 E3
09 E8 09 E8 09 E8 75 01 95 08 81 02 95 00 81 01
C0 05 0C 09 01 A1 01 85 06 15 00 25 01 75 01 95
01 09 3F 81 06 09 3F 81 06 09 3F 81 06 09 3F 81
06 09 3F 81 06 09 3F 81 06 09 3F 81 06 09 3F 81
06 C0 05 0C 09 01 A1 01 85 03 09 05 15 00 26 FF
00 75 08 95 02 B1 02 C0

解析它之後(嘗試使用建議的工具自行解析它!),可以看到該裝置呈現了兩個 Mouse 應用程式集合(報告分別由報告 ID 1 和 2 標識)、一個 Keypad 應用程式集合(其報告由報告 ID 5 標識)和兩個 Consumer Controls 應用程式集合(報告 ID 分別為 6 和 3)。但是,請注意,對於同一個應用程式集合,裝置可以具有不同的報告 ID。

傳送的資料將以報告 ID 位元組開頭,然後是相應的資訊。例如,為最後一個消費者控制傳輸的資料

0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x03,        //   Report ID (3)
0x09, 0x05,        //   Usage (Headphone)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x75, 0x08,        //   Report Size (8)
0x95, 0x02,        //   Report Count (2)
0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection

將是三個位元組:第一個是報告 ID (3),接下來的兩個是耳機,帶有兩個 (Report Count (2)) 位元組 (Report Size (8)),每個位元組的範圍從 0 (Logical Minimum (0)) 到 255 (Logical Maximum (255))。

裝置傳送的所有輸入資料都應轉換為相應的 Evdev 事件,以便堆疊的其餘部分可以知道發生了什麼,例如,第一個按鈕的位轉換為 EV_KEY/BTN_LEFT evdev 事件,相對 X 移動轉換為 EV_REL/REL_X evdev 事件”。

事件

在 Linux 中,為每個 Application Collection 建立一個 /dev/input/event*。回到滑鼠示例,並重復單擊並按住按鈕 1,然後單擊並按住按鈕 2,釋放按鈕 1,最後釋放按鈕 2 的序列,您會得到

$ sudo libinput record /dev/input/event1
# libinput record
version: 1
ndevices: 1
libinput:
  version: "1.23.0"
  git: "unknown"
system:
  os: "opensuse-tumbleweed:20230619"
  kernel: "6.3.7-1-default"
  dmi: "dmi:bvnHP:bvrU77Ver.01.05.00:bd03/24/2022:br5.0:efr20.29:svnHP:pnHPEliteBook64514inchG9NotebookPC:pvr:rvnHP:rn89D2:rvrKBCVersion14.1D.00:cvnHP:ct10:cvr:sku5Y3J1EA#ABZ:"
devices:
- node: /dev/input/event1
  evdev:
    # Name: PixArt HP USB Optical Mouse
    # ID: bus 0x3 vendor 0x3f0 product 0x94a version 0x111
    # Supported Events:
    # Event type 0 (EV_SYN)
    # Event type 1 (EV_KEY)
    #   Event code 272 (BTN_LEFT)
    #   Event code 273 (BTN_RIGHT)
    #   Event code 274 (BTN_MIDDLE)
    # Event type 2 (EV_REL)
    #   Event code 0 (REL_X)
    #   Event code 1 (REL_Y)
    #   Event code 8 (REL_WHEEL)
    #   Event code 11 (REL_WHEEL_HI_RES)
    # Event type 4 (EV_MSC)
    #   Event code 4 (MSC_SCAN)
    # Properties:
    name: "PixArt HP USB Optical Mouse"
    id: [3, 1008, 2378, 273]
    codes:
      0: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] # EV_SYN
      1: [272, 273, 274] # EV_KEY
      2: [0, 1, 8, 11] # EV_REL
      4: [4] # EV_MSC
    properties: []
  hid: [
    0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x03,
    0x15, 0x00, 0x25, 0x01, 0x95, 0x08, 0x75, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31,
    0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0
  ]
  udev:
    properties:
    - ID_INPUT=1
    - ID_INPUT_MOUSE=1
    - LIBINPUT_DEVICE_GROUP=3/3f0/94a:usb-0000:05:00.3-2
  quirks:
  events:
  # Current time is 12:31:56
  - evdev:
    - [  0,      0,   4,   4,      30] # EV_MSC / MSC_SCAN                 30 (obfuscated)
    - [  0,      0,   1, 272,       1] # EV_KEY / BTN_LEFT                  1
    - [  0,      0,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +0ms
  - evdev:
    - [  1, 207892,   4,   4,      30] # EV_MSC / MSC_SCAN                 30 (obfuscated)
    - [  1, 207892,   1, 273,       1] # EV_KEY / BTN_RIGHT                 1
    - [  1, 207892,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +1207ms
  - evdev:
    - [  2, 367823,   4,   4,      30] # EV_MSC / MSC_SCAN                 30 (obfuscated)
    - [  2, 367823,   1, 272,       0] # EV_KEY / BTN_LEFT                  0
    - [  2, 367823,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +1160ms
  # Current time is 12:32:00
  - evdev:
    - [  3, 247617,   4,   4,      30] # EV_MSC / MSC_SCAN                 30 (obfuscated)
    - [  3, 247617,   1, 273,       0] # EV_KEY / BTN_RIGHT                 0
    - [  3, 247617,   0,   0,       0] # ------------ SYN_REPORT (0) ---------- +880ms

注意:如果您的系統上沒有 libinput record,請嘗試使用 evemu-record

何時出現問題

裝置行為不正確的原因有很多。例如

  • HID 裝置提供的 HID 報告描述符可能錯誤,因為例如

    • 它不遵循標準,因此核心將無法理解 HID 報告描述符;

    • HID 報告描述符 *與* 裝置實際傳送的內容不匹配(可以透過讀取原始 HID 資料來驗證);

  • HID 報告描述符可能需要一些“怪癖”(稍後會介紹)。

因此,可能不會為每個應用程式集合建立一個 /dev/input/event*,並且/或者那裡的事件可能與您期望的不符。

怪癖

核心知道如何修復 HID 裝置的一些已知特性 - 這些被稱為 HID 怪癖,並且可以在 include/linux/hid.h 中找到這些怪癖的列表。

如果出現這種情況,則只需在核心中為手頭的 HID 裝置新增所需的怪癖即可。這可以在檔案 drivers/hid/hid-quirks.c 中完成。在檢視該檔案後,如何完成它應該相對簡單。

當前定義的怪癖列表,來自 include/linux/hid.h,是

HID_QUIRK_NOTOUCH:
HID_QUIRK_IGNORE:忽略此裝置
HID_QUIRK_NOGET:
HID_QUIRK_HIDDEV_FORCE:
HID_QUIRK_BADPAD:
HID_QUIRK_MULTI_INPUT:
HID_QUIRK_HIDINPUT_FORCE:
HID_QUIRK_ALWAYS_POLL:
HID_QUIRK_INPUT_PER_APP:
HID_QUIRK_X_INVERT:
HID_QUIRK_Y_INVERT:
HID_QUIRK_IGNORE_MOUSE:
HID_QUIRK_SKIP_OUTPUT_REPORTS:
HID_QUIRK_SKIP_OUTPUT_REPORT_ID:
HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP:
HID_QUIRK_HAVE_SPECIAL_DRIVER:
HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE:
HID_QUIRK_IGNORE_SPECIAL_DRIVER
HID_QUIRK_FULLSPEED_INTERVAL:
HID_QUIRK_NO_INIT_REPORTS:
HID_QUIRK_NO_IGNORE:
HID_QUIRK_NO_INPUT_SYNC:

可以在載入 usbhid 模組時指定 USB 裝置的怪癖,請參閱 modinfo usbhid,儘管正確的修復應進入 hid-quirks.c 並且 已向上遊提交。有關如何提交補丁的指南,請參閱提交補丁:將您的程式碼提交到核心的基本指南。其他匯流排的怪癖需要進入 hid-quirks.c。

修復 HID 報告描述符

如果您需要修補 HID 報告描述符,最簡單的方法是求助於 eBPF,如 HID-BPF 中所述。

基本上,您可以更改原始 HID 報告描述符的任何位元組。samples/hid 中的示例應該是您的程式碼的一個很好的起點,請參閱例如 samples/hid/hid_mouse.bpf.c

SEC("fmod_ret/hid_bpf_rdesc_fixup")
int BPF_PROG(hid_rdesc_fixup, struct hid_bpf_ctx *hctx)
{
  ....
     data[39] = 0x31;
     data[41] = 0x30;
  return 0;
}

當然,這也可以在核心原始碼中完成,請參閱例如 drivers/hid/hid-aureal.cdrivers/hid/hid-samsung.c 以獲取稍微複雜的檔案。

如果您需要任何幫助來瀏覽 HID 手冊並瞭解 HID 報告描述符十六進位制數字的確切含義,請檢視 手動解析 HID 報告描述符

無論您提出什麼解決方案,請記住將 修復提交給 HID 維護者,以便它可以直接整合到核心中,並且該特定 HID 裝置將開始為其他人工作。有關如何執行此操作的指南,請參閱提交補丁:將您的程式碼提交到核心的基本指南

動態修改傳輸的資料

使用 eBPF 也可以修改與裝置交換的資料。再次參閱 samples/hid 中的示例。

再次,請釋出您的修復程式,以便它可以整合到核心中!

編寫專用驅動程式

這應該是您的最後手段。

腳註