作者:陈广 日期:2019-1-24
ID 卡通信协议很简单,很快弄完了,接下来要考虑 IC 卡怎么讲了。从 13.56MHz 的 ISO14443 开始。学校实验室配的实验箱是有这个设备,我看了下实验指导书,只讲了很简单的一小部分协议,如果要做模拟器,我肯定需要完整的协议文档,再加上能用的实验箱没几台了,这条路肯定走不通。然后我在实验室找到了几台便携式的 13.56MHz 读卡器,上面居然一个字都木有,驱动也没找到,协议文档更无从谈起。没办法,只能自己掏银子上万能的淘宝买了。
目标很明确,要便宜,要能读多种卡,要有完整源码,最好是 C# 的,要有完整协议文档。还真有这样的设备,话不多说,先上淘宝购买链接:
上海讯闪电子有限公司出品,型号 R321,159 大洋,加运费 174 大洋,可读 ISO14443A/B、ISO15693 卡,以及多种品牌的 NFC 手机。相当划算啊!这一个系列文章如果大家要学的话,还是建议买一个。实物操作起来更有感觉,另外它还有一个重要的功能,就是透传,允许你直接使用 ISO 14443 协议操作阅读器。虽然以后我会写模拟器,但只会实现它的小部分功能,够我上课用就行了。
东西弄回来后立马研究,它配有一个使用 C# 开发的完整的、功能较为强大的 Demo,帮助你学习理解各种卡的使用及原理,但最终我发现,核心通信是用 C++ 写的,而且被包装在了 DLL 里面,并没有提供源码。这难不倒我,有完整的通信协议文档,自己搞定就行,而且如果要写模拟器,这块始终都是要自己写的。在着手写程序的时候,突然发现这个读卡器居然使用的不是串口通信协议。而是 USB 的 HID 协议。先来看看在我电脑上的【设备管理器】中这个设备长什么样子:
虽然他们公司也提供串口读卡器,但现在谁的电脑上还有串口啊。这个坑有点大,没办法,只能花时间研究 HID 了。研究了 HID 后,发现这个东西相当不错,最大的好处是不用装驱动,即插即用。之前的 ID 读卡器装串口转 USB 驱动就比较头痛,最后用 360 驱动大师解决问题。现在相当多的设备都是使用的 HID,学会 HID 设备的编程还是相当不错的。唯一的问题是模拟器,不过问题不大,到时模拟器还是用串口或 Socket 来写,把通信层包装到底层,可以很方便地切换就行了。
HID(Human Interface Device,人机接口设备)是 USB 设备中常用的设备类型,由其名称可以了解 HID 设备是直接与人交互的设备,例如键盘、鼠标与游戏杆等。在 USB 设备中,HID 设备的成本较低。另外,HID设备并不一定要有人机接口,只要符合 HID 类别规范的设备都是 HID 设备。
Windows 操作系统最先支持 HID 设备。在 Windows 98 以及后来的版本中内置有 HID 设备的驱动程序。USB HID 设备的一个好处就是操作系统自带了 HID 类的驱动程序,而用户无需去开发驱动程序,只需调用系统 API 即可完成通信。
HID 设备的特点为:
HID 设备除了传送数据给主机外,它也会从主机接收数据。只要能够符合 HID 类别规范的设备都可以是 HID 设备。
HID 类别设备的规范文件主要是以下两份:
其中前者是 HID 的基本规范文件,后者可以是前者的附件,为开发人员提供实际的控制类型的描述。这两份文件是由 USB Device Working Group 制定的,可以在网址 https://www.usb.org/hid#Class_Definition下载。
在 github 和 Vistal Studio 的 NuGet 上可以搜到很多 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 设备有二个我们需要知道的信息:
主机通过 VID 和 PID 来识别不同的设备,根据它们(以及设备的版本号)可以给设备加载或安装相应的驱动程序。VID 和 PID 的长度都是两个字节的。
我手上的 R321 阅读器的 VID 为 0x0505,PID 为 0x5050。
这段代码一共分为三个部分:
[DllImport]
特性实现此功能。其它的代码我们可以不必关注,基本和 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 了。
;