天天看點

Windows主機端與自定義USB HID裝置通信詳解

說明:

-          以下結論都是基于Windows XP系統所得出的,不保證在其他系統的适用性。

-          在此讨論的是HID自定義裝置,對于标準裝置,譬如USB滑鼠和鍵盤,由于作業系統對其獨占,許多操作未必能正确執行。

1.  所使用的典型Windows API

CreateFile

ReadFile

WriteFile

以下函數是DDK的内容:

HidD_SetFeature

HidD_GetFeature

HidD_SetOutputReport

HidD_GetInputReport

其中,CreateFile用于打開裝置;ReadFile、HidD_GetFeature、HidD_GetInputReport用于裝置到主機方向的資料通信;WriteFile、HidD_SetFeature、HidD_SetOutputReport用于主機到裝置方向的資料通信。鑒于實際應用,後文主要讨論CreateFile,WriteFile,ReadFile,HidD_SetFeature四個函數,明白了這四個函數,其它的可以類推之。

2.  幾個常見錯誤

       當使用以上API時,如果操作失敗,調用GetLastError()會得到以下常見錯誤:

       6:         句柄無效

       23:       資料錯誤(循環備援碼檢查)

       87:       參數錯誤

       1784:    使用者提供的buffer無效

       後文将會詳細說明這些錯誤情況。

3.         主機端裝置枚舉程式流程

Windows主機端與自定義USB HID裝置通信詳解

4.         函數使用說明

CreateFile(devDetail->DevicePath,                                         //裝置路徑

               GENERIC_READ | GENERIC_WRITE,                    //通路方式

               FILE_SHARE_READ | FILE_SHARE_WRITE,         //共享模式

               NULL,

               OPEN_EXISTING,                                           //檔案不存在時,傳回失敗

               FILE_FLAG_OVERLAPPED,                                 //以重疊(異步)模式打開

               NULL);

在這裡,CreateFile用于打開HID裝置,其中裝置路徑通過函數SetupDiGetInterfaceDeviceDetail取得。CreateFile有以下幾點需要注意:

-     通路方式: 如果是系統獨占裝置,例如滑鼠、鍵盤等等,應将此參數設定為0,否則後續函數操作将失敗(譬如HidD_GetAttributes);也就是說,不能對獨占裝置進行除了查詢以外的任何操作,是以能夠使用的函數也是很有限的,下文的一些函數并不一定适合這些裝置。在此順便列出MSDN上關于此參數的說明:

If this parameter is zero, the application can query file and device attributes without accessing the device. This is useful if an application wants to determine the size of a floppy disk drive and the formats it supports without requiring a floppy in the drive. It can also be used to test for the file's or directory's existence without opening it for read or write access。

-          重疊(異步)模式:此參數并不會在此處表現出明顯的意義,它主要是對後續的WriteFile,ReadFile有影響。如果這裡設定為重疊(異步)模式,那麼在使用WriteFile,ReadFile時也應該使用重疊(異步)模式,反之亦然。這首先要求WriteFile,ReadFile的最後一個參數不能為空(NULL)。否則,便會傳回87(參數錯誤)錯誤号。當然,87号錯誤并不代表就是此參數不正确,更多的資訊将在具體講述這兩個函數時指出。此參數為0時,代表同步模式,即WriteFile,ReadFile操作會在資料處理完成之後才傳回,否則阻塞在函數内部。

ReadFile(hDev,                                 //裝置句柄,即CreateFile的傳回值

              recvBuffer,                          //用于接收資料的buffer

              IN_REPORT_LEN,              //要讀取資料的長度

              &recvBytes,                         //實際收到的資料的位元組數

              &ol);                                  //異步模式

在這裡,ReadFile用于讀取HID裝置通過中斷IN傳輸發來的輸入報告。有以下幾點要注意:

1、ReadFile的調用不會引起裝置的任何反應,即HID裝置與主機之間的中斷IN傳輸不與ReadFile打交道。實際上主機會在最大間隔時間(由裝置的端點描述符來指定)内輪詢裝置,發出中斷IN傳輸的請求。“讀取”即意味着從某個buffer裡面取回資料,實際上這個buffer就是HID裝置驅動中的buffer。這個buffer的大小可以通過HidD_SetNumInputBuffers來改變。在XP上預設值是32(個報告)。

2、讀取的資料對象是輸入報告,也即通過中斷輸入管道傳入的資料。是以,如果裝置不支援中斷IN傳輸,那麼是無法使用此函數來得到預期結果的。實際上這種情況不可能在HID中出現,因為協定指明了至少要有一個中斷IN端點。

3、IN_REPORT_LEN代表要讀取的資料的長度(實際的資料正文+一個byte的報告ID),這裡是一個常數,主要是因為裝置固件的資訊我是完全知道的,當然知道要讀取多少資料(也就是報告的長度);不過也可以通過另外的函數(HidD_GetPreparsedData)來事先取得報告的長度,這裡不做詳細讨論。因為很難想象在不了解固件資訊的情況下來做自定義裝置的HID通信,在實際應用中一般來說就是固件與PC程式比對着來開發。此參數如果設定過大,不會有實質性的錯誤,在recvBytes參數中會輸出實際讀到的長度;如果設定過小,即小于報告的長度,會傳回1784号錯誤(使用者提供的buffer無效)。

4、關于異步模式。前面已經提過,此參數的設定必須與CreateFile時的設定相對應,否則會傳回87号錯誤(參數錯誤)。如果不需要異步模式,此參數需置為NULL。在這種情況下,ReadFile會一直等待直到資料讀取成功,是以會阻塞住程式的目前過程。

       WriteFile(hDev,                                 //裝置句柄,即CreateFile的傳回值

                     reportBuf,                           //存有待發送資料的buffer

                     OUT_REPORT_LEN,           //待發送資料的長度

                     &sendBytes,                        //實際收到的資料的位元組數

                     &ol);                                  //異步模式

       在這裡,WriteFile用于傳輸一個輸出報告給HID裝置。有以下幾點要注意:

1、  與ReadFile不同,WriteFile函數被調用後,雖然也是經過驅動程式,但是最終會反映到裝置中。也就是說,調用WriteFile後,裝置會接收到輸出報告的請求。如果裝置使用了中斷OUT傳輸,則WriteFile會通過中斷OUT管道來進行傳輸;否則會使用SetReport請求通過控制管道來傳輸。

2、  OUT_REPORT_LEN代表要寫入的資料長度(實際的資料正文+一個byte的報告ID)。如果大于實際報告的長度,則使用實際報告長度;如果小于實際報告長度,會傳回1784号錯誤(使用者提供的buffer無效)。

3、  reportBuf[0]必須存有待發送報告的ID,并且此報告ID訓示的必須是輸出報告,否則會傳回87号錯誤(參數錯誤)。這種情況可能容易被程式員忽略,結果不知錯誤号所反映的是什麼,網上也經常有類似疑問的文章。順便指出,輸入報告、輸入報告、特征報告這些報告類型,是反映在HID裝置的報告描述符中。後文将做舉例讨論。

4、  關于異步模式。前面已經提過,此參數的設定必須與CreateFile時的設定相對應,否則會傳回87号錯誤(參數錯誤)。如果不需要異步模式,此參數需置為NULL。在這種情況下,WriteFile會一直等待直到資料讀取成功,是以會阻塞住程式的目前過程。

HidD_SetFeature(hDev,                                    //裝置句柄,即CreateFile的傳回值

                     reportBuf,                                   //存有待發送資料的buffer

                     FEATURE_REPORT_LEN);        //buffer的長度

HidD_SetOutputReport(hDev,                            //裝置句柄,即CreateFile的傳回值

                     reportBuf,                                   //存有待發送資料的buffer

                     OUT_REPORT_LEN);                //buffer的長度

HidD_SetFeature發送一個特征報告給裝置,HidD_ SetOutputReport發送一個輸出報告給裝置。注意以下幾點:

1、  跟WriteFile類似,必須在reportBuf[0]中指明要發送的報告的ID,并且和各自适合的類型相對應。也就是說,HidD_SetFeature隻能發送特征報告,是以報告ID必須是特征報告的ID;HidD_SetOutputReport隻能發送輸出報告,是以報告ID隻能是輸出報告的ID。

2、  這兩個函數最常傳回的錯誤代碼是23(資料錯誤)。包括但不僅限于以下情況:

- 報告ID與固件描述的不符。

- 傳入的buffer長度少于固件描述的報告的長度。

據有關資料反映(非官方文檔),隻要是驅動程式對請求無反應,都會産生此錯誤。

5.         常見錯誤彙總

- HID ReadFile

  - Error Code 6 (handle is invalid)

    傳入的句柄無效

  - Error Code 87 (參數錯誤)

    很可能是createfile時聲明了異步方式,但是讀取時按同步讀取。

  - Error Code 1784 (使用者提供的buffer無效):

    傳參時傳入的“讀取buffer長度”與實際的報告長度不符。

- HID WriteFile

  - Error Code 6 (handle is invalid)

    傳入的句柄無效

  - Error Code 87(參數錯誤)

    - CreateFile時聲明的同步/異步方式與實際調用WriteFile時傳入的不同。

    - 報告ID與固件中定義的不一緻(buffer的首位元組是報告ID)

  - Error Code 1784 (使用者提供的buffer無效)

    傳參時傳入的“寫入buffer長度”與實際的報告長度不符。

- HidD_SetFeature

- HidD_SetOutputReport

  - Error Code 1 (incorrect function)

    不支援此函數,很可能是裝置的報告描述符中未定義這樣的報告類型(輸入、輸出、特征)

  - Error Code 6 (handle is invalid)

    傳入的句柄無效

  - Error Code 23(資料錯誤(循環備援碼檢查))

    - 報告ID與固件中定義的不相符(buffer的首位元組是報告ID)

    - 傳入的buffer長度少于固件定義的報告長度(報告正文+1byte, 1byte為報告ID)

    - 據相關資料反映(非官方文檔),隻要是驅動程式不接受此請求(對請求無反應),都會産生此錯誤

6.         報告描述符及資料通信程式示例

報告描述符(由于是彙編代碼,是以不必留意其文法,僅需注意表中的每個資料都占1個位元組):

_ReportDescriptor:                            //報告描述符

       .dw 0x06,  0x00, 0xff                 //用法頁

    .dw 0x09,  0x01                            //用法(供應商用法1)

    .dw 0xa1,  0x01                            //集合開始

    .dw 0x85,  0x01                            //報告ID(1)

    .dw 0x09,  0x01                            //用法(供應商用法1)  

    .dw 0x15,  0x00                            //邏輯最小值(0)

    .dw 0x26,  0xff, 0x0                      //邏輯最大值(255)

    .dw 0x75,  0x08                            //報告大小(8)

    .dw 0x95,  0x07                            //報告計數(7)

    .dw 0x81,  0x06                            //輸入(資料,變量,相對值)

    .dw 0x09,  0x01                          //用法(供應商用法1)  

    .dw 0x85,  0x03                          //報告ID(3)

    .dw 0xb1,  0x06                          //特征(資料,變量,相對值)

       .dw 0x09,  0x01                      //用法(供應商用法1)

    .dw 0x85,  0x02                         //報告ID(2)

    .dw 0xb1,  0x06                         //特征(資料,變量,相對值)

    .dw 0x09,  0x01                         //用法(供應商用法1)  

    .dw 0x85,  0x04                         //報告ID(4)

    .dw 0x91,  0x06                         //輸出(資料,變量,相對值)

    .dw   0xc0                                  //結合結束

_ReportDescriptor_End:

這個報告描述符,定義了 4 個不同的報告:輸入報告 1 ,特征報告 2 ,特征報告 3 ,輸出報告 4 (數字代表其報告 ID )。為了簡化,每個報告都是 7 個位元組(加上報告 ID 就是 8 個位元組)。下面用一個簡單的示例來描述 PC 端與 USB HID 裝置進行通信的一般方法。  

#define USB_VID 0xFC0 #define USB_PID 0x420 HANDLE OpenMyHIDDevice(int overlapped); void HIDSampleFunc() {     HANDLE        hDev;     BYTE        recvDataBuf[8];     BYTE        reportBuf[8];     DWORD        bytes;          hDev = OpenMyHIDDevice(0); //打開裝置,不使用重疊(異步)方式;          if (hDev == INVALID_HANDLE_VALUE)         return;          reportBuf[0] = 4; //輸出報告的報告ID是4     memset(reportBuf, 0, 8);     reportBuf[1] = 1;     if (!WriteFile(hDev, reportBuf, 8, &bytes, NULL)) //寫入資料到裝置         return;          ReadFile(hDev, recvDatatBuf, 8, &bytes, NULL); //讀取裝置發給主機的資料 } HANDLE OpenMyHIDDevice(int overlapped) {     HANDLE     hidHandle;     GUID     hidGuid;          HidD_GetHidGuid(&hidGuid);          HDEVINFO hDevInfo = SetupDiGetClassDevs(                     &hidGuid,                     NULL,                     NULL,                     (DIGCF_PRESENT | DIGCF_DEVICEINTERFACE));     if (hDevInfo == INVALID_HANDLE_VALUE)     {      return INVALID_HANDLE_VALUE;     }          SP_DEVICE_INTERFACE_DATA devInfoData;     devInfoData.cbSize = sizeof (SP_DEVICE_INTERFACE_DATA);     int deviceNo = 0;          SetLastError(NO_ERROR);          while (GetLastError() != ERROR_NO_MORE_ITEMS)     {         if (SetupDiEnumInterfaceDevice (hDevInfo,                     0,                     &hidGuid,                     deviceNo,                     &devInfoData))         {             ULONG requiredLength = 0;             SetupDiGetInterfaceDeviceDetail(hDevInfo,                                             &devInfoData,                                             NULL,                                             0,                                             &requiredLength,                                             NULL);             PSP_INTERFACE_DEVICE_DETAIL_DATA devDetail =                 (SP_INTERFACE_DEVICE_DETAIL_DATA*) malloc (requiredLength);             devDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);             if(!SetupDiGetInterfaceDeviceDetail(hDevInfo,                         &devInfoData,                         devDetail,                         requiredLength,                         NULL,                         NULL))             {                 free(devDetail);                            SetupDiDestroyDeviceInfoList(hDevInfo);                 return INVALID_HANDLE_VALUE;             }                   if (overlapped)                   {                          hidHandle = CreateFile(devDetail->DevicePath,                         GENERIC_READ | GENERIC_WRITE,                         FILE_SHARE_READ | FILE_SHARE_WRITE,                         NULL,                         OPEN_EXISTING,                         FILE_FLAG_OVERLAPPED,                         NULL);                   }                   else                   {                          hidHandle = CreateFile(devDetail->DevicePath,                         GENERIC_READ | GENERIC_WRITE,                         FILE_SHARE_READ | FILE_SHARE_WRITE,                         NULL,                         OPEN_EXISTING,                         0,                         NULL);                   }             free(devDetail);             if (hidHandle==INVALID_HANDLE_VALUE)             {                 SetupDiDestroyDeviceInfoList(hDevInfo);                 free(devDetail);                 return INVALID_HANDLE_VALUE;             }             _HIDD_ATTRIBUTES hidAttributes;             if(!HidD_GetAttributes(hidHandle, &hidAttributes))             {                 CloseHandle(hidHandle);                            SetupDiDestroyDeviceInfoList(hDevInfo);                 return INVALID_HANDLE_VALUE;             }             if (USB_VID == hidAttributes.VendorID                 && USB_PID == hidAttributes.ProductID)             {                 break;             }             else             {                 CloseHandle(hidHandle);                 ++deviceNo;             }         }     }     SetupDiDestroyDeviceInfoList(hDevInfo);     return hidHandle; }

繼續閱讀