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>