HID设备是Windows系统提供了完善支持的一类,应用程序可以通过标准的API函数调用,实现与HID设备的通信。Windows系统提供了几千个API函数,作为应用程序与操作系统的接口,与HID相关的API函数被封装在hid.dll、setupapi.dll和kernal32.dll文件中。
1.1 HID访问使用的API函数
文件hid.dll中提供了很多个API,因为与HID设备通信有一定的复杂性。首先,在应用程序与HID传输数据之前,应用程序必须先识别该设备并且读取它的报表信息,这些动作需要调用多个API函数才可以实现。应用程序需要寻找连接到系统上的是哪些HID设备,然后读取每个设备的信息直到查找到所需的属性。如果是客户化的设备,应用程序可以寻找特定的厂商与产品ID,或者应用程序可以寻找特定类型的设备,例如键盘或鼠标。
表5-1 用于HID设备的API函数
用于了解HID设备情况的API函数(hid.dll)
|
HidD_GetAttributes
|
请求获得HID设备的厂商ID、产品ID和版本号
|
HidD_FreePreparsedData
|
释放函数HidD_GetPreparsedData所使用的资源
|
HidD_GetHidGuid
|
请求获得HID设备的GUID
|
HidD_GetIndexString
|
请求获得由索引识别的字符串
|
HidD_GetManufactureString
|
请求获得设备制造商字符串
|
HidD_GetPhysicalDescriptor
|
请求获得设备实体字符串
|
HidD_GetPreparsedData
|
请求获得与设备能力信息相关的缓冲区的代号
|
HidD_GetProductString
|
请求获得产品字符串
|
HidD_GetSerialNumberString
|
请求获得产品序列号字符串
|
HidP_GetButtonCaps
|
请求获得HID报表中所有按钮的能力
|
HidP_GetCaps
|
请求获得用于描述设备能力的结构的指针
|
HidP_GetLinkCollectionNotes
|
请求获得描述在顶层集合中的连接集合(Link Collection)关系的结构的数组
|
HidP_GetSpecificButtonCaps
|
请求获得报表中按钮的能力,该请求可以设定一个Usage Page、Usage或是Link Collection
|
HidP_GetSpecificValueCaps
|
请求获得报表中数值的能力,该请求可以设定一个Usage Page、Usage或是Link Collection
|
HidP_GetValueCaps
|
请求获得HID报表中所有数值的能力
|
HidP_MaxUsageListLength
|
请求获得HID报表中可以回传的按钮的最大数目,该请求可以设定一个Usage Page
|
HidP_UsageListDifference
|
比较两个按钮列表,并且求出在一个列表中设定而在另一个列表中没有设定的按钮
|
用于从设备读取、向设备传送报表的API函数(hid.dll)
|
HidD_GetFeature
|
从设备读取一个特征报表
|
HidD_SetFeature
|
向设备传送一个特征报表
|
HidP_GetButtons
|
从设备读取包含每个按下的按钮的用法(Usage)的缓冲区的指针,该请求可以设定一个Usage Page
|
HidP_GetButtonEx
|
从设备读取包含每个按下的按钮的Usage和Usage Page的缓冲区的指针
|
HidP_GetScaledUsageValue
|
从设备读取一个已经经过比例因子调整的有符号数值
|
HidP_GetUsageValue
|
从设备读取一个指向数值的指针
|
HidP_GetUsageValueArray
|
从设备读取包含多个数据项的Usage的数据
|
HidP_SetButtons
|
向设备传送设置按钮的数据
|
HidP_SetScaledUsageValue
|
将一个实际数值转换成设备使用的逻辑数值,并将其插入到报表中
|
HidP_SetUsageValue
|
向设备传送数据
|
HidP_SetUsageValueArray
|
向设备传送包含多个数据项的Usage的数据
|
HidD_FlushQueue
|
清空输入缓冲区
|
HidD_GetNumInputBuffer
|
获得驱动程序用于存储输入报表的环形缓冲区的大小,默认值是8
|
HidD_SetNumInputBuffer
|
设置驱动程序用于存储输入报表的环形缓冲区的大小
|
用于查找和识别设备的API函数(setupapi.dll)
|
SetupDiGetClassDevs
|
获得HID的信息,针对已安装的设备,回传一个指向其信息集的代码
|
SetupDiEnumDeviceInterfaces
|
请求获得设备信息群内的一个设备的信息
|
SetupDiGetDeviceInterfaceDetail
|
请求获得设备的路径
|
SetupDiDestroyDeviceInfoList
|
释放SetupDiGetClassDevs使用的资源
|
用于打开、关闭设备和实现数据传送的API函数(kernal32.dll)
|
CreatFile
|
取得设备的路径后,调用该函数获得设备代号
|
WriteFile
|
向设备传送输出报表
|
ReadFile
|
从设备读取输入报表
|
CloseHandle
|
关闭设备,释放CreateFile所使用的资源
|
5.2 查找HID的过程
在实现HID的访问之前,首先要查找指定(根据设备的厂商ID、产品ID和产品序列号)的HID。查找指定设备的过程如下:
• 调用函数HidD_GetHidGuid获得USB设备的GUID;
• 调用函数SetupDiGetClassDevs,获得一个包含全部HID信息的结构数组的指针,下面根据此数组逐项查找指定的HID;
• 调用函数SetupDiEnumDeviceInterfaces,填写SP_DEVICE_INTERFACE_DATA结构的数据项,该结构用于识别一个HID设备接口;
• 调用函数SetupDiGetDeviceInterfaceDetail,获得一个指向该设备的路径名;
• 调用函数CreateFile,获得设备句柄;
• 调用函数HidD_GetAttributes,填写HIDD_ATTRIBUTES结构的数据项,该结构包含设备的厂商ID、产品ID和产品序列号,比照这些数值确定该设备是否是查找的设备。
查找HID的流程如下图:
下面介绍VB实现的查找过程。
(1)获得GUID
应用程序要与HID设备通信之前,必须先获得HID类别的GUID(Globally Unique Indentifer)。GUID是一个128位的数值,每个对象都有惟一的GUID。HID类别的GUID包含在hidclass.h文档内,可以接引用,或是使用 HidD_GetHidGuid函数来取得HID类别的 GUID。
- ‘ 函数声明
- Public Declare Function HidD_GetHidGuid Lib "hid.dll" (ByRef HidGuid As GUID
- ) As Long
-
- ‘ GUID结构说明
- Public Type GUID
- Data1 As Long
- Data2 As Integer
- Data3 As Integer
- Data4(7) As Byte
- End Type
-
- ‘ 变量说明
- Dim HidGuid as GUID
-
- ‘ 调用
- Call HidD_GetHidGuid(HidGuid)
(2) 获得HID的结构数组
得到GUID后调用SetupDiGetClassDevs函数传回所有已经连接并且检测过的HID包含其信息的结构数组的地址。
- ‘ 函数声明
- Public Declare Function SetupDiGetClassDevs Lib "setupapi.dll" Alias "SetupDiGetClassDevsA"( _
- ByRef ClassGuid As GUID, _
- ByVal Enumerator As String, _
- ByVal hwndParent As Long, _
- ByVal Flags As Long) _
- As Long
- ‘ 常量说明
- Public Const DIGCF_PRESENT = &H2
- Public Const DIGCF_DEVICEINTERFACE = &H10
-
- ‘ 调用
- hDevInfoSet = SetupDiGetClassDevs(
- HidGuid, _ ‘通过HidD_GetHidGuid函数获得GUID
- vbNullString, _
- 0, _
- (DIFCG_PRESENT or DIFCG_DEVICEINTERFACE) _
- )
该函数的ClassGuid参数值为通过HidD_GetHidGuid函数获得GUID。Enumerator与hwndParent参数没有用到,Flags参数是两个定义在setupapi.h文档内的系统常数。代码中的Flags常数告诉SetupDiGetClassDevs函数只寻找目前存在(连接并且检测过)的设备接口,并且是HID类别的成员。
返回值hDevlnfoSet是包含所有连接并且检测到的全部HID的信息的结构数组的地址。在程序中并不需要访问hDevInfoSet的元素,只需要将hDevlnfSet值传给SetupDiEnumDeviceInterfaces函数即可。
当程序不再需要hDevInfoSet指向的数组时,应该调用 SetupDiDestroyDeviceInfo List函数来释放资源。
下面在hDevInfoSet指向的结构数组中查找。
(3) 识别HID接口
接下来调用SetupDiEnumDeviceInterfaces,填写SP_DEVICE_INTERFACE_DATA结构的数据项,该结构用于识别一个HID设备接口。
- ‘ 函数声明
- Public Declare Function SetupDiEnumDeviceInterfaces Lib "setupapi.dll" (
- ByVal DeviceInfoSet As Long, _
- ByVal DeviceInfoData As Long, _
- ByRef InterfaceClassGuid As GUID, _
- ByVal MemberIndex As Long, _
- ByRef DeviceInterfaceData As SP_DEVICE_INTERFACE_DATA _
- )As Long
-
- ‘ 结构定义
- Public Type SP_DEVICE_INTERFACE_DATA
- cbSize As Long
- InterfaceClassGuid As GUID
- Flags As Long
- Reserved As Long
- End Type
-
- ‘ 变量说明
- Dim Result as Long
- Dim MemberIndex as Long
- Dim MyDeviceInterfaceData as SP_DEVICE_INTERFACE_DATA
-
- ‘ 调用
- MyDeviceInterfaceData.cbSize = LenB(MyDeviceInterfaceData)
- MemberIndex = 0
- Result = SetupDiEnumDeviceInterfaces(
- DeviceInfoSet, _ ‘SetupDiGetClassDevs的返回值
- 0, _
- HidGuid, _ ‘通过HidD_GetHidGuid函数获得的GUID
- MemberIndex, _ ‘第一次调用设置为0
- MyDeviceInterfaceData _
- )
参数cbSize是SP_DEVICE_INTERFACE_DATA结构的大小,以字节为单位。在调用SetupDiEnumDeviceInterfaces函数之前,cbSize必须储存在结构内来当做传递的参数。
参数HidGuid和DeviceInfoSet是函数之前的传回值。
DeviceInfoData是SP_DEVICE_INTERFACE_DATA结构的指针,用来限制检测特定设备的接口。MemberIndex是DeviceInfoSet数组的索引值,在遍历整个数组的循环中MemberIndex递增。MyDeviceInterfaceData是回传的结构,用来识别HID的一个接口。
(4) 获得设备路径
下面通过调用SetupDiGetDeviceInterfaceDetail函数用来获得另外一个结构:SP_DEVICE_INTERFACE_DETAIL_DATA。此结构与前一个函数SetupDiEnumDeviceInterfaces所识别的设备接口有关。结构的DevicePath成员是一个设备路径,应用程序可以用此路径来实现与该设备的通信。
- ‘ 函数声明:
- Public Declare Function SetupDiGetDeviceInterfaceDetail Lib "setupapi.dll" _
- Alias "SetupDiGetDeviceInterfaceDetailA" (_
- ByVal DeviceInfoSet As Long, _
- ByRef DeviceInterfaceData As SP_DEVICE_INTERFACE_DATA, _
- ByVal DeviceInterfaceDetailData As Long, _
- ByVal DeviceInterfaceDetailDataSize As Long, _
- ByRef RequiredSize As Long, _
- ByVal DeviceInfoData As Long _
- ) As Long
-
- ‘ 结构声明
- Public Type SP_DEVICE_INTERFACE_DETAIL_DATA
- cbSize As Long
- DevicePath As Byte
- End Type
-
- ‘ 变量定义
- Dim Needed as Long, DetailData as long
- Dim MyDeviceInterfaceDetailData As SP_DEVICE_INTERFACE_DETAIL_DATA
- Dim DetailDataBuffer() as Byte
- Dim DevicePathName As String
-
- ‘ 调用
- MyDeviceInterfaceData.cbSize = LenB(MyDeviceInterfaceData)
-
- Result = SetupDiGetDeviceInterfaceDetailA(
- DeviceInfoSet, _
- DeviceInterfaceData, _
- 0, _
- 0, _
- Needed, _
- 0 _
- )
-
- DetailData = needed
- MyDeviceInterfaceDetailData.cbSize = Len(MyDeviceInterfaceDetailData)
- ReDim DetailDataBuffer(Needed)
-
- Call RtlMoveMemory(DetailDataBuffer(0), MyDeviceInterfaceDetailData, 4)
- Result = SetupDiGetDeviceInterfaceDetailA(
- DeviceInfoSet, _
- DeviceInterfaceData, _
- VarPtr(DetailDataBuffer(0)), _
- DetailData, _
- Needed, _
- 0 _
- )
-
- ‘ 转换成字符串,再转换成Unicode并从中删除4个字符得到设备路径
- DevicePathName = Cstr(DetailDataBuffer())
- DevicePathName = StrCovn(DevicePathName, vbUnicode)
- DevicePathName = Right$(DevicePathName, Len(DevicePathName)-4)
DeviceInterfaceDetailDataSize包含DeviceInterfaceData结构的大小,以字节为单位。但是,在调用SetupDiGetDeviceInterfaceDetail函数之前无法知道此数值的大小,如果没有DeviceInterfaceDetailDataSize函数,SetupDiGetDeviceInterfaceDetail函数不会传回所需的结构。
这个问题的解决方式是两次调用SetupDiGetDeviceInterfaceDetail函数。第一次调用时GetLastError函数会传回错误信息,即:The data erea passed to a system call is too small,但是RequireSize参数会包含正确的DeviceInterfaceDetailDataSize数值。再次调用时传递此传回值,函数就会执行成功。
(5) 获得设备句柄
取得设备的路径以后,就可以准备开始与设备通信。使用CreateFile来打开一个HID设备,并且取得此设备的句柄,使用设备的句柄来与设备交换数据。当不再需要访问此设备时,应该调用CIoseHandle函数来关闭设备并释放系统资源。
- ‘ 函数声明:
- Public Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" ( _
- ByVal lpFileName As String, _
- ByVal dwDesiredAccess As Long, _
- ByVal dwShareMode As Long, _
- ByRef lpSecurityAttributes As Long, _
- ByVal dwCreationDisposition As Long, _
- ByVal dwFlagsAndAttributes As Long, _
- ByVal hTemplateFile As Long _
- ) As Long
-
- ‘ 常量定义
- Public Const GENERIC_READ = &H80000000
- Public Const GENERIC_WRITE = &H40000000
- Public Const FILE_SHARE_READ = &H1
- Public Const FILE_SHARE_WRITE = &H2
- Public Const OPEN_EXISTING = 3
-
- ‘ 调用
- HidDevice = CreateFile( _
- DevicePathName, _
- GENERIC_READ Or GENERIC_WRITE, _
- (FILE_SHARE_READ Or FILE_SHARE_WRITE), _
- 0, _
- OPEN_EXISTING, _
- 0, _
- 0
- )
(6) 获得设备的厂商ID、产品ID和产品序列号
要识别一个设备是否是所要的设备,只要比较此设备的厂商与产品ID 即可。HidD_GetAttributes函数用来取得一个包含厂商与产品ID以及产品的版本号码的结构。
- ‘ 函数声明
- Public Declare Function HidD_GetAttributes Lib "hid.dll" ( _
- ByVal HidDeviceObject As Long, _
- ByRef Attributes As HIDD_ATTRIBUTES _
- ) As Long
-
- ‘ 结构说明
- Public Type HIDD_ATTRIBUTES
- Size As Long
- VendorID As Integer ‘厂商ID
- ProductID As Integer ‘产品ID
- VersionNumber As Integer ‘产品版本号
- End Type
-
- ‘ 变量定义
- Dim DeviceAttributes As HIDD_ATTRIBUTES
-
- ‘ 调用
- DeviceAttributes.Size = LenB(DeviceAttributes)
- Result = HidD_GetAttributes( _
- HidDevice, _ ‘由CreateFile函数返回
- DeviceAttributes
- )
HidDevice是由 CreateFile函数所传回的设备句柄。如果CreateFile函数传回的是非零值,DeviceAttributes结构就会填写正确值。应用程序可以由DeviceAttributes结构取得厂商ID、产品ID以及产品的版本号码。
如果厂商与产品ID不是想查找的,应用程序应该使用CloseHandle函数来关闭该设备,然后移到下一个SetupDiEnumDeviceInterfaces所检测到的下一个HlD继续查找。当应用程序检测到指定的设备或是检测完全部HID,它应该调用SetupDiDestroyDeviceInfoList函数来释放SetupDiGetClassDevs函数所保留的资源。
5.3 获得HID的能力
获得设备的能力是可以进一步了解HID的手段,但不是必须的。如果在查找设备时,如果通过厂商ID、产品ID和产品序列号可以确定特定的设备,一般是已知设备的细节信息了。如果不通过厂商ID、产品ID和产品序列号确定设备,另一个方法是通过获得设备能力来确定设备。
获得HID的能力的过程主要经过以下几个步骤:
● 先使用HidD_GetPreparsedData函数获得一个包含设备能力信息的缓冲区的指针,调用HidP_GetCaps获得描述HID的能力的数据结构;
● 调用HidP_GetValueCaps取得描述能力的数值。
(1) 获得描述HID能力的数据结构
- ‘ 函数声明
- Public Declare Function HidD_GetPreparsedData Lib "hid.dll" (_
- ByVal HidDeviceObject As Long, _
- ByRef PreparsedData As Long
- ) As Long
-
- ‘ 变量定义
- Dim PreparsedData As Long
-
- ‘ 调用
- Result = HidD_GetPreparsedData (HidDevice, _ ‘ 由CreateFile获得的设备句柄
- PreparsedData _
- )
PreparsedData是一个包含数据的缓冲区的指针。程序并不需要访问缓冲区内的数据,只需要将缓冲区的开始地址传递给其他的API函数。当应用程序不再需要PreparsedData时,它应该调用HidD_FreePreparsedData函数来释放系统资源。
接下来调用HidP_GetCaps,该函数传回一个结构,里面包含设备能力的信息,包括设备的Usage Page、Usage、报表长度以及按钮能力和数值能力等的数目。如果不使用厂商与产品ID来识另设备,HidP_GetCaps函数传回的设备能力信息可以帮助决定是否继续与此设备通信。
- ‘ 函数声明
- Public Declare Function HidP_GetCaps Lib "hid.dll" ( _
- ByVal PreparsedData As Long, _
- ByRef Capabilities As HIDP_CAPS
- ) As Long
-
- ‘ 结构定义
- Public Type HIDP_CAPS
- Usage As Integer ‘ 用法
- UsagePage As Integer ‘ 用法页
- InputReportByteLength As Integer ‘ 输入报表长度
- OutputReportByteLength As Integer ‘ 输出报表长度
- FeatureReportByteLength As Integer ‘ 特征报表长度
- Reserved(16) As Integer
- NumberLinkCollectionNodes As Integer ‘ 由函数HidP_GetLinkCollectionNodes
- ‘ 返回的顶层集合连接数目
- NumberInputButtonCaps As Integer ‘ 由函数HidP_GetButtonCaps返回的
- ‘ 包含输入按钮能力的结构HIDP_BUTTON_
- ‘ CAPS的数目
- NumberInputValueCaps As Integer ‘ 由函数HidP_GetValueCaps返回的
- ‘ 包含输入数值能力的结构HIDP_VALUE_
- ‘ CAPS的数目
- NumberInputDataIndices As Integer ‘ 在输入报表中有关按键和数值的数据
- ‘ 指示器(Data Indices)的数目
- NumberOutputButtonCaps As Integer ‘ 由函数HidP_GetButtonCaps返回的
- ‘ 包含输出按钮能力的结构HIDP_BUTTON_
- ‘ CAPS的数目
- NumberOutputValueCaps As Integer ‘ 由函数HidP_GetValueCaps返回的
- ‘ 包含输出数值能力的结构HIDP_VALUE_
- ‘ CAPS的数目
- NumberOutputDataIndices As Integer ‘ 在输出报表中有关按键和数值的数据
- ‘ 指示器(Data Indices)的数目
- NumberFeatureButtonCaps As Integer ‘ 由函数HidP_GetButtonCaps返回的
- ‘ 包含特征按钮能力的结构HIDP_BUTTON_
- ‘ CAPS的数目
- NumberFeatureValueCaps As Integer ‘ 由函数HidP_GetValueCaps返回的
- ‘ 包含特征数值能力的结构HIDP_VALUE_
- ‘ CAPS的数目
- NumberFeatureDataIndices As Integer ‘ 在特征报表中有关按键和数值的数据
- ‘ 指示器(Data Indices)的数目
- End Type
-
- ‘ 变量定义
- Dim Capabilities As HIDP_CAPS
-
- ‘ 调用
- Result = HidP_GetCaps (_
- PreparsedData, _ ‘ 由函数HidD_GetPreparsedData定义
- Capabilities
- )
HidP_GetCaps函数填写Capabilities结构中的数据项,Capabilities结构成员说明了HID的基本信息。这些信息包括:
● 用法页和用法:UsagePage、Usage;
● 输入、输出和特征报表长度:InputReportByteLength、 OutputReportByteLength和 FeatureReportByteLength;
● 由函数HidP_GetLinkCollectionNodes返回的顶层集合连接数目NumberLinkCollectionNodes
● 在输入、输出和特征报表的按钮、数值和数据指示器的的数目。
(2) 获得描述HID数值能力的数据结构
通过HidP_GetCaps获得的HID的基本能力信息不是能从设备得到全部信息,还可以调用另一个函数HidP_GetValueCap,该函数可以获得报表描述符中的数值和按钮的能力。HidP_GetValueCap返回的是包含了报表描述符中全部数值信息的结构的指针。
- ‘ 函数声明
- Public Declare Function HidP_GetValueCaps Lib "hid.dll" (_
- ByVal ReportType As Integer, _
- ByRef ValueCaps As Byte, _
- ByRef ValueCapsLength As Integer, _
- ByVal PreparsedData As Long _
- ) As Long
-
- ‘ ReportType常量定义
- Public Const HidP_Input = 0
- Public Const HidP_Output = 1
- Public Const HidP_Feature = 2
-
- Public Type HidP_Value_Caps
- UsagePage As Integer
- ReportID As Byte
- IsAlias As Long
- BitField As Integer
- LinkCollection As Integer
- LinkUsage As Integer
- LinkUsagePage As Integer
- IsRange As Long
- IsStringRange As Long
- IsDesignatorRange As Long
- IsAbsolute As Long
- HasNull As Long
- Reserved As Byte
- BitSize As Integer
- ReportCount As Integer
- Reserved2 As Integer
- Reserved3 As Integer
- Reserved4 As Integer
- Reserved5 As Integer
- Reserved6 As Integer
- LogicalMin As Long
- LogicalMax As Long
- PhysicalMin As Long
- PhysicalMax As Long
- UsageMin As Integer
- UsageMax As Integer
- StringMin As Integer
- StringMax As Integer
- DesignatorMin As Integer
- DesignatorMax As Integer
- DataIndexMin As Integer
- DataIndexMax As Integer
- End Type
-
-
- ‘ 变量定义
- Dim ValueCaps(1023) As Byte
- ‘ 调用
- Result = HidP_GetValueCaps (_
- HidP_Input, _
- ValueCaps(0), _
- Capabilities.NumberInputValueCaps, _
- PreparsedData
- )
(3) 输出报表到设备
当应用程序取得HID设备的句柄,并且知道输出报表的字节数目后,它就可以传送输出报表给此设备。应用程序先将要传送的数据复制到一个缓冲区内,然后调用WriteFile函数。缓冲区的大小等于HidP_GetCaps函数返回的HIDP_CAPS结构的OutputReportByte Length属性值。这个大小值等于报表的字节大小,再加上一个字节的Report ID。Report ID是缓冲区的第一个字节。
HlD驱动程序用来确定输出报表的传输类型,根据Windows的版本以及HID接口有无中断输出端点而定。应用程序不需要干预,低阶的驱动程序会自动处理。
- ‘ 函数声明
- Public Declare Function WriteFile Lib "kernel32" (_
- ByVal hFile As Long, _
- ByRef lpBuffer As Byte, _
- ByVal nNumberOfBytesToWrite As Long, _
- ByRef lpNumberOfBytesWritten As Long, _
- ByVal lpOverlapped As Long _
- ) As Long
-
- ‘ 变量定义
- Dim SendBuffer() As Byte
- Dim OutputReportData(7) As Byte
- Dim Count as Long
-
- ‘ 调用
- ReDim SendBuffer(Capabilities.OutputReportByteLength - 1)
- SendBuffer(0) = 0 ‘ Report ID
- ‘ 将准备的数据从OutputReportData复制到SendBuffer
- For Count = 1 To Capabilities.OutputReportByteLength - 1
- SendBuffer(Count) = OutputReportData(Count - 1)
- Next Count
- NumberOfBytesWritten = 0
- Result = WriteFile( _
- HidDevice, _ ‘ 由CreateFile函数返回的设备句柄
- SendBuffer(0), _ ‘ 输出报表缓存
- CLng(Capabilities.OutputReportByteLength), _ ‘ 要输出字节数
- NumberOfBytesWritten, _ ‘ 实际输出的字节数
- 0
- )
如果函数返回的Result数值不等于零,表示函数成功执行。
如果接口只支持数值为0的Report ID,这个Report ID并不传送,但需要出现在应用程序传给WriteFile函数的缓冲区内。
WriteFile函数在HID通信中最常发生的错误是CRC error。此错误表示主机控制器试图要传送报表,但是没有从设备收到预期的响应。通常该错误不是发生在CRC计算时所检测到的错误,而是因为主机没有收到固件预期的响应。
(4) 从设备输入出报表
当应用程序取得HID设备的句柄,并且知道输入报表的字节数目后,就可以从此设备读取输入报表。应用程序先声明一个缓冲区来储存数据,然后调用ReadFile函数。用来储存数据的缓冲区大小等于HidP_GetCaps函数所返回的HIDP_CAPS结构的InputReportByteLength属性值。
- ‘ 函数声明
- Public Declare Function ReadFile Lib "kernel32" ( _
- ByVal hFile As Long, _
- ByRef lpBuffer As Byte, _
- ByVal nNumberOfBytesToRead As Long, _
- ByRef lpNumberOfBytesRead As Long, _
- ByVal lpOverlapped As Long _
- ) As Long
-
- ‘ 变量定义
- Dim Count
- Dim NumberOfBytesRead As Long
- Dim ReadBuffer() As Byte ‘ 输入缓冲区,字节0为Report ID
- ReDim ReadBuffer(Capabilities.InputReportByteLength - 1)
-
- ‘ 调用
- Result = ReadFile( _
- HidDevice, _ ‘ 由CreateFile函数返回的设备句柄
- ReadBuffer(0), _ ‘ 输入缓冲区首地址
- CLng(Capabilities.InputReportByteLength), _ ‘ 要读取的字节数
- NumberOfBytesRead, _ ‘ 读到的字节数
- 0
- )
ReadBuffer字节数组包含报表的数据。如果函数返回的Result数值不等于零,表示函数成功执行。
通过ReadFile读取的缓冲区的第一个字节是Report ID,后续是从设备读取的报表数据。如果接口只支持一个Report ID,此Report ID不在总线上传输,但会出现在ReadBuffer缓冲区内。
调用ReadFile函数不会立刻开始总线上的传输,只是主机在定时的中断输入传输中读取一个报表。如果没有未读取的报表,就等待下一个传输完成。主机在检测设备后开始请求报表,当HlD驱动程序加载后,驱动程序将报表储存在环状缓冲区内。当缓冲区已经填满并有新的报表到达时,旧的报表会被覆盖。调用ReadFile函数会读取缓冲区内最旧的报表。
在Windows 98 SE以及后来的版本中,默认的环状缓冲区尺寸是8个报表。应用程序可以使用HidD_SetNumInputBuffers函数来设置缓冲区的大小。如果应用程序没有频繁的请求报表,有些报表就会丢失。如果不想要丢失报表,就应该改用特征报表。
如果报表的数据从上一次传输后就没有改变,闲置速率决定设备是否要传送报表。在检测设备时HlD驱动程序会试图将设备的闲置速率设置为0,这表示除非报表的数据有改变否则HID不会传送报表。没有可以改变闲置速率的API函数。
如果设备拒绝将闲置速率设置为0,可以传回Stall来响应Set_Idle请求来通知主机设备不支持该请求。
如果设备不支持Set_Idle请求,而且应用程序只要读取一次报表,固件可以设置成只传送一次报表。在送报表后,固件可以设置端点传回NAK来响应输入令牌信息包。如果设备有新的数据要传送,固件可以设置端点来传回该数据。否则设备会在主机轮询端点时继续传送相同的报表,应用程序也会重复地读取相同的报表。
上面程序中ReadFile的最后一个参数为0,表示ReadFile调用是阻塞的。当应用程序在环形缓冲区为空时调用ReadFile,应用程序将会被挂起,直到有输入报表为止,否则只能按下Ctrl+Alt+Del来关闭应用程序,或是从总线上移除设备。
采用多线程方式编程可以较好的解决这个问题,在另一个线程中调用ReadFile可以避免主线程被挂起,在使用Visual Basic编写多线程应用程序会遇到困难,这是因为Visual Basic本身不支持多线程。而在Visual C++编写API方式通信程序时可以采用多线程方式,ReadFile函数调用发生在一个独立的线程,这样可以实现重叠I/O操作。
解决的办法之一是保证设备永远有数据传送,可以将固件设计为输入端点永远启用并且准备响应要求。如果没有新的数据传送,设备可以传送上一次的数据,或是传回一个特定代码来指示没有新的数据。
也可以采用这样的方法,在调用ReadFile之前,应用程序先调用WriteFile来传送一个报表,报表内可以包含一个特定代码来告诉固件准备传送数据,这样当应用程序调用ReadFile时,设备的端点就会启用并且有数据准备传送。
比较好的方法是使用ReadFile的重叠选项。在重叠的读取时ReadFile函数会立即返回(即使没有可读数据),然后应用程序调用 WaitForSingleObject函数来读取数据。WaitForSingleObject函数可以设置暂停,如果数据在指定时间内尚未抵达,此函数会传回一个码来指示此情况,然后应用程序可以使用CancelIo函数来取消读取动作。
要使用重叠I/O,CreateFile函数必须在dwFlagsAndAttributes参数中传递一个重叠的结构。应用程序调用CreateEvent函数建立一个事件对象,在ReadFile完成后此事件对象会被设置成信号状态。当应用程序调用ReadFile时,它传递一个重叠结构的指针,重叠结构的hEvent参数是一个事件对象的代号。应用程序调用 WaitForSingleObject函数来传递此事件代号,以及一个以ms为单位的指定时间间隔。在读取到数据或到达该时间间隔时,此函数才返回。
- ‘ 函数声明
- Public Declare Function ReadFile Lib "kernel32" ( _
- ByVal hFile As Long, _
- ByRef lpBuffer As Byte, _
- ByVal nNumberOfBytesToRead As Long, _
- ByRef lpNumberOfBytesRead As Long, _
- ByVal lpOverlapped As Long _
- ) As Long
-
- Public Declare Function CreateEvent Lib "kernal32.dll" Alias CreatEventA( _
- ByVal SecurityAttributes As Long, _
- ByVal bManualReset As Long, _
- ByVal bInitialState As Long, _
- ByVal lpName As String
- ) As Long
-
- Public Declare Function WaitForSingleObject Lib “kernal32.dll” (_
- ByVal hHandle as Long, _
- ByVal dwMilliseconds as Long
- ) as Long
-
- ‘ 重叠结构声明
- Public Type OVERLAPPED
- Internal as Long
- InternalHigh as Long
- Offset as Long
- OffsetHigh as Long
- hEvent as Long
- End Type
-
- ‘ 常量定义
- Public Const FILE_FLAG_OVERLAPPED = &H40000000
-
- ‘ 变量定义
- Dim EventObject as Long
- Dim HIDOverlapped as OVERLAPPED
-
- ‘ 部分代码
- EventObject = CreatEvent (0&, True, True, “”)
- HIDOverlapped.hEvent = EventObject
- HIDOverlapped.Offset = 0
- HIDOverlapped.OffsetHigh = 0
-
- HidDevice = CreateFile( _
- DevicePathName, _
- GENERIC_READ Or GENERIC_WRITE, _
- (FILE_SHARE_READ Or FILE_SHARE_WRITE), _
- 0, _
- OPEN_EXISTING, _
- FILE_FLAG_OVERLAPPED, _
- 0
- ) ‘ 获得HID设备句柄
-
- Result = ReadFile( _
- HidDevice, _ ‘ 由CreateFile函数返回的设备句柄
- ReadBuffer(0), _ ‘ 输入缓冲区首地址
- CLng(Capabilities.InputReportByteLength), _ ‘ 要读取的字节数
- NumberOfBytesRead, _ ‘ 读到的字节数
- HIDOverlapped
- ) ‘ 读取报表,同时传送一个指针到重叠结构
-
- Result = WaitForSingleObject (EventObject, 5000)
- ‘ 等待ReadFile完成,超时间隔设为5秒
(5) 特征报表的传送
应用程序调用HidD_SetFeature函数传送一个特征报表到设备。
- ‘ 函数声明
- Public Declare Function HidD_SetFeature Lib "hid.dll" ( _
- ByVal HidDeviceObject As Long, _
- ByRef ReportBuffer As Byte, _
- ByVal ReportBufferLength As Long
- ) As Long
-
- ‘ 调用
- Result = HidD_SetFeature( _
- HidDevice, _ ‘ 由CreateFile函数返回的设备句柄
- SendBuffer(0), _ ‘ 输出缓冲区首地址
- CLng(Capabilities.FeatureReportByteLength) ‘ 特征报表长度字节数
- )
API函数HidD_GetFeature用于从设备读取特征报表,通过对该API函数的调用,主机控制器以控制传输送出Get_Feature请求,并在数据阶段,设备回传特征报表。
- ‘ 函数声明
- Public Declare Function HidD_GetFeature Lib "hid.dll" ( _
- ByVal HidDeviceObject As Long, _
- ByRef ReportBuffer As Byte, _
- ByVal ReportBufferLength As Long
- ) As Long
-
- ‘ 调用
- Result = HidD_GetFeature( _
- HidDevice, _ ‘ 由CreateFile函数返回的设备句柄
- ReadBuffer(0), _ ‘ 输出缓冲区首地址
- CLng(Capabilities.FeatureReportByteLength) ‘ 特征报表长度字节数
- )
(6) 关闭设备
当结束与HID的通信,需要调用CloseHandle函数关闭通信。
- ‘ 函数声明
- Public Declare Function CloseHandle Lib "kernel32" ( _
- ByVal hObject As Long _
- ) As Long
-
- ‘ 调用
- Result = CloseHandle(HidDevice)
|