C#网络编程

USB HID 设备通信协议

作者:陈广 日期:2019-1-24


ID 卡通信协议很简单,很快弄完了,接下来要考虑 IC 卡怎么讲了。从 13.56MHz 的 ISO14443 开始。学校实验室配的实验箱是有这个设备,我看了下实验指导书,只讲了很简单的一小部分协议,如果要做模拟器,我肯定需要完整的协议文档,再加上能用的实验箱没几台了,这条路肯定走不通。然后我在实验室找到了几台便携式的 13.56MHz 读卡器,上面居然一个字都木有,驱动也没找到,协议文档更无从谈起。没办法,只能自己掏银子上万能的淘宝买了。

目标很明确,要便宜,要能读多种卡,要有完整源码,最好是 C# 的,要有完整协议文档。还真有这样的设备,话不多说,先上淘宝购买链接:

上海讯闪电子有限公司出品,型号 R321,159 大洋,加运费 174 大洋,可读 ISO14443A/B、ISO15693 卡,以及多种品牌的 NFC 手机。相当划算啊!这一个系列文章如果大家要学的话,还是建议买一个。实物操作起来更有感觉,另外它还有一个重要的功能,就是透传,允许你直接使用 ISO 14443 协议操作阅读器。虽然以后我会写模拟器,但只会实现它的小部分功能,够我上课用就行了。

图 1:R321 读卡器

东西弄回来后立马研究,它配有一个使用 C# 开发的完整的、功能较为强大的 Demo,帮助你学习理解各种卡的使用及原理,但最终我发现,核心通信是用 C++ 写的,而且被包装在了 DLL 里面,并没有提供源码。这难不倒我,有完整的通信协议文档,自己搞定就行,而且如果要写模拟器,这块始终都是要自己写的。在着手写程序的时候,突然发现这个读卡器居然使用的不是串口通信协议。而是 USB 的 HID 协议。先来看看在我电脑上的【设备管理器】中这个设备长什么样子:

图 2:设备管理器

虽然他们公司也提供串口读卡器,但现在谁的电脑上还有串口啊。这个坑有点大,没办法,只能花时间研究 HID 了。研究了 HID 后,发现这个东西相当不错,最大的好处是不用装驱动,即插即用。之前的 ID 读卡器装串口转 USB 驱动就比较头痛,最后用 360 驱动大师解决问题。现在相当多的设备都是使用的 HID,学会 HID 设备的编程还是相当不错的。唯一的问题是模拟器,不过问题不大,到时模拟器还是用串口或 Socket 来写,把通信层包装到底层,可以很方便地切换就行了。

HID 简介

HID(Human Interface Device,人机接口设备)是 USB 设备中常用的设备类型,由其名称可以了解 HID 设备是直接与人交互的设备,例如键盘、鼠标与游戏杆等。在 USB 设备中,HID 设备的成本较低。另外,HID设备并不一定要有人机接口,只要符合 HID 类别规范的设备都是 HID 设备。

Windows 操作系统最先支持 HID 设备。在 Windows 98 以及后来的版本中内置有 HID 设备的驱动程序。USB HID 设备的一个好处就是操作系统自带了 HID 类的驱动程序,而用户无需去开发驱动程序,只需调用系统 API 即可完成通信。

HID 设备的特点为:

  • 交换的数据储存在称为报表(Report)的结构内,设备的固件必须支持 HID 报表的格式。主机通过控制和中断传输中的传送和请求报表来传送和接收数据。报表的格式非常灵活。
  • 每一笔事务可以携带小量或中量的数据。低速设备每一笔事务最大是 8B,全速设备每一笔事务最大是 64B,高速设备每一笔事务最大是 1024B。一个报表可以使用多笔事务。
  • 设备可以在未预期的时间传送信息给主机,例如键盘的按键或是鼠标的移动。所以主机会定时轮询设备,以取得最新的数据。
  • HID 设备的最大传输速度有限制。主机可以保证低速的中断端点每 10ms 内最多 1 笔事务,每 1 秒最多是 800B。保证全速端点每 1ms 一笔事务,每一秒最多是 64000B。保证高速端点每 125us 三笔事务,每一秒最多是 24.576MB。
  • HID 设备没有保证的传输速率。如果设备是设置在 10ms 的时距,事务之间的时间可能等于或小于 10ms。除非设备是设置在全速时在每个帧传输数据,或是在高速时在每个微帧传输数据。这是最快的轮询速率,所以端点可以保证有正确的带宽可供使用。

HID 设备除了传送数据给主机外,它也会从主机接收数据。只要能够符合 HID 类别规范的设备都可以是 HID 设备。

HID 类别设备的规范文件主要是以下两份:

  • Device Class Definition for Human interface Devices
  • HID Usage Tables

其中前者是 HID 的基本规范文件,后者可以是前者的附件,为开发人员提供实际的控制类型的描述。这两份文件是由 USB Device Working Group 制定的,可以在网址 https://www.usb.org/hid#Class_Definition下载。

编程实现 HID 通信

在 github 和 Vistal Studio 的 NuGet 上可以搜到很多 HID 开发包。代码都很长,很难下手学习。最后在网上找到了一份相对简单,又能运行的代码。复杂度还是相当高,它将数据接收和串口一样,包装在了事件里面,这需要很多代码实现。并不利于初学,我一顿操作,删了大半!,搞了一个读写器上可用的最小版本。

HID 类库

新建一个控制台应用程序,新建一个名为 HID.cs 的文件,输入如下代码

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HidDemo
{
    class HID
    {
        #region 以下是调用windows的API的函数
        [DllImport("hid.dll")]
        private static extern void HidD_GetHidGuid(ref Guid HidGuid);

        [DllImport("setupapi.dll", SetLastError = true)]
        private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, DIGCF Flags);

        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern Boolean SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);

        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern Boolean SetupDiEnumDeviceInterfaces(IntPtr deviceInfoSet, IntPtr deviceInfoData, ref Guid interfaceClassGuid, UInt32 memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);

        [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, SP_DEVINFO_DATA deviceInfoData);

        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetAttributes(IntPtr hidDeviceObject, out HIDD_ATTRIBUTES attributes);

        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetSerialNumberString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);

        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr PreparsedData);

        [DllImport("hid.dll")]
        private static extern Boolean HidD_FreePreparsedData(IntPtr PreparsedData);

        [DllImport("hid.dll")]
        private static extern uint HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFile(string fileName, uint desiredAccess, uint shareMode, uint securityAttributes, uint creationDisposition, uint flagsAndAttributes, uint templateFile);
        #endregion

        #region 结构体、编码
        public struct SP_DEVICE_INTERFACE_DATA
        {
            public int cbSize;
            public Guid interfaceClassGuid;
            public int flags;
            public int reserved;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 2)]
        internal struct SP_DEVICE_INTERFACE_DETAIL_DATA
        {
            internal int cbSize;
            internal short devicePath;
        }

        [StructLayout(LayoutKind.Sequential)]
        public class SP_DEVINFO_DATA
        {
            public int cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
            public Guid classGuid = Guid.Empty; // temp
            public int devInst = 0; // dumy
            public int reserved = 0;
        }

        public struct HIDD_ATTRIBUTES
        {
            public int Size;
            public ushort VendorID;
            public ushort ProductID;
            public ushort VersionNumber;
        }

        public struct HIDP_CAPS
        {
            public ushort Usage;
            public ushort UsagePage;
            public ushort InputReportByteLength;
            public ushort OutputReportByteLength;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
            public ushort[] Reserved;
            public ushort NumberLinkCollectionNodes;
            public ushort NumberInputButtonCaps;
            public ushort NumberInputValueCaps;
            public ushort NumberInputDataIndices;
            public ushort NumberOutputButtonCaps;
            public ushort NumberOutputValueCaps;
            public ushort NumberOutputDataIndices;
            public ushort NumberFeatureButtonCaps;
            public ushort NumberFeatureValueCaps;
            public ushort NumberFeatureDataIndices;
        }

        static class DESIREDACCESS
        {
            public const uint GENERIC_READ = 0x80000000;
            public const uint GENERIC_WRITE = 0x40000000;
            public const uint GENERIC_EXECUTE = 0x20000000;
            public const uint GENERIC_ALL = 0x10000000;
        }

        static class CREATIONDISPOSITION
        {
            public const uint CREATE_NEW = 1;
            public const uint CREATE_ALWAYS = 2;
            public const uint OPEN_EXISTING = 3;
            public const uint OPEN_ALWAYS = 4;
            public const uint TRUNCATE_EXISTING = 5;
        }

        static class FLAGSANDATTRIBUTES
        {
            public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
            public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
            public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
            public const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000;
            public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
            public const uint FILE_FLAG_DELETE_ON_CLOSE = 0x04000000;
            public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
            public const uint FILE_FLAG_POSIX_SEMANTICS = 0x01000000;
            public const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
            public const uint FILE_FLAG_OPEN_NO_RECALL = 0x00100000;
            public const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
        }

        #endregion

        private IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
        private const int MAX_USB_DEVICES = 64;
        private bool deviceOpened = false;
        private FileStream hidDevice = null;
        private IntPtr hHubDevice;
        private UInt16 vID;
        private UInt16 pID;
        private string serial;

        public HID(UInt16 vid, UInt16 pid, string sl)
        {
            vID = vid; //厂商 ID
            pID = pid; //产品 ID
            serial = sl; //序列号
        }

        int outputReportLength;//输出报告长度,包刮一个字节的报告ID
        public int OutputReportLength
        {
            get { return outputReportLength; }
        }
        int inputReportLength;//输入报告长度,包刮一个字节的报告ID   
        public int InputReportLength
        {
            get { return inputReportLength; }
        }

        public PortReturn OpenDevice()
        {
            if (deviceOpened == false)
            {
                //获取连接的HID列表
                List<string> deviceList = new List<string>();
                GetHidDeviceList(ref deviceList);
                if (deviceList.Count == 0)
                    return PortReturn.NoDeviceConected;
                for (int i = 0; i < deviceList.Count; i++)
                {
                    IntPtr device = CreateFile(deviceList[i],
                            DESIREDACCESS.GENERIC_READ | DESIREDACCESS.GENERIC_WRITE,
                            0,
                            0,
                            CREATIONDISPOSITION.OPEN_EXISTING,
                            FLAGSANDATTRIBUTES.FILE_FLAG_OVERLAPPED,
                            0);
                    if (device != INVALID_HANDLE_VALUE)
                    {
                        HIDD_ATTRIBUTES attributes;
                        IntPtr serialBuff = Marshal.AllocHGlobal(512);
                        HidD_GetAttributes(device, out attributes);
                        HidD_GetSerialNumberString(device, serialBuff, 512);
                        string deviceStr = Marshal.PtrToStringAuto(serialBuff);
                        Marshal.FreeHGlobal(serialBuff);
                        if (attributes.VendorID == vID && attributes.ProductID == pID && deviceStr.Contains(serial))
                        {
                            IntPtr preparseData;
                            HIDP_CAPS caps;
                            HidD_GetPreparsedData(device, out preparseData);
                            HidP_GetCaps(preparseData, out caps);
                            HidD_FreePreparsedData(preparseData);
                            outputReportLength = caps.OutputReportByteLength;
                            inputReportLength = caps.InputReportByteLength;

                            hidDevice = new FileStream(new SafeFileHandle(device, false), FileAccess.ReadWrite, inputReportLength, true);
                            deviceOpened = true;
                            //BeginAsyncRead();

                            hHubDevice = device;
                            return PortReturn.Success;
                        }
                    }
                }
                return PortReturn.DeviceNotFond;
            }
            else
            {
                return PortReturn.DeviceOpened;
            }
        }

        /// <summary>
        /// 关闭打开的设备
        /// </summary>
        public void CloseDevice()
        {
            if (deviceOpened == true)
            {
                deviceOpened = false;
                hidDevice.Close();
            }
        }

        /// <summary>
        /// 获取所有连接的hid的设备路径
        /// </summary>
        /// <returns>包含每个设备路径的字符串数组</returns>
        public static void GetHidDeviceList(ref List<string> deviceList)
        {
            Guid hUSB = Guid.Empty;
            uint index = 0;

            deviceList.Clear();
            // 取得hid设备全局id
            HidD_GetHidGuid(ref hUSB);
            //取得一个包含所有HID接口信息集合的句柄
            IntPtr hidInfoSet = SetupDiGetClassDevs(ref hUSB, 0, IntPtr.Zero, DIGCF.DIGCF_PRESENT | DIGCF.DIGCF_DEVICEINTERFACE);
            if (hidInfoSet != IntPtr.Zero)
            {
                SP_DEVICE_INTERFACE_DATA interfaceInfo = new SP_DEVICE_INTERFACE_DATA();
                interfaceInfo.cbSize = Marshal.SizeOf(interfaceInfo);
                //查询集合中每一个接口
                for (index = 0; index < MAX_USB_DEVICES; 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);
                        //构建接收缓冲
                        IntPtr pDetail = Marshal.AllocHGlobal(buffsize);
                        SP_DEVICE_INTERFACE_DETAIL_DATA detail = new SP_DEVICE_INTERFACE_DETAIL_DATA();
                        detail.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA));
                        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);
                    }
                }
            }
            SetupDiDestroyDeviceInfoList(hidInfoSet);
        }
        //发送数据
        public PortReturn Write(byte[] data)
        {
            if (!deviceOpened) return PortReturn.Write_Faild;
            if (data.Length > outputReportLength)
            {
                return PortReturn.BuffOutOfRange;
            }
            try
            {
                byte[] buffer = new byte[outputReportLength];
                buffer[0] = 0;
                buffer[1] = (byte)data.Length;

                Array.Copy(data, 0, buffer, 2, data.Length);
                hidDevice.Write(buffer, 0, outputReportLength);
                return PortReturn.Success;
            }
            catch
            {
                CloseDevice();
                return PortReturn.NoDeviceConected;
            }
        }
        //异步读取数据
        public byte[] Read()
        {
            try
            {
                byte[] buff = new byte[inputReportLength];
                hidDevice.Read(buff, 0, buff.Length);
                int count = buff[1];
                byte[] newBuf = new byte[count];
                Array.Copy(buff, 2, newBuf, 0, count);
                return newBuf;
            }
            catch (Exception e)
            {
                CloseDevice();
                return null;
            }
        }
    }
    #region 枚举

    public enum DIGCF
    {
        DIGCF_DEFAULT = 0x00000001,                 
        DIGCF_PRESENT = 0x00000002,
        DIGCF_ALLCLASSES = 0x00000004,
        DIGCF_PROFILE = 0x00000008,
        DIGCF_DEVICEINTERFACE = 0x00000010
    }

    public enum PortReturn
    {
        Success = 0,
        NoDeviceConected,
        DeviceNotFond,
        DeviceOpened,
        Write_Faild,
        ReadFaild,
        BuffOutOfRange
    }
    #endregion
}

HID 设备有二个我们需要知道的信息:

  1. VID :Vendor ID,供应商识别码,它由供应商向 USB-IF 申请。每个供应准则的 VID 是唯一的。
  2. PID :Product ID,产品识别码,由供应商自行决定。

主机通过 VID 和 PID 来识别不同的设备,根据它们(以及设备的版本号)可以给设备加载或安装相应的驱动程序。VID 和 PID 的长度都是两个字节的。

我手上的 R321 阅读器的 VID 为 0x0505,PID 为 0x5050。

这段代码一共分为三个部分:

  1. 第一部分是将 DLL 中的 Windows API 包装成 C# 可调用的方法。使用[DllImport]特性实现此功能。
  2. 第二部分是包装符合 C++ 标准的, DLL 中使用的结构体,以及 C# 程序自己使用的结构体和一些数字常量。
  3. 实现了 5 个方法:
    • 打开 HID 设备
    • 列出所有 HID 设备
    • 关闭 HID 设备
    • 向 HID 设备发送数据
    • 从 HID 设备读取数据

其它的代码我们可以不必关注,基本和 C++ 相关。只需读懂发和收数据的方法即可。其实发送数据就是向一个FileStream写入数据;收数据就是从FileStream接收数据。读写数据我都使用了最简单的同步方法。将来我会根据程序的需要使用异步读写,使其更为强壮。

这里需要注意的是 R321 RFID 阅读器为全速 HID 设备,意味着每次发送为 64 个字节。即使只需发送 1 个字节,也需要将整个 64 个字节发送过去;如果超过 64 个字节,需要编写代码进行处理。这一块将来需要用到的时候再更改。

使用类库与阅读器通信

最简类库搞定,接下来尝试与阅读器进行通信,检验成果。在Main方法中输入如下代码:

HID hid = new HID(0x0505, 0x5050, "");
if (hid.OpenDevice() == PortReturn.Success)
{
    Console.WriteLine("成功连接阅读器");
    byte[] frame =
    {
        0x7E, 0x55, 0x09, 0x00, 0x00,0x01,
        0x00, 0xF5, 0x00, 0x00, 0xBD, 0x91
    };
    if (hid.Write(frame) == PortReturn.Success)
    {
        Console.WriteLine("向阅读器发送数据:");
        foreach (byte b in frame)
        {
            Console.Write(b.ToString("X2") + " ");
        }
        byte[] recv = hid.Read();
        Console.WriteLine("\r\n\r\n接收到阅读器传来的数据:");
        foreach (byte b in recv)
        {
            Console.Write(b.ToString("X2") + " ");
        }
    }
    else
    {
        Console.WriteLine("数据发送失败");
    }                
}
else
{
    Console.Write("连接失败");
}

Console.ReadLine();

将阅读器 USB 口连上电脑,运行程序,将得到如下结果:

成功连接阅读器
向阅读器发送数据:
7E 55 09 00 00 01 00 F5 00 00 BD 91

接收到阅读器传来的数据:
7E 55 0F 01 00 00 00 1F F5 00 12 01 00 B7 01 01 D1 C1

做好这些准备之后,我们就可以开始研究 14443 了。

;

© 2018 - IOT小分队文章发布系统 v0.3