本文章主要介紹一下如何在XP下做一個基於usb hid設備的上位機程序,實現簡單的上位機與硬件設備的通信. 由於本人自身的能力限制,有不足和出錯的地方,希望讀者見諒.我假設這篇文章的讀者已經對USB, HID,報告描述符等相關概念都至少有所了解,如果不是的話,自行學習.
開發環境, vs2005, DDK的支持.如果沒有安裝DDK,去網上找相關的庫文件和頭文件也行. 有以下幾個文件是所需的:
basetsd.h
hidclass.h
hidpddi.h
hidpi.h
hidsdi.h
hidusage.h
hid.lib
hidclass.lib
hidparse.lib
setupapi.lib
開發這種程序並不復雜,起碼跟用DDK自己寫驅動比起來,簡單很多, 主要是對DDK里的一些接口的功能要熟悉,這樣才能用起來得心應手
一 識別設備
要和自己的HID設備通信,第一步當然是找到設備.找到設備的原理很簡單,我們把讀到的設備信息與實際設備的信息相比較,就可以知道是否讀到了正確的設備. 在USB設備中,設備描述符里的信息可以唯一的標識不同的USB設備. 我們一般用
idVendor idProduct bcdDevice
這三個信息識別一個USB設備. 這三個信息都是設備描述符里的屬性. 所以我們的上位機程序可以把讀到的上述三個信息與實際設備的相比較,從而確定是否正確的連接到了設備. 實際設備的設備描述符在設備的固件程序中可以找得到,如果你沒有固件程序的源碼,也可以通過一些工具軟件讀出來設備的描述符信息,比如USB View就是一個很好用的工具.
知道了識別設備的原理,就可以通過DDK里相關的API接口去實現了.
HidD_GetAttributes函數可以獲取到上面的屬性信息, 它的定義如下:
BOOLEAN HidD_GetAttributes(
IN HANDLE HidDeviceObject,
OUT PHIDD_ATTRIBUTES Attributes
);
第二個參數是一個指向HIDD_ATTRIBUTES結構體的指針, 這個結構體的定義如下:
typedef struct _HIDD_ATTRIBUTES {
ULONG Size;
USHORT VendorID;
USHORT ProductID;
USHORT VersionNumber;
} HIDD_ATTRIBUTES
所以,這個函數可以從設備中讀到我們想要的信息. 但是,函數還有一個入口參數, HidDeviceObject,這是一個指向設備的句柄,所以在調用HidD_GetAttributes前,先要調用CreateFile函數返回一個有效的設備操作句柄. 有了這個句柄才能與設備進行正常的通信.
CreateFile的第一個參數要求提供一個設備名,這里我們要提供一個完整的設備路徑名,否則將返回無效的句柄. 這個路徑名是操作系統在識別到設備后分配給設備的, 可以通過DDK里的接口SetupDiGetDeviceInterfaceDetail來獲取到, 這個函數的定義如下:
SetupDiGetDeviceInterfaceDetailW(
__in HDEVINFO DeviceInfoSet,
__in PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
__out_bcount_opt(DeviceInterfaceDetailDataSize) PSP_DEVICE_INTERFACE_DETAIL_DATA_W DeviceInterfaceDetailData,
__in DWORD DeviceInterfaceDetailDataSize,
__out_opt PDWORD RequiredSize,
__out_opt PSP_DEVINFO_DATA DeviceInfoData
);
該函數可以獲取到一個設備接口的詳細信息, 注意第三個參數, 我們要的那個路徑名就由第三個參數返回. 它的結構體定義如下:
typedef struct _SP_DEVICE_INTERFACE_DETAIL_DATA_W {
DWORD cbSize;
WCHAR DevicePath[ANYSIZE_ARRAY];
} SP_DEVICE_INTERFACE_DETAIL_DATA_W
第二個數據就是我們要的路徑名.
這個函數的參數比較多,先來看一下第三個參數, 它用來接收設備的相關信息, 根據MSDN上的說明,我們可以這樣定義:
PSP_DEVICE_INTERFACE_DETAIL_DATADetailDataBuffer;
DetailDataBuffer = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(RequiredSize);
DetailDataBuffer -> cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
第四個參數指明第三個參數的大小, 第五個參數是一個出口參數, 它由系統返回,告訴我們實際需要的空間大小. 所以我們可以調用兩次SetupDiGetDeviceInterfaceDetail函數, 第一次獲取RequiredSize的值, 然后把它當作第四個參數來用, 如下:
SetupDiGetDeviceInterfaceDetail (DeviceInfoSet, &MyDeviceInterfaceData,
NULL, 0, &RequiredSize, NULL);
SetupDiGetDeviceInterfaceDetail(DeviceInfoSet,&MyDeviceInterfaceData,
DetailDataBuffer, RequiredSize,&RequiredSize, NULL);
前兩個參數都是入參,要獲取它們的值,還需要調用其它的一些DDK 接口. 第二個參數MyDeviceInterfaceData 需要用SetupDiEnumDeviceInterfaces來獲取到, 該函數的定義如下:
SetupDiEnumDeviceInterfaces(
__in HDEVINFO DeviceInfoSet,
__in_opt PSP_DEVINFO_DATA DeviceInfoData,
__in CONST GUID *InterfaceClassGuid,
__in DWORD MemberIndex,
__out PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
);
它的功能是可以枚舉到某一類設備中,某一個設備的接口信息, InterfaceClassGuid指明設備的類別, 用GUID標識, 我的設備就是標准的HID設備, MemberIndex具體指示某一個設備, 比如我電腦上連接了兩個HID設備,分別是鼠標和鍵盤,它們共用一個GUID,我用MemberIndex來區分它們,可能0對應鼠標,1對應鍵盤. 所以很明顯,這個函數可以循環調用,通過改變MemberIndex的值(從0到n), 一直到找到我們所需的設備為止.
最后就是如何獲取設備的GUID了, 要用一個函數,
HidD_GetHidGuid (
OUT LPGUID HidGuid
);
這個函數傳出一個GUID類型的數據, 得到的結果類似下面的形式:
GUID: {4D1E55B2-F16F-11CF-88CB-001111000030}
有了上面的函數已經可以正確的識別到一個USB HID的設備了, DDK里還提供了一些函數,可以在找到設備后, 獲取設備更詳細的信息,下面舉幾個比較常用的.
BOOLEAN __stdcall HidD_GetProductString(
__in HANDLE HidDeviceObject,
__out PVOID Buffer,
__in ULONG BufferLength
);
這個函數可以獲取到設備的產品字符串, 當然,前提是設備里要有產品字符串描述符, 因為字符串描述符在設備中是可選的. 該函數的第一個參數是CreateFile返回的句柄, 字符串描述符是用寬字符來表示的,所以讀取時要注意轉換. 同一類型的函數還有:
HidD_GetManufacturerString
HidD_GetSerialNumberString1
HidD_GetIndexedString
另外,還有一個比較重要的函數
NTSTATUS HidP_GetCaps(
PHIDP_PREPARSED_DATA PreparsedData,
PHIDP_CAPS Capabilities
);
這個函數可以獲取設備的通信能力, 它的第二個出口參數是這樣的一個結構體:
typedef struct _HIDP_CAPS
{
USAGE Usage;
USAGE UsagePage;
USHORT InputReportByteLength;
USHORT OutputReportByteLength;
USHORT FeatureReportByteLength;
USHORT Reserved[17];
USHORT NumberLinkCollectionNodes;
USHORT NumberInputButtonCaps;
USHORT NumberInputValueCaps;
USHORT NumberInputDataIndices;
USHORT NumberOutputButtonCaps;
USHORT NumberOutputValueCaps;
USHORT NumberOutputDataIndices;
USHORT NumberFeatureButtonCaps;
USHORT NumberFeatureValueCaps;
USHORT NumberFeatureDataIndices;
} HIDP_CAPS, *PHIDP_CAPS;
這個結構體里的屬性直接描述了一個設備的具體功能, 比如InputReportByteLength和OutputReportByteLength這兩個屬性分別表示設備在通信端點輸入和輸出的能力. 這些值是在設備的端點描述符里讀到的. 這些值直接決定了下面如何讀寫HID設備.
還可以再進一步獲取某個用途的功能(比如邏輯最大值,最小值等), 比如有個函數:
NTSTATUS HidP_GetValueCaps(
HIDP_REPORT_TYPE ReportType,
PHIDP_VALUE_CAPS ValueCaps,
PULONG ValueCapsLength,
PHIDP_PREPARSED_DATA PreparsedData
);
可以獲取值類型用途的功能, 在成功調用HidP_GetCaps后可以這樣調用HidP_GetValueCaps
///////////////////////////////////////////////////////////
Result = HidP_GetCaps(PreparsedData, &Capabilities);
WORD nValueCount = Capabilities.NumberInputValueCaps;
PHIDP_VALUE_CAPS valueCaps =(PHIDP_VALUE_CAPS)malloc(nValueCount*sizeof(PHIDP_VALUE_CAPS));
Result = HidP_GetValueCaps(HidP_Input, valueCaps, &nValueCount, PreparsedData);
//////////////////////////////////////////////////////////
二讀寫設備
正確到識別到設備后,下面就是對設備進行讀寫了.
對設備進行寫操作,有兩個方法可以用, 分別是HidD_SetOutputReport 和WriteFile. 前者到底層只能用control transfer, 而WriteFile可以用interrupt out transfer來傳輸數據. 同樣對於讀操作,也有兩個類似的操作. 下面的表格比較清楚的說明這幾個函數的關系:
先來說說寫,以WriteFile舉例. 可以用類似下面的形式發送一個報告
BOOL bRet = WriteFile(HidDevice,
WriteBuffer,
Capabilities.OutputReportByteLength,
&NumberOfBytesWriten,
&WriteOverlapped)
HidDevice是CreateFile返回的句柄, WriteBuffer是發送數據的緩沖區, Capabilities.OutputReportByteLength是要發送的數據長度,這個值就是報告的大小加1, NumberOfBytesWriten返回實際發送的大小. 最后一個參數比較復雜點, 它跟CreateFile有關,如果在前面的CreateFile中用的是重疊模式(異步),這里最后一個參數就不能為空. 在異步模式下,即使WriteFile沒有完成,函數也會返回, 這種情況下, GetLastError會返回ERROR_IO_PENDING, 我們的程序可以根據這個返回值繼續完成對設備的寫操作. 那么如何等待寫操作的完成呢, windows為我們提供了一個API,
BOOL GetOverlappedResult(
HANDLE hFile,
LPOVERLAPPED lpOverlapped,
LPDWORD lpNumberOfBytesTransferred,
BOOL bWait
);
注意它的第三個參數跟WriteFile是一樣的,事實上, 在異步模式操作時,WriteFile的第三個參數可以置空,因為它並沒有實際的意義,在寫操作完成時,getoverlappedresult函數會返回實際傳送的字節數.
讀操作與寫操作類似, 同樣也是用重疊異步模式, 可以設置個超時時間,在這個時間內把數據讀出來. 實際應用中,是用異步還是同步模式,沒有什么具體要求,我曾經試過做兩種模式下的主機程序,效果差別不大. 不過,從程序的高效性和健壯性上考慮,肯定用異步模式.
至於重疊(異步)模式的概念及應用其實遠不止於此,不過這不是我這篇文章的重點,下面這篇博文寫的不錯:
http://www.cppblog.com/Lee7/archive/2008/01/07/40630.html
作完讀寫操作,最后要釋放一些資源, 既然有CreateFile, CloseHandle是必不可少的. 另外,如果在識別設備階段調用了DDK的
HidD_GetPreparsedData函數, 那么最后要調用HidD_FreePreparsedData釋放掉.