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、蓝牙-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 报告完成。报告有以下三种类型
输入报告 (INPUT Report):输入报告提供从设备到主机的数据。此数据可能包括按钮事件、轴事件、电池状态等。此数据由设备生成,并发送到主机,无论是否需要显式请求。设备可以选择连续发送数据或仅在数据变化时发送。
输出报告 (OUTPUT Report):输出报告改变设备状态。它们从主机发送到设备,可能包括 LED 请求、震动请求等。输出报告从不从设备发送到主机,但主机可以检索其当前状态。主机可以选择连续发送输出报告或仅在数据变化时发送。
功能报告 (FEATURE Report):功能报告用于特定的静态设备功能,从不自发报告。主机可以读取和/或写入它们,以访问电池状态或设备设置等数据。功能报告从不未经请求发送。主机必须显式设置或检索功能报告。这也意味着,功能报告从不通过 intr 通道发送,因为此通道是异步的。
输入和输出报告可以作为纯数据报告在 intr 通道上传输。对于输入报告,这是常见的操作模式。但对于输出报告,这种情况很少发生,因为输出报告通常非常稀少。但设备可以自由地大量使用异步输出报告(例如,自定义 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 规范的设备,除了在设备初始化期间检索当前状态外,此请求不是必需的。GET_REPORT 请求可以发送给三种报告类型中的任何一种,并且应返回设备的当前报告状态。但是,如果规范不允许,作为有效载荷的 OUTPUT 报告可能会被底层传输驱动程序阻止。
SET_REPORT:SET_REPORT 请求将报告 ID 和数据作为有效载荷。它从主机发送到设备,设备必须根据给定数据更新其当前报告状态。可以使用三种报告类型中的任何一种。但是,如果规范不允许,作为有效载荷的 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)在调用 ->open() 后,当 HID 设备驱动程序不再对设备报告感兴趣时调用。(通常是用户空间关闭了驱动程序的任何输入设备时)。
如果所有 ->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)在再次调用 ->request() 之前由 HID 核心使用。如果一次只允许一个请求,传输驱动程序可以使用它来等待任何挂起请求完成。
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>