HID 报告描述符简介

本章旨在概述 HID 报告描述符是什么,以及非内核程序员如何处理与 Linux 不能很好地配合使用的 HID 设备。

简介

HID 代表人机接口设备,它可以是您用来与计算机交互的任何设备,无论是鼠标、触摸板、平板电脑还是麦克风。

许多 HID 设备都可以开箱即用,即使它们的硬件不同。例如,鼠标可以有任意数量的按钮;它们可能有滚轮;不同型号之间的移动灵敏度不同,等等。尽管如此,大多数情况下一切正常工作,而无需在内核中为自 1970 年以来开发的每个鼠标型号编写专门的代码。

这是因为现代 HID 设备通过 *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 驱动程序来读取 HID 报告描述符 [1]

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))和一个滚轮(使用 (滚轮))

# 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 和滚轮匹配。

# 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-tools 中的 hid-recorder 工具来检查发送的值:点击并释放按钮 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 也需要输出。“输入”表示数据来自设备。

“功能”并非供最终用户使用,而是定义设备的配置选项。它们可以从主机查询;当声明为*易失性*时,应由主机更改。

集合、报告 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_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 中的示例。

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

编写专用驱动程序

这确实应该是您的最后手段。

脚注