应用程序与硬件进行交互(实现硬件绑定、应用程序与硬件通信)

时间:2024-03-10 11:40:52


一、前言
    在无操作系统的裸机中,或者类似如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属性