Windows主機端與自定義USB HID設備通信詳解 收藏
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. 主機端設備枚舉程序流程
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 設備進行通信的一般方法。
view plaincopy to clipboardprint?
#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;
}
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/kevinyujm/archive/2009/06/12/4264506.aspx