USB 裝置驅動程式在現代電腦系統中扮演著重要的角色,負責連線各種 USB 裝置並進行資料傳輸。理解 USB 描述符的結構和作用對於開發高效能的 USB 裝置至關重要。本文除了介紹介面、端點、字串等標準描述符外,也涵蓋了 HID 描述符的解析,並探討 Linux USB 子系統的架構和驅動程式開發的關鍵環節,包含驅動程式註冊、電源管理、URB 機制以及重要的資料結構如 usb_driverusb_interfacestruct urb。這些知識對於理解和開發 Linux USB 驅動程式至關重要,能幫助開發者更好地掌握 USB 裝置的運作機制。

USB 裝置驅動程式深度解析

USB(Universal Serial Bus)介面已成為現代電腦系統中不可或缺的元件,其廣泛應用於各種裝置的連線與資料傳輸。深入瞭解USB裝置驅動程式的內部結構與運作機制,是開發高效能USB裝置的關鍵。

USB 介面描述符(USB_INTERFACE_DESCRIPTOR)

USB介面描述符是定義USB裝置介面特性的重要結構,其主要功能是描述裝置的介面組態。以下是一個典型的USB介面描述符結構範例:

typedef struct __attribute__ ((packed))
{
    uint8_t bLength;            // 描述符長度(單位:位元組)
    uint8_t bDescriptorType;    // 描述符型別(介面描述符 = 0x04)
    uint8_t bInterfaceNumber;   // 介面編號
    uint8_t bAlternateSetting;  // 替代設定值
    uint8_t bNumEndpoints;      // 端點數量(不包含端點0)
    uint8_t bInterfaceClass;    // 介面類別
    uint8_t bInterfaceSubClass; // 介面子類別
    uint8_t bInterfaceProtocol; // 介面協定
    uint8_t iInterface;         // 描述此介面的字串描述符索引
} USB_INTERFACE_DESCRIPTOR;

內容解密:

  1. bLength:表示此描述符的總位元組數。
  2. bDescriptorType:固定為0x04,表示這是一個介面描述符。
  3. bInterfaceNumber:指定此介面的編號,用於區分多個介面。
  4. bAlternateSetting:用於切換介面的不同設定。
  5. bNumEndpoints:指出此介面使用的端點數量(不包括控制端點0)。
  6. bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol:定義了介面的類別、子類別和協定,用於驅動程式辨識裝置型別。
  7. iInterface:指向描述此介面的字串描述符索引值。

USB 端點描述符(USB_ENDPOINT_DESCRIPTOR)

端點描述符用於定義非控制端點(即端點0以外)的傳輸特性。結構如下:

typedef struct __attribute__ ((packed))
{
    uint8_t bLength;            // 描述符長度(7位元組)
    uint8_t bDescriptorType;    // 端點描述符(0x05)
    uint8_t bEndpointAddress;   // 端點位址
    uint8_t bmAttributes;       // 傳輸型別屬性
    uint16_t wMaxPacketSize;    // 最大封包大小
    uint8_t bInterval;          // 輪詢間隔
} USB_ENDPOINT_DESCRIPTOR;

內容解密:

  1. bEndpointAddress:定義端點的位址,其中低4位表示端點編號,第7位表示傳輸方向(0=輸出,1=輸入)。
  2. bmAttributes:指定傳輸型別(控制、同步、中斷或批次傳輸)。對於同步傳輸,還需定義同步和用法型別。
    • 位元0-1:傳輸型別(00=控制,01=同步,10=批次,11=中斷)
    • 位元2-7:保留或用於同步傳輸的其他屬性設定。
  3. wMaxPacketSize:指定此端點可傳輸的最大封包大小。
  4. bInterval:定義端點資料傳輸的輪詢間隔,單位為幀(1ms或125μs,取決於裝置速度)。

USB 字串描述符(USB_STRING_DESCRIPTOR)

字串描述符提供可讀的裝置資訊,支援多語言。結構如下:

typedef struct __attribute__ ((packed))
{
    uint8_t bLength;            // 描述符長度
    uint8_t bDescriptorType;    // 字串描述符(0x03)
    uint16_t bString[1];        // UNICODE字串
} USB_STRING_DESCRIPTOR;

內容解密:

  1. bLength:字串描述符的總長度。
  2. bDescriptorType:固定為0x03,表示這是字串描述符。
  3. bString:UNICODE編碼的字串內容,用於提供裝置相關資訊。

USB HID 描述符

USB HID(Human Interface Device)裝置類別支援多種人機介面裝置,如鍵盤、滑鼠等。HID描述符結構如下:

typedef struct __attribute__((packed))
{
    uint8_t bLength;            // 描述符長度
    uint8_t bDescriptorType;    // HID描述符型別(0x21)
    uint16_t bcdHID;            // HID規範版本
    uint8_t bCountryCode;       // 國家程式碼
    uint8_t bNumDescriptors;    // 下屬描述符數量
    uint8_t bReportDescriptorType; // 報告描述符型別
    uint16_t wItemLength;       // 報告描述符長度
} USB_HID_DESCRIPTOR;

內容解密:

  1. bcdHID:表示裝置遵循的HID規範版本,以二進位制編碼的十進位制數字表示。
  2. bCountryCode:指定裝置所針對的國家,若設為0x00則表示無特定國家限制。
  3. bNumDescriptors:指出此HID組態包含的報告描述符數量。
  4. bReportDescriptorType和wItemLength:描述報告描述符的型別和長度,用於定義HID裝置的資料格式。

USB 裝置驅動程式開發

HID 描述符與報告描述符詳解

在 USB 裝置驅動程式開發中,HID(Human Interface Device)描述符扮演著至關重要的角色。其中,bReportDescriptorType 欄位指定了緊隨其後的描述符型別。例如,當該值為 0x22 時,表示後續的描述符為報告描述符(Report Descriptor)。wItemLength 欄位則告知主機後續描述符的大小。

HID 報告描述符是一組硬編碼的位元組陣列,用於描述裝置的資料封包結構。這包括裝置支援的封包數量、封包大小,以及每個位元組和位元在封包中的用途。例如,一個帶有計算器程式按鈕的鍵盤可以透過報告描述符告知主機,按鈕的按下/釋放狀態儲存在資料封包編號 4 的第 6 個位元組中的第 2 個位元。

Linux USB 子系統架構

Linux 核心從 2.2 版本開始支援 USB,並持續發展至今。除了對新一代 USB 的支援外,各種主機控制器也獲得了支援,同時新增了周邊裝置的驅動程式,並引入了進階功能,如延遲測量和改進的電源管理。

在 Linux 中,「USB Core」是一個特定的 API,用於支援 USB 周邊裝置和主機控制器。該 API 透過定義一系列資料結構、巨集和函式,抽象化了所有硬體。USB 裝置的主機端驅動程式與這些「usbcore」API 進行互動。API 分為兩組:一組用於通用 USB 裝置驅動程式,另一組用於核心驅動程式,如管理 USB 裝置樹的集線器驅動程式和控制個別匯流排的 USB 主機介面卡驅動程式。

Linux USB API 特性

  • 支援同步呼叫,用於控制和批次訊息傳輸。
  • 支援非同步呼叫,適用於所有型別的資料傳輸,透過使用稱為「URBs」(USB Request Blocks)的請求結構實作。

只有主機控制器裝置(HCDs)驅動程式實際與硬體互動(讀取/寫入暫存器、處理中斷請求等)。理論上,所有 HCDs 透過相同的 API 提供相同的功能。實際上,雖然這種情況正變得越來越真實,但仍然存在差異,尤其是在較不常見的控制器上處理故障時。

編寫 Linux USB 裝置驅動程式

在接下來的實驗中,您將開發多個 Linux 驅動程式,以瞭解 Linux USB 裝置驅動程式的基本框架。在進行實驗之前,將介紹主要的 Linux USB 資料結構和函式。

USB 裝置驅動程式註冊

Linux USB 裝置驅動程式首先需要向 Linux USB 核心註冊,提供有關該驅動程式支援哪些裝置以及當支援的裝置插入或移除系統時呼叫哪些函式的資訊。所有這些資訊都透過 usb_driver 結構傳遞給 USB 核心。

static struct usb_driver sevseg_driver = {
    .name = "usbsevseg",
    .probe = sevseg_probe,
    .disconnect = sevseg_disconnect,
    .suspend = sevseg_suspend,
    .resume = sevseg_resume,
    .reset_resume = sevseg_reset_resume,
    .id_table = id_table,
};

關鍵函式與回呼

  • probe():當 USB 核心偵測到與 id_table 中資訊相符的裝置時呼叫,用於判斷驅動程式是否願意管理該裝置的特定介面。
  • disconnect():當介面不再可存取時呼叫,通常是因為裝置被斷開連線或驅動程式模組被解除安裝。

電源管理回呼

  • suspend:當裝置即將進入掛起狀態時呼叫。
  • resume:當裝置被還原時呼叫。
  • reset_resume:當掛起的裝置被重置而非還原時呼叫。

裝置層級操作

  • pre_reset:當裝置即將被重置時呼叫。
  • post_reset:當裝置重置完成後呼叫。

詳細解析 usb_driver 結構

usb_driver 結構定義了 USB 裝置驅動程式的核心操作和屬性。其中,.name 欄位用於描述驅動程式,並在系統日誌中顯示。.probe.disconnect 回呼函式分別在裝置插入和移除時被呼叫。

probe() 函式詳解

int (*probe)(struct usb_interface *intf, const struct usb_device_id *id);

該函式由 USB 核心呼叫,以判斷驅動程式是否願意管理特定的介面。如果願意管理,則傳回零並使用 usb_set_intfdata() 將驅動程式特定的資料與介面關聯。如果不願意管理,則傳回 -ENODEV;如果發生真實的 IO 錯誤,則傳回適當的負 errno 值。

disconnect() 回呼詳解

void disconnect(struct usb_device *dev, void *drv_context);

當介面不再可存取時呼叫,通常是因為裝置被斷開或驅動程式模組被解除安裝。

USB 裝置驅動程式深度解析

USB 裝置驅動程式使用 ID 表來支援熱插拔功能。usb_driver 結構中的 id_table 指向一個 usb_device_id 結構陣列,用於宣告該驅動程式支援的裝置。大多數驅動程式使用 USB_DEVICE() 巨集來建立 usb_device_id 結構,並透過 MODULE_DEVICE_TABLE(usb, xxx) 巨集向 USB 核心註冊。

USB 裝置識別與序號產生器制

以下程式碼範例(取自 drivers/usb/misc/usbsevseg.c 驅動程式)展示瞭如何建立和註冊 USB 裝置:

#define VENDOR_ID 0x0fc5
#define PRODUCT_ID 0x1227

static const struct usb_device_id id_table[] = {
    { USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
    { },
};

MODULE_DEVICE_TABLE(usb, id_table);

內容解密:

  1. VENDOR_IDPRODUCT_ID 定義了特定的供應商 ID 和產品 ID,用於識別 USB 裝置。
  2. usb_device_id 陣列使用 USB_DEVICE() 巨集建立,指定驅動程式支援的裝置。
  3. MODULE_DEVICE_TABLE(usb, id_table) 將裝置 ID 表註冊到 USB 核心,使其在裝置插入時能夠正確匹配驅動程式。

Linux 主機端資料結構解析

USB 裝置驅動程式實際上繫結到介面(interface)而非裝置本身。usb_interface 結構代表 USB 介面,是驅動程式與裝置互動的核心資料結構。

usb_interface 結構關鍵成員:

  • altsetting:包含多個可選的介面設定,每個設定對應不同的端點組態。
  • cur_altsetting:目前使用的介面設定。
  • num_altsetting:可用的替代設定數量。
struct usb_interface {
    struct usb_host_interface *altsetting;
    struct usb_host_interface *cur_altsetting;
    unsigned num_altsetting;
    // ... 其他成員
};

內容解密:

  1. altsetting 陣列儲存了所有可用的替代設定,驅動程式可透過 usb_set_interface() 切換設定。
  2. cur_altsetting 指向目前使用的設定,方便驅動程式存取當前端點組態。
  3. 替代設定常用於控制週期性端點的頻寬使用,符合 USB 標準的裝置會在非預設設定中使用等時端點。

端點描述符與資料傳輸

每個介面設定包含多個端點描述符,用於定義資料傳輸的特性。usb_host_endpoint 結構包裝了 usb_endpoint_descriptor,提供了端點的詳細資訊。

端點描述符結構:

struct usb_endpoint_descriptor {
    __u8 bLength;
    __u8 bDescriptorType;
    __u8 bEndpointAddress;
    __u8 bmAttributes;
    __le16 wMaxPacketSize;
    __u8 bInterval;
    // ... 其他成員
} __attribute__ ((packed));

內容解密:

  1. bEndpointAddress 指定了端點的位址和方向(IN 或 OUT)。
  2. bmAttributes 定義了端點的傳輸型別(控制、批次、中斷或等時)。
  3. wMaxPacketSize 指定了端點的最大封包大小,直接影響資料傳輸效率。

USB 請求區塊(URB)機制

USB 資料傳輸透過 USB 請求區塊(URB)進行非同步操作。URB 包含了執行 USB 交易所需的所有資訊,並在完成時呼叫回呼函式。

URB 操作流程:

  1. 使用 usb_submit_urb() 提交 URB 請求,核心會非同步執行傳輸操作。
  2. 可透過 usb_unlink_urb() 取消佇列中的 URB 請求。
  3. 每個 URB 包含一個完成處理函式,在傳輸完成或取消時被呼叫。

內容解密:

  1. URB 的非同步特性允許驅動程式在等待傳輸完成的同時處理其他任務。
  2. 端點支援佇列機制,可在處理一個 URB 的同時繼續傳輸下一個,提高了 USB 頻寬的利用率。

USB裝置驅動程式章節13

USB URB 結構與相關操作

在USB裝置驅動程式的開發中,struct urb(USB Request Block)是核心的資料結構,用於描述一次USB傳輸請求。它包含了傳輸所需的各種資訊,如目標裝置、端點、傳輸方向、資料緩衝區等。

struct urb 重要欄位解析

struct urb {
    struct usb_device *dev; // 指向相關聯的USB裝置
    unsigned int pipe; // 端點資訊
    unsigned int transfer_flags; // 傳輸旗標,如URB_ISO_ASAP、URB_SHORT_NOT_OK等
    void *context; // 完成例程的上下文
    usb_complete_t complete; // 完成例程的函式指標
    int status; // 完成後的狀態碼
    void *transfer_buffer; // 資料傳輸緩衝區
    u32 transfer_buffer_length; // 資料緩衝區長度
    int number_of_packets; // 同步傳輸的封包數
    u32 actual_length; // 實際傳輸的資料長度
    unsigned char *setup_packet; // 控制傳輸的設定封包
    int start_frame; // 開始框架(適用於同步傳輸)
    int interval; // 輪詢間隔(適用於中斷傳輸)
    int error_count; // 錯誤計數(適用於同步傳輸)
    struct usb_iso_packet_descriptor iso_frame_desc[0]; // 同步傳輸的封包描述符陣列
};

URB 的分配與釋放

  • 使用 usb_alloc_urb() 函式分配 URB:struct urb *usb_alloc_urb(int isoframes, int mem_flags);
  • 使用 usb_free_urb() 函式釋放 URB:void usb_free_urb(struct urb *urb);

中斷傳輸與 URB 提交

中斷傳輸是一種週期性的傳輸模式,適合用於需要定期更新的裝置,如HID裝置。可以使用 usb_fill_int_urb() 巨集來填充中斷傳輸的 URB 欄位。

提交 URB 使用 usb_submit_urb() 函式:int usb_submit_urb(struct urb *urb, int mem_flags);

取消 URB

可以使用 usb_unlink_urb()usb_kill_urb() 函式來取消已提交但尚未完成的 URB。

  • usb_unlink_urb():非同步取消 URB。
  • usb_kill_urb():同步取消 URB,並等待完成例程執行完畢。

完成例程

完成例程的型別為 typedef void (*usb_complete_t)(struct urb *);,在 URB 完成後由USB子系統呼叫。完成例程中應檢查 urb->status 以檢測任何USB錯誤。

LAB 13.1:USB HID 裝置應用

在本實驗中,您將學習如何建立一個完整的USB HID裝置,並使用HID報告傳送和接收資料。實驗使用 Curiosity PIC32MX470 Development Board,該開發板具備PIC32MX系列微控制器(PIC32MX470512H),支援全速USB和多種擴充功能。

實驗步驟

  1. 建立新專案:在 MPLAB X IDE 中建立一個新的 Harmony 專案,命名為 USB_LED。
  2. 組態 Harmony:啟動 MPLAB Harmony Configurator 外掛程式,並選擇您的開發板,啟用 BSP(Board Support Package)。

#### 內容解密:

在實驗步驟中,首先需要在MPLAB X IDE中建立一個新的專案,這個專案將用於開發USB HID裝置。接著,透過MPLAB Harmony Configurator來組態專案,使其能夠支援Curiosity PIC32MX470 Development Board的硬體特性,包括USB功能。這樣做的目的是為了能夠正確地初始化和組態微控制器,以便進行後續的USB HID裝置開發。

使用 Plantuml 圖表呈現 USB 傳輸流程

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title USB裝置驅動程式深度解析

package "資料視覺化流程" {
    package "資料準備" {
        component [資料載入] as load
        component [資料清洗] as clean
        component [資料轉換] as transform
    }

    package "圖表類型" {
        component [折線圖 Line] as line
        component [長條圖 Bar] as bar
        component [散佈圖 Scatter] as scatter
        component [熱力圖 Heatmap] as heatmap
    }

    package "美化輸出" {
        component [樣式設定] as style
        component [標籤註解] as label
        component [匯出儲存] as export
    }
}

load --> clean --> transform
transform --> line
transform --> bar
transform --> scatter
transform --> heatmap
line --> style --> export
bar --> label --> export

note right of scatter
  探索變數關係
  發現異常值
end note

@enduml

此圖示說明瞭 USB 傳輸的基本流程,包括分配 URB、填充 URB 欄位、提交 URB 以及等待 URB 完成後呼叫完成例程檢查狀態。