5、HID主机程序设计(VB)
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的流程如下图:
图5-1 查找设备的流程
下面介绍VB实现的查找过程。
(1)获得GUID
应用程序要与HID设备通信之前,必须先获得HID类别的GUID(Globally Unique Indentifer)。GUID是一个128位的数值,每个对象都有惟一的GUID。HID类别的GUID包含在hidclass.h文档内,可以接引用,或是使用 HidD_GetHidGuid函数来取得HID类别的 GUID。
(2) 获得HID的结构数组
得到GUID后调用SetupDiGetClassDevs函数传回所有已经连接并且检测过的HID包含其信息的结构数组的地址。
该函数的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设备接口。
参数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成员是一个设备路径,应用程序可以用此路径来实现与该设备的通信。
DeviceInterfaceDetailDataSize包含DeviceInterfaceData结构的大小,以字节为单位。但是,在调用SetupDiGetDeviceInterfaceDetail函数之前无法知道此数值的大小,如果没有DeviceInterfaceDetailDataSize函数,SetupDiGetDeviceInterfaceDetail函数不会传回所需的结构。
这个问题的解决方式是两次调用SetupDiGetDeviceInterfaceDetail函数。第一次调用时GetLastError函数会传回错误信息,即:The data erea passed to a system call is too small,但是RequireSize参数会包含正确的DeviceInterfaceDetailDataSize数值。再次调用时传递此传回值,函数就会执行成功。
(5) 获得设备句柄
取得设备的路径以后,就可以准备开始与设备通信。使用CreateFile来打开一个HID设备,并且取得此设备的句柄,使用设备的句柄来与设备交换数据。当不再需要访问此设备时,应该调用CIoseHandle函数来关闭设备并释放系统资源。
(6) 获得设备的厂商ID、产品ID和产品序列号
要识别一个设备是否是所要的设备,只要比较此设备的厂商与产品ID 即可。HidD_GetAttributes函数用来取得一个包含厂商与产品ID以及产品的版本号码的结构。
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能力的数据结构
PreparsedData是一个包含数据的缓冲区的指针。程序并不需要访问缓冲区内的数据,只需要将缓冲区的开始地址传递给其他的API函数。当应用程序不再需要PreparsedData时,它应该调用HidD_FreePreparsedData函数来释放系统资源。
接下来调用HidP_GetCaps,该函数传回一个结构,里面包含设备能力的信息,包括设备的Usage Page、Usage、报表长度以及按钮能力和数值能力等的数目。如果不使用厂商与产品ID来识另设备,HidP_GetCaps函数传回的设备能力信息可以帮助决定是否继续与此设备通信。
HidP_GetCaps函数填写Capabilities结构中的数据项,Capabilities结构成员说明了HID的基本信息。这些信息包括:
● 用法页和用法:UsagePage、Usage;
● 输入、输出和特征报表长度:InputReportByteLength、 OutputReportByteLength和 FeatureReportByteLength;
● 由函数HidP_GetLinkCollectionNodes返回的顶层集合连接数目NumberLinkCollectionNodes
● 在输入、输出和特征报表的按钮、数值和数据指示器的的数目。
(2) 获得描述HID数值能力的数据结构
通过HidP_GetCaps获得的HID的基本能力信息不是能从设备得到全部信息,还可以调用另一个函数HidP_GetValueCap,该函数可以获得报表描述符中的数值和按钮的能力。HidP_GetValueCap返回的是包含了报表描述符中全部数值信息的结构的指针。
(3) 输出报表到设备
当应用程序取得HID设备的句柄,并且知道输出报表的字节数目后,它就可以传送输出报表给此设备。应用程序先将要传送的数据复制到一个缓冲区内,然后调用WriteFile函数。缓冲区的大小等于HidP_GetCaps函数返回的HIDP_CAPS结构的OutputReportByte Length属性值。这个大小值等于报表的字节大小,再加上一个字节的Report ID。Report ID是缓冲区的第一个字节。
HlD驱动程序用来确定输出报表的传输类型,根据Windows的版本以及HID接口有无中断输出端点而定。应用程序不需要干预,低阶的驱动程序会自动处理。
如果函数返回的Result数值不等于零,表示函数成功执行。
如果接口只支持数值为0的Report ID,这个Report ID并不传送,但需要出现在应用程序传给WriteFile函数的缓冲区内。
WriteFile函数在HID通信中最常发生的错误是CRC error。此错误表示主机控制器试图要传送报表,但是没有从设备收到预期的响应。通常该错误不是发生在CRC计算时所检测到的错误,而是因为主机没有收到固件预期的响应。
(4) 从设备输入出报表
当应用程序取得HID设备的句柄,并且知道输入报表的字节数目后,就可以从此设备读取输入报表。应用程序先声明一个缓冲区来储存数据,然后调用ReadFile函数。用来储存数据的缓冲区大小等于HidP_GetCaps函数所返回的HIDP_CAPS结构的InputReportByteLength属性值。
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为单位的指定时间间隔。在读取到数据或到达该时间间隔时,此函数才返回。
(5) 特征报表的传送
应用程序调用HidD_SetFeature函数传送一个特征报表到设备。
API函数HidD_GetFeature用于从设备读取特征报表,通过对该API函数的调用,主机控制器以控制传输送出Get_Feature请求,并在数据阶段,设备回传特征报表。
(6) 关闭设备
当结束与HID的通信,需要调用CloseHandle函数关闭通信。
USB开源项目
百合电子工作室曾在2009年推出了一个USB开源项目:Easy USB 51 Programer,此项目以开源的形式展示了USB通信的基础性内容、USB HID设备类固件程序开发及PC端应用程序开发、自定义USB设备类固件程序开发及PC端驱动程...
|
USB产品
EASY USB D12 是原EASY USB 51 PROGRAMER的升级版,是百合电子工作室历时一年精心设计的一款USB学习板/开发板,与老款相比,其实例更丰富,技术文档更详尽,更重要的是此款USB学习板还提供技术支持。......
|