返回文章列表

USB裝置驅動程式開發

本文提供一個開發 Linux USB 驅動程式的,以控制 PIC32MX USB HID 裝置。文章涵蓋了 USB 狀態機的設計與實作、資料傳輸、事件處理以及程式碼範例。透過 USB HID 協定,驅動程式可以傳送命令控制 LED,並接收來自使用者按鈕的狀態。文章詳細說明瞭如何在 C 語言中實作 USB

嵌入式系統 驅動程式開發

在嵌入式系統開發中,撰寫 USB 驅動程式是常見的需求。本文將引導讀者開發一個 Linux USB 驅動程式,用於與 PIC32MX USB HID 裝置通訊,實作控制 LED 和讀取按鈕狀態的功能。文章將著重於 USB 狀態機的設計與實作,並提供程式碼範例說明如何處理 USB 事件和資料傳輸。透過理解 USB 狀態機的運作原理,讀者可以更有效地開發和維護 USB 驅動程式,確保裝置與主機之間的穩定通訊。

USB裝置驅動程式開發

步驟3:修改生成的程式碼

HID(Human Interface Device)類別通常用於實作人機介面產品,如滑鼠和鍵盤。然而,HID協定相當靈活,可以用於向USB裝置傳送/接收一般用途的資料。在以下兩個實驗中,您將學習如何使用USB協定進行基本的一般用途USB資料傳輸。您將開發Linux USB驅動程式,向PIC32MX USB HID裝置傳送USB命令,以切換包含在PIC32MX Curiosity板上的三個LED(LED1、LED2、LED3)。PIC32MX USB HID裝置還將檢查使用者按鈕(S1)的值,並回復包含其值的封包給Raspberry Pi。

USB裝置端實作

  • 切換LED:Linux USB驅動程式傳送報告給HID裝置。報告的第一個位元組可以是0x01、0x02或0x03。當接收到0x01時,HID裝置必須切換LED1;接收到0x02時,切換LED2;接收到0x03時,切換LED3。
  • 取得按鈕狀態:Linux USB驅動程式傳送報告給HID裝置。報告的第一個位元組是0x00。HID裝置必須回復另一個報告,其中第一個位元組是S1按鈕的狀態(“0x00"表示按下,“0x01"表示未按下)。

程式碼實作

// app.c中的USB_Task()函式內部實作USB狀態機
void USB_Task(void) {
    switch (appData.stateUSB) {
        case USB_STATE_INIT:
            // 初始化狀態機
            appData.stateUSB = USB_STATE_WAITING_FOR_DATA;
            // 排程讀取請求以接收報告
            USB_DEVICE_HID_ReportReceive(appData.handleUsbDevice, &appData.rxTransferHandle, appData.receiveDataBuffer, 64);
            break;
        case USB_STATE_WAITING_FOR_DATA:
            // 等待主機傳送報告
            if (appData.hidDataReceived) {
                // 處理接收到的報告
                uint8_t receivedCommand = appData.receiveDataBuffer[0];
                if (receivedCommand == 0x00) {
                    // 取得按鈕狀態並回復
                    appData.transmitDataBuffer[0] = (PORTB & (1 << 12)) ? 0x01 : 0x00; // 假設S1連線到RB12
                    USB_DEVICE_HID_ReportSend(appData.handleUsbDevice, &appData.txTransferHandle, appData.transmitDataBuffer, 64);
                    appData.stateUSB = USB_STATE_SEND_REPORT;
                } else if (receivedCommand >= 0x01 && receivedCommand <= 0x03) {
                    // 切換對應的LED
                    switch (receivedCommand) {
                        case 0x01:
                            LATB ^= (1 << LED1_PIN); // 假設LED1連線到RBx
                            break;
                        case 0x02:
                            LATB ^= (1 << LED2_PIN); // 假設LED2連線到RBy
                            break;
                        case 0x03:
                            LATB ^= (1 << LED3_PIN); // 假設LED3連線到RBz
                            break;
                    }
                    // 排程下一次讀取
                    USB_DEVICE_HID_ReportReceive(appData.handleUsbDevice, &appData.rxTransferHandle, appData.receiveDataBuffer, 64);
                    appData.stateUSB = USB_STATE_WAITING_FOR_DATA;
                }
                appData.hidDataReceived = false;
            }
            break;
        case USB_STATE_SEND_REPORT:
            if (appData.hidDataTransmitted) {
                // 傳送完成,排程下一次讀取
                USB_DEVICE_HID_ReportReceive(appData.handleUsbDevice, &appData.rxTransferHandle, appData.receiveDataBuffer, 64);
                appData.stateUSB = USB_STATE_WAITING_FOR_DATA;
                appData.hidDataTransmitted = false;
            }
            break;
        default:
            break;
    }
}

內容解密:

  1. USB狀態機初始化:在USB_STATE_INIT狀態下,狀態機轉移到USB_STATE_WAITING_FOR_DATA,並排程一個讀取請求以接收來自主機的報告。
  2. 等待資料:在USB_STATE_WAITING_FOR_DATA狀態下,裝置等待主機傳送報告。一旦接收到報告,就根據報告的第一個位元組執行相應的操作(切換LED或回復按鈕狀態)。
  3. 傳送報告:在USB_STATE_SEND_REPORT狀態下,裝置傳送包含按鈕狀態的報告給主機,並在傳送完成後排程下一次讀取請求。

步驟4:宣告USB狀態機狀態

app.h檔案中宣告一個列舉型別(例如USB_STATES),它包含實作狀態機所需的四個狀態的標籤。

typedef enum {
    USB_STATE_INIT = 0,
    USB_STATE_WAITING_FOR_DATA,
    USB_STATE_SCHEDULE_READ,
    USB_STATE_SEND_REPORT
} USB_STATES;

步驟5:新增成員到APP_DATA型別

app.h檔案中的APP_DATA結構型別中新增必要的成員,用於儲存USB狀態機的狀態、接收和傳送資料的緩衝區指標、HID傳輸控制程式碼以及傳輸狀態旗標。

typedef struct {
    // ...
    USB_STATES stateUSB; // USB任務的當前狀態
    uint8_t *receiveDataBuffer; // 接收資料緩衝區
    uint8_t *transmitDataBuffer; // 傳送資料緩衝區
    USB_DEVICE_HID_TRANSFER_HANDLE txTransferHandle; // 傳送報告傳輸控制程式碼
    USB_DEVICE_HID_TRANSFER_HANDLE rxTransferHandle; // 接收報告傳輸控制程式碼
    bool hidDataReceived; // HID資料接收旗標
    bool hidDataTransmitted; // HID資料傳送旗標
} APP_DATA;

內容解密:

  • stateUSB:儲存USB狀態機的當前狀態。
  • receiveDataBuffer和transmitDataBuffer:分別用於儲存接收和傳送的資料。
  • txTransferHandle和rxTransferHandle:用於控制HID報告的傳輸。
  • hidDataReceived和hidDataTransmitted:分別指示HID資料是否已接收或傳送。

USB裝置驅動程式開發

初始化USB狀態機

在開發USB裝置驅動程式時,第一步是初始化USB狀態機。這個過程涉及設定狀態機的初始狀態以及相關的緩衝區指標。

程式碼實作

void APP_Initialize(void) {
    /* 將應用程式狀態機設定為初始狀態 */
    appData.state = APP_STATE_INIT;

    /* 初始化USB HID裝置應用程式資料 */
    appData.handleUsbDevice = USB_DEVICE_HANDLE_INVALID;
    appData.usbDeviceIsConfigured = false;
    appData.idleRate = 0;

    /* 初始化USB任務狀態機的appData成員 */
    appData.receiveDataBuffer = &receiveDataBuffer[0];
    appData.transmitDataBuffer = &transmitDataBuffer[0];
    appData.stateUSB = USB_STATE_INIT;
}

內容解密:

  1. appData.state = APP_STATE_INIT; 將應用程式狀態機重置為初始狀態,確保每次啟動時都從同一個起點開始。
  2. appData.handleUsbDevice = USB_DEVICE_HANDLE_INVALID; 初始化USB裝置控制程式碼為無效狀態,表示尚未組態。
  3. appData.usbDeviceIsConfigured = false; 設定USB裝置組態狀態為未組態,這個標誌用於判斷裝置是否已經被主機組態。
  4. appData.receiveDataBufferappData.transmitDataBuffer 被設定為指向相應的緩衝區,用於接收和傳送資料。
  5. appData.stateUSB = USB_STATE_INIT; 初始化USB狀態機的狀態為初始狀態。

處理USB裝置斷開事件

當USB裝置被斷開或電源被移除時,需要處理相關事件以重置裝置狀態。

程式碼實作

case USB_DEVICE_EVENT_POWER_REMOVED:
    /* VBUS不再可用,斷開裝置 */
    USB_DEVICE_Detach(appData.handleUsbDevice);
    appData.usbDeviceIsConfigured = false;
    break;

內容解密:

  1. 當檢測到USB_DEVICE_EVENT_POWER_REMOVED事件時,表示VBUS電源被移除,裝置需要斷開連線。
  2. USB_DEVICE_Detach(appData.handleUsbDevice); 執行裝置斷開操作,通知主機該裝置不再可用。
  3. appData.usbDeviceIsConfigured = false; 重置裝置組態狀態為未組態,準備下一次連線。

處理HID事件

HID(Human Interface Device)事件包括報告傳送和接收事件,需要更新相關標誌以反映傳輸狀態。

程式碼實作

static void APP_USBDeviceHIDEventHandler(USB_DEVICE_HID_INDEX hidInstance, USB_DEVICE_HID_EVENT event, void* eventData, uintptr_t userData) {
    APP_DATA* appData = (APP_DATA*)userData;
    switch (event) {
        case USB_DEVICE_HID_EVENT_REPORT_SENT:
            {
                USB_DEVICE_HID_EVENT_DATA_REPORT_SENT* report = (USB_DEVICE_HID_EVENT_DATA_REPORT_SENT*)eventData;
                if (report->handle == appData->txTransferHandle) {
                    appData->hidDataTransmitted = true;
                }
                break;
            }
        case USB_DEVICE_HID_EVENT_REPORT_RECEIVED:
            {
                USB_DEVICE_HID_EVENT_DATA_REPORT_RECEIVED* report = (USB_DEVICE_HID_EVENT_DATA_REPORT_RECEIVED*)eventData;
                if (report->handle == appData->rxTransferHandle) {
                    appData->hidDataReceived = true;
                }
                break;
            }
        // ...
    }
}

內容解密:

  1. 當收到USB_DEVICE_HID_EVENT_REPORT_SENT事件時,表示報告已傳送給主機。透過比較傳輸控制程式碼確認事件與當前傳輸請求的關聯性。
  2. 如果控制程式碼匹配,設定appData->hidDataTransmitted = true;表示資料已成功傳輸。
  3. 對於USB_DEVICE_HID_EVENT_REPORT_RECEIVED事件,類別似地檢查控制程式碼並更新appData->hidDataReceived標誌。

建立USB狀態機

USB狀態機負責管理USB裝置的不同狀態,包括初始化、讀寫操作等。

程式碼實作

static void USB_Task(void) {
    if (appData.usbDeviceIsConfigured) {
        switch (appData.stateUSB) {
            case USB_STATE_INIT:
                {
                    appData.hidDataTransmitted = true;
                    appData.txTransferHandle = USB_DEVICE_HID_TRANSFER_HANDLE_INVALID;
                    appData.rxTransferHandle = USB_DEVICE_HID_TRANSFER_HANDLE_INVALID;
                    appData.stateUSB = USB_STATE_SCHEDULE_READ;
                    break;
                }
            // 其他case處理
        }
    } else {
        appData.stateUSB = USB_STATE_INIT;
    }
}

內容解密:

  1. 狀態機根據appData.usbDeviceIsConfigured的狀態決定是否執行。
  2. USB_STATE_INIT狀態下,初始化傳輸標誌和控制程式碼,並轉移到USB_STATE_SCHEDULE_READ狀態以安排讀取操作。
  3. 如果裝置未組態,則重置狀態機到初始狀態。