Windows下开发HID设备主机程序
本文章主要介绍一下如何在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释放掉.