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 中的示例。

再次,请发布您的修复程序,以便它可以集成到内核中!

编写专用驱动程序

这应该是您的最后手段。

脚注