HID I/O 传输驱动程序

HID 子系统独立于底层传输驱动程序。最初,仅支持 USB,但其他规范采用了 HID 设计并提供了新的传输驱动程序。内核至少包括对 USB、蓝牙、I2C 和用户空间 I/O 驱动程序的支持。

1) HID 总线

HID 子系统被设计为总线。任何 I/O 子系统都可以提供 HID 设备并将其注册到 HID 总线。然后,HID 核心在其之上加载通用设备驱动程序。传输驱动程序负责原始数据传输和设备设置/管理。HID 核心负责报告解析、报告解释和用户空间 API。设备细节和怪癖由所有层处理,具体取决于怪癖。

+-----------+  +-----------+            +-----------+  +-----------+
| Device #1 |  | Device #i |            | Device #j |  | Device #k |
+-----------+  +-----------+            +-----------+  +-----------+
         \\      //                              \\      //
       +------------+                          +------------+
       | I/O Driver |                          | I/O Driver |
       +------------+                          +------------+
             ||                                      ||
    +------------------+                    +------------------+
    | Transport Driver |                    | Transport Driver |
    +------------------+                    +------------------+
                      \___                ___/
                          \              /
                         +----------------+
                         |    HID Core    |
                         +----------------+
                          /  |        |  \
                         /   |        |   \
            ____________/    |        |    \_________________
           /                 |        |                      \
          /                  |        |                       \
+----------------+  +-----------+  +------------------+  +------------------+
| Generic Driver |  | MT Driver |  | Custom Driver #1 |  | Custom Driver #2 |
+----------------+  +-----------+  +------------------+  +------------------+

示例驱动程序

  • I/O:USB、I2C、Bluetooth-l2cap

  • 传输:USB-HID、I2C-HID、BT-HIDP

此图表中,“HID 核心”以下的所有内容都已简化,因为它仅对 HID 设备驱动程序感兴趣。传输驱动程序不需要知道具体细节。

1.1) 设备设置

I/O 驱动程序通常会为传输驱动程序提供热插拔检测或设备枚举 API。传输驱动程序使用它来查找任何合适的 HID 设备。它们分配 HID 设备对象并将其注册到 HID 核心。传输驱动程序不需要向 HID 核心注册自己。HID 核心永远不知道有哪些可用的传输驱动程序,并且对此不感兴趣。它只对设备感兴趣。

传输驱动程序将一个常量 “struct hid_ll_driver” 对象附加到每个设备。一旦设备在 HID 核心中注册,HID 核心将使用通过此结构提供的回调函数与设备通信。

传输驱动程序负责检测设备故障和拔出。只要设备已注册,HID 核心就会操作设备,而不管任何设备故障如何。一旦传输驱动程序检测到拔出或故障事件,它们必须从 HID 核心取消注册设备,HID 核心将停止使用提供的回调。

1.2) 传输驱动程序要求

本文档中的术语“异步”和“同步”描述了关于确认的传输行为。异步通道不得执行任何同步操作,例如等待确认或验证。通常,在异步通道上操作的 HID 调用必须在原子上下文中正常运行。另一方面,同步通道可以由传输驱动程序以他们喜欢的任何方式实现。它们可能与异步通道相同,但它们也可以以阻塞方式提供确认报告、故障时自动重传等。如果异步通道需要此类功能,则传输驱动程序必须通过其自己的工作线程来实现。

HID 核心要求传输驱动程序遵循给定的设计。传输驱动程序必须为每个 HID 设备提供两个双向 I/O 通道。这些通道本身不一定在硬件中是双向的。传输驱动程序可能只提供 4 个单向通道。或者它可能会在单个物理通道上复用所有四个通道。但是,在本文档中,我们将它们描述为两个双向通道,因为它们有几个共同的属性。

  • 中断通道 (intr):intr 通道用于异步数据报告。在此通道上不发送任何管理命令或数据确认。任何未经请求的传入或传出数据报告都必须在此通道上发送,并且永远不会得到远程端的确认。设备通常在此通道上发送其输入事件。传出事件通常不会通过 intr 发送,除非需要高吞吐量。

  • 控制通道 (ctrl):ctrl 通道用于同步请求和设备管理。未经请求的数据输入事件不得在此通道上发送,并且通常会被忽略。相反,设备仅在此通道上发送管理事件或对主机请求的响应。控制通道用于直接阻塞对设备的查询,而与 intr 通道上的任何事件无关。传出报告通常通过同步 SET_REPORT 请求在 ctrl 通道上发送。

设备和 HID 核心之间的通信主要通过 HID 报告完成。报告可以是三种类型之一

  • 输入报告:输入报告提供从设备到主机的数据。此数据可能包括按钮事件、轴事件、电池状态等。此数据由设备生成并发送到主机,无论是否需要显式请求。设备可以选择连续发送数据或仅在更改时发送数据。

  • 输出报告:输出报告更改设备状态。它们从主机发送到设备,可能包括 LED 请求、震动请求等。输出报告永远不会从设备发送到主机,但主机可以检索其当前状态。主机可以选择连续发送输出报告或仅在更改时发送输出报告。

  • 功能报告:功能报告用于特定的静态设备功能,并且永远不会自发报告。主机可以读取和/或写入它们以访问诸如电池状态或设备设置之类的数据。功能报告永远不会在没有请求的情况下发送。主机必须显式设置或检索功能报告。这也意味着,功能报告永远不会在 intr 通道上发送,因为该通道是异步的。

INPUT 和 OUTPUT 报告可以作为纯数据报告在 intr 通道上发送。对于 INPUT 报告,这是通常的操作模式。但是对于 OUTPUT 报告,这种情况很少发生,因为 OUTPUT 报告通常非常稀少。但是设备可以自由地过度使用异步 OUTPUT 报告(例如,定制的 HID 音频扬声器就大量使用它)。

但是,普通报告不得在 ctrl 通道上发送。相反,ctrl 通道提供同步的 GET/SET_REPORT 请求。普通报告仅允许在 intr 通道上发送,并且是那里的唯一数据传输方式。

  • GET_REPORT:GET_REPORT 请求以报告 ID 作为负载,并从主机发送到设备。设备必须使用 ctrl 通道上请求的报告 ID 的数据报告作为同步确认进行响应。每个设备只能挂起一个 GET_REPORT 请求。HID 核心强制执行此限制,因为一些传输驱动程序不允许同时进行多个 GET_REPORT 请求。请注意,作为 GET_REPORT 请求的答案发送的数据报告不会被视为通用设备事件。也就是说,如果设备不在连续数据报告模式下运行,则对 GET_REPORT 的响应不会在状态更改时替换 intr 通道上的原始数据报告。GET_REPORT 仅由自定义 HID 设备驱动程序用于查询设备状态。通常,HID 核心会缓存任何设备状态,因此对于遵循 HID 规范的设备(除了在设备初始化期间检索当前状态外),此请求不是必需的。可以为 3 种报告类型中的任何一种发送 GET_REPORT 请求,并且应返回设备的当前报告状态。但是,如果规范不允许,则基础传输驱动程序可能会阻止作为有效负载的 OUTPUT 报告。

  • SET_REPORT:SET_REPORT 请求具有报告 ID 加数据作为负载。它从主机发送到设备,设备必须根据给定的数据更新其当前报告状态。可以使用 3 种报告类型中的任何一种。但是,如果规范不允许,则基础传输驱动程序可能会阻止作为有效负载的 INPUT 报告。设备必须使用同步确认进行响应。但是,HID 核心不要求传输驱动程序将此确认转发到 HID 核心。与 GET_REPORT 相同,一次只能挂起一个 SET_REPORT。HID 核心强制执行此限制,因为某些传输驱动程序不支持多个同步 SET_REPORT 请求。

USB-HID 支持其他 ctrl 通道请求,但在大多数其他传输级别规范中不可用(或已弃用)

  • GET/SET_IDLE:仅由 USB-HID 和 I2C-HID 使用。

  • GET/SET_PROTOCOL:HID 核心不使用。

  • RESET:由 I2C-HID 使用,未在 HID 核心中挂钩。

  • SET_POWER:由 I2C-HID 使用,未在 HID 核心中挂钩。

2) HID API

2.1) 初始化

传输驱动程序通常使用以下过程向 HID 核心注册新设备

struct hid_device *hid;
int ret;

hid = hid_allocate_device();
if (IS_ERR(hid)) {
        ret = PTR_ERR(hid);
        goto err_<...>;
}

strscpy(hid->name, <device-name-src>, sizeof(hid->name));
strscpy(hid->phys, <device-phys-src>, sizeof(hid->phys));
strscpy(hid->uniq, <device-uniq-src>, sizeof(hid->uniq));

hid->ll_driver = &custom_ll_driver;
hid->bus = <device-bus>;
hid->vendor = <device-vendor>;
hid->product = <device-product>;
hid->version = <device-version>;
hid->country = <device-country>;
hid->dev.parent = <pointer-to-parent-device>;
hid->driver_data = <transport-driver-data-field>;

ret = hid_add_device(hid);
if (ret)
        goto err_<...>;

一旦输入 hid_add_device(),HID 核心可能会使用 “custom_ll_driver” 中提供的回调。请注意,如果不支持,则底层传输驱动程序可以忽略诸如 “country” 之类的字段。

要取消注册设备,请使用

hid_destroy_device(hid);

一旦 hid_destroy_device() 返回,HID 核心将不再使用任何驱动程序回调。

2.2) hid_ll_driver 操作

可用的 HID 回调是

int (*start) (struct hid_device *hdev)

一旦 HID 设备驱动程序想要使用设备,就会调用此回调。传输驱动程序可以选择在此回调中设置其设备。但是,通常设备在传输驱动程序将其注册到 HID 核心之前就已经设置好,因此这主要仅由 USB-HID 使用。

void (*stop) (struct hid_device *hdev)

当 HID 设备驱动程序完成对设备的操作后,会调用此函数。传输驱动程序可以释放任何缓冲区并取消初始化设备。但请注意,如果另一个 HID 设备驱动程序加载到该设备上,则可能再次调用 ->start()

传输驱动程序可以忽略此函数,并在通过 hid_destroy_device() 销毁设备后取消初始化设备。

int (*open) (struct hid_device *hdev)

当 HID 设备驱动程序对数据报告感兴趣时,会调用此函数。通常,当用户空间没有打开任何输入 API 等时,设备驱动程序对设备数据不感兴趣,传输驱动程序可以将设备置于睡眠状态。但是,一旦调用 ->open(),传输驱动程序必须准备好进行 I/O 操作。对于每个打开 HID 设备的客户端,都会嵌套调用 ->open()。

void (*close) (struct hid_device *hdev)

当 HID 设备驱动程序调用 ->open() 后,但不再对设备报告感兴趣时,会调用此函数(通常是用户空间关闭了驱动程序的任何输入设备)。

如果所有 ->open() 调用都跟随着 ->close() 调用,传输驱动程序可以将设备置于睡眠状态并终止任何 I/O 操作。但是,如果设备驱动程序再次对输入报告感兴趣,则可能会再次调用 ->start()

int (*parse) (struct hid_device *hdev)

在调用 ->start() 后,设备设置期间会调用一次此函数。传输驱动程序必须从设备读取 HID 报告描述符,并通过 hid_parse_report() 将其告知 HID 核心。

int (*power) (struct hid_device *hdev, int level)

HID 核心调用此函数,向传输驱动程序提供 PM 提示。通常,这与 ->open() 和 ->close() 提示类似且是冗余的。

void (*request) (struct hid_device *hdev, struct hid_report *report,
                 int reqtype)

在 ctrl 通道上发送 HID 请求。“report”包含应发送的报告,“reqtype”包含请求类型。请求类型可以是 HID_REQ_SET_REPORT 或 HID_REQ_GET_REPORT。

此回调是可选的。如果未提供,HID 核心将按照 HID 规范组装一个原始报告,并通过 ->raw_request() 回调发送它。传输驱动程序可以异步实现此功能。

int (*wait) (struct hid_device *hdev)

HID 核心在再次调用 ->request() 之前使用此回调。如果一次只允许一个请求,传输驱动程序可以使用它来等待任何挂起的请求完成。

int (*raw_request) (struct hid_device *hdev, unsigned char reportnum,
                    __u8 *buf, size_t count, unsigned char rtype,
                    int reqtype)

与 ->request() 相同,但以原始缓冲区形式提供报告。此请求应为同步的。传输驱动程序不得使用 ->wait() 来完成此类请求。此请求是强制性的,如果缺少此请求,hid 核心将拒绝该设备。

int (*output_report) (struct hid_device *hdev, __u8 *buf, size_t len)

通过 intr 通道发送原始输出报告。某些 HID 设备驱动程序使用此方法,这些驱动程序需要 intr 通道上高吞吐量的输出请求。这不得导致 SET_REPORT 调用!必须将其实现为 intr 通道上的异步输出报告!

int (*idle) (struct hid_device *hdev, int report, int idle, int reqtype)

执行 SET/GET_IDLE 请求。仅由 USB-HID 使用,请勿实现!

2.3) 数据路径

传输驱动程序负责从 I/O 设备读取数据。它们必须自行处理任何与 I/O 相关的状态跟踪。HID 核心不实现协议握手或给定 HID 传输规范可能需要的其他管理命令。

从设备读取的每个原始数据包都必须通过 hid_input_report() 送入 HID 核心。您必须指定通道类型(intr 或 ctrl)和报告类型(输入/输出/特征)。在正常情况下,只有输入报告通过此 API 提供。

通过 ->request() 对 GET_REPORT 请求的响应也必须通过此 API 提供。对 ->raw_request() 的响应是同步的,必须由传输驱动程序拦截,而不是传递给 hid_input_report()。对 SET_REPORT 请求的确认不被 HID 核心关注。


编写于 2013 年,David Herrmann <dh.herrmann@gmail.com>