一、前言
在无操作系统的裸机中,或者类似如DOS这样操作系统中,应用程序和硬件的交互是非常简单的。如果没有操作系统,我们访问硬件空间可能只需一条指令就行,甚至可以访问任意的内存空间或者IO空间。但是在WINDOWS操作系统中,应
用程序与硬件间被完全隔离开来,应用程序与软件的通信就必须依赖于依赖驱动程序。有点类似于现在的网上购物,卖家相当于硬件,而卖家相当于应用程序,淘宝等类似的购物网站相当于驱动程序,而卖家与买家之间的买卖就得依赖
于淘宝等类似的购物网站。至于为什么要把硬件层和应用程序隔开,答案是——安全。允许应用程序随意访问硬件是一件很危险的事情,除了可能会造成蓝屏死机之类的问题,还会发生密码的安全性问题。那在我们日常开发过程当中哪
些场景需要涉及到硬件与软件的交互呢?
二、应用程序与硬件交互的场景
1.读取硬件信息
当应用程序需要与硬件进行捆绑时,就需要获取对应的硬件信息,例如触摸框绑定、MCU绑定等等,需要读取出对应硬件的信息,并与预期的信息进行比较来判断是否进行了绑定。获取MCU版本号等等。
2.与硬件进行通信
应用程序与硬件进行通信,比单纯的读取硬件信息稍微复杂一点。例如通过MCU来进行定时开关机,首先需要根据对应的信息查找指定的MCU设备,然后将对应的定时开关机指定发送给MCU,MCU收到信息之后,发送信息给应用程序,告之
定时开关机的指定是否设置成功。这其中包含了应用程序与硬件之间的通信。
3.硬件的管理
例如磁盘的管理,我们可以自己编写应用程序对磁盘进行一系列的管理,磁盘分区、磁盘大小等等。移动设备管理工具等等都可以实现。
4.其他
当然还有很多很多啦....
那么,既然我们提到了这么多得应用场景,那我们如何来实现它们呢?那接下来就不得不提SetupDi这一系列API了。
三、SetupDi API
1.HidD_GetHidGuid
函数定义
BOOL
Hidd_GetHidGuid(
&guidHID 指向GUID类型的指针
);
HID类设备是通过GUID类型值作标识的,调用函数HidD_GetHidGuid颗获得HID设备的标识
2.SetupDiGetClassDevs
函数定义
HDEVINFO
SetupDiGetClassDevs(
const GUID *ClassGuid,//HID类设备是通过GUID类型值作标识的,如图1.图Guid示例,通过指向Guid的指针,获取对应的设备列表。
PCTSTR Enumerator,
HWND hwndParent,
DWORD Flags//Flags,当值为DIGCF_ALLCLASSES,该函数会将*ClassGuid忽略掉。
);
获取一个指定类别或全部类别的所有已安装设备的信息,其中两个参数需要注意一下。返回值,如果函数运行成功,返回设备信息结构的句柄,该结构包含与指定参数匹配的所有已安装设备。如果失败,则返回INVALID_HANDLE_VALUE。
3.SetupDiEnumDeviceInterfaces
函数定义
BOOL
SetupDiEnumDeviceInterfaces(
HDEVINFO DeviceInfoSet, //一个指向设备信息集合的句柄,包含设备接口返回信息,通常是SetupDiGetClassDevs的返回
PSP_DEVINFO_DATA DeviceInfoData, //指向特定设备的SP_DEVINFO_DATA 类型的指针,
const GUID *InterfaceClassGuid, //指向制定设备接口类的GUID指针
DWORD MemberIndex, //设备信息中接口列表的索引值(初始值为0)
PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData //指向调用者分配的SP_DEVICE_INTERFACE_DATA类型的内存区域的指针,调用前必须先配置DeviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA)
);
获取设备列表中指定接口的信息,通过次接口只能获取接口信息,需要取得接口的详细信息需要调用SetupDiGetDeviceInterfaceDetail这个接口。返回值,ture则成功,false为失败。
4.SetupDiGetDeviceInterfaceDetail
BOOL
SetupDiGetDeviceInterfaceDetail(
IN HDEVINFO DeviceInfoSet,//指向设备信息集的指针,它包含了所要接收信息的接口。该句柄通常由SetupDiGetClassDevs函数返回。
IN PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData,//一个指向SP_DEVICE_INTERFACE_DETAIL_DATA结构的指针,该结构用于接收指定接口的信息
OUT PSP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData..OPTIONAL,
IN DWORD DeviceInterfaceDetailDataSize,//缓冲的大小。该缓冲的大小不能小于 (offsetof(SP_DEVICE_INTERFACE_DETAIL_DATA, DevicePath) + sizeof(TCHAR)) 字节。
OUT PDWORD RequiredSize..OPTIONAL,
OUT PSP_DEVINFO_DATA DeviceInfoData OPTIONAL
);
返回设备接口的详细信息。这里需要特别提一下的是,该接口需要调用两次,因为该接口主要是获取设备的路径而路径会放到DeviceDetailData面, 然而每一个设备的路径是不一样的, 所以大小不一定, 所以需要调用两次,第一次
获取路径的大小,第二次获取具体的路径信息。返回值,ture则成功,false为失败。
5.CreateFile
函数定义
HANDLE
CreateFile(
LPCTSTR lpFileName, //设备路径
DWORD dwDesiredAccess, //访问模式(写/读)
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全属性的指针
DWORD dwCreationDisposition, //如何创建
DWORD dwFlagsAndAttributes, //文件属性
HANDLE hTemplateFile //用于复制文件句柄
);
与设备建立连接,获取相应的属性(设备的供应商标识与产品标识等),应用程序就能够与对应的设备进行通信了。如果调用成功,该函数返回文件的句柄;如果调用失败,则返回INVALID_HANDLE_VALUE,在打开通信设备时,应该以独占方式打开。
6.HidD_GetAttributes
函数定义
HidD_GetAttributes(
SafeFileHandle hidDeviceObject,//对应与选定设备的句柄
out HiddAttributes attributes,//指向HIDD_ATTRIBUTES类型的指针
);
//设备属性结构体
struct HiddAttributes
{
int Size;
ushort VendorId;
ushort ProductId;
ushort VersionNumber;
};
获取设备的属性
已经介绍了这么多SetupDi API,那我们如何来利用这些API来实现我们的需求呢?下面就来介绍一下这些API一般情况下使用的步骤。
四、Setup API使用步骤
以下相关代码片段为C#。
1.调用HidD_GetHidGuid,获取Hid设备的Guid。当然,假如我们只需要获取磁盘相关设备的列表,且知道磁盘的Guid为53f56307-b6bf-11d0-94f2-00a0c91efb8b。则无须调用HidD_GetHidGuid函数。
示例代码:
var hUsb = Guid.Empty;
// 取得hid设备全局id
HidD_GetHidGuid(ref hUsb);
2.执行第1步,或者说知道指定的Guid,调用SetupDiGetClassDevs函数,获取Guid对应的设备集合。
示例代码:
//取得一个包含所有HID接口信息集合的句柄
var hidInfoSet = SetupDiGetClassDevs(ref hUsb, 0, IntPtr.Zero, Digcf.DIGCF_PRESENT | Digcf.DIGCF_DEVICEINTERFACE);
3.通过第2步,获取对应的设备列表,我们就可以获取其中某个接口的相关信息,这个时候,需要调用SetupDiEnumDeviceInterfaces来获取接口的信息了。
4.执行完第3步之后,则需要调用SetupDiGetDeviceInterfaceDetail来获取接口的详细信息,主要是获取对应设备的路径,此函数必须连续调用两次,详情见接口说明。
第3、4步的示例代码:
for (index = 0; index < MaxUsbDevices; index++)
{
//得到第index个接口信息
if (SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hUsb, index, ref interfaceInfo))
{
int buffsize = 0;
// 取得接口详细信息:第一次读取错误,但可以取得信息缓冲区的大小
SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, IntPtr.Zero, buffsize, ref buffsize, null);
//构建接收缓冲
var pDetail = Marshal.AllocHGlobal(buffsize);
var detail = new SpDeviceInterfaceDetailData
{
cbSize = Marshal.SizeOf(typeof(SpDeviceInterfaceDetailData))
};
Marshal.StructureToPtr(detail, pDetail, false);
if (SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, pDetail, buffsize, ref buffsize, null))
{
deviceList.Add(Marshal.PtrToStringAuto((IntPtr)((int)pDetail + 4)));
}
Marshal.FreeHGlobal(pDetail);
}
}
5.执行完第4步后,可以获取到设备的路径,通过调用CreateFile与设备创建连接之后调用HidD_GetAttributes获取设备对应的属性,执行完这一步,我们就可以完成上述应用场景中的MCU、触摸框等相关硬件的绑定,以及与应用程序通信
了,如图2. PID,VID属性。例如MCU的供应商标识为0x1ff7,产品标识为0x0f13,应用程序实现MCU绑定了。
示例代码:
//建立和设备的连接
_device = CreateFile(device, Desiredaccess.GENERIC_READ | Desiredaccess.GenericWrite, 0, 0, Creationdisposition.OPEN_EXISTING, Flagsandattributes.FILE_FLAG_OVERLAPPED, 0);
if (_device.IsInvalid) continue;
HiddAttributes attributes;
//获取连接属性
HidD_GetAttributes(_device, out attributes);
if (attributes.VendorId == vId && attributes.ProductId == pId)
{
IntPtr preparseData;
HidpCaps caps;
HidD_GetPreparsedData(_device, out preparseData);
//获取设备具体信息
HidP_GetCaps(preparseData, out caps);
HidD_FreePreparsedData(preparseData);//??
OutputReportLength = caps.OutputReportByteLength;//获取设备接收到字节的长度
InputReportLength = caps.InputReportByteLength;//获取设备发送的字节的长度
//根据设备初始化FileStream实例,通过流来实现数据接收与发送
_hidDevice = new FileStream(_device, FileAccess.ReadWrite, InputReportLength, true);
_deviceOpened = true;
return true;
}
总结:
通过调用上述API,以及执行对应的步骤,就能够应用程序实现硬件绑定,与硬件进行交互,当然,其中可能会遇到一些问题,需要有耐心,一步一步来。关于磁盘管理、识别大容量的移动设备等,在本文中没有讲到,
由于时间关系,过几天另起一章总结一下磁盘相关的知识。
相关图片:
图1. 设备Guid示例 图2. PID,VID属性