作者:陈广 日期:2020-11-5
之前已经配置好读卡器的基础软件,现在可以实现一些实用功能了。本想实现公交卡的刷卡功能,但转念一想,在刷卡之前,总得先往里面充值吧。于是就有了这篇文章,先实现一个简单的充值功能,练练手。
Mifare S50 是国内最常用的卡,但它比较特殊,只使用到了 ISO 14443-3 协议,不支持 ISO 14443-4。在使用 14443-3 实现防冲撞功能后,其它功能就需要使用 S50 自己的命令来处理了。这些命令使用的是标签生产厂商自行开发的协议,并非国际标准。这就是 ST25R3911B-DISCO 开发板不支持 Mifare S50 的原因,好在找到了现成的代码,要不让我自己写,就太痛苦了。
虽然有现成代码,但只实现了证实和读操作,其它的只能自己实现了。读了几天的 S50 数据手册,一点点试,幸运的是,虽然花掉不少时间,最终总算是实现了,没有杨白劳。国外我看不少人在找 ST25R3911B 的 Mifare Classic 开发库,STM 公司就是不提供,现在我这里有了。当然,并未实现所有的功能,程序写得很不健壮,如果商用,需要大规模修改,毕竟不是商业软件,仅用于教学,越简单越好,实现我需要的功能即可。
充值功能比较简单,为使程序更为直观,易用,我们使用上位机进行操作,这时就需要设计一个简单的通信协议。首先来分析一下整个充值程序需要实现的功能。
由于实现的功能非常简单,为让读者更容易理解及学习,协议设计能省则省,数据包长度以及 CRC 校验全不要了。要学习完整,功能强大的协议,请参考之前 ISO 14443 相关文章。
0x01(1字节) + UID(4字节)+ 卡内余额(4字节)
收到上位机读卡、格式化以及充值请求,成功操作后,返回卡的UID以及卡内余额。
0x02(1字节)+ UID(4字节)+ 错误码(1字节)
在收到上位机读卡请求,成功读取UID,但标签值块格式不对,无法读取余额时,返回此数据包
0xFF(1字节) + 错误码(1字节)
向上位机返回操作失败信息。
我们首先实现单片机端代码。由于读卡器进化到了 Version 3.5,加入了蜂鸣器,本例程使用到了蜂鸣器,所以需要进行配置。最方便的方法是在 STM32CubeMX 中进行配置,但考虑到重配 STM32CubeMX 会导致之前写的一些代码丢失,带来不必要的麻烦;另一方面,有部分读者使用 ST25R3911B-DISCO 评估板来学习本文,那是不带蜂鸣器的,所以最终决定单独拎出来手动配置。如果读者使用的是 ST25R3911B-DISCO,则可以忽略本小节。
本文的代码是在《基础程序配置》这篇文章代码的基础上进行编写的。
由于蜂鸣器使用的是 PA2 引脚,我们首先得给此引脚起个别名。打开 main.h 文件,找到/* USER CODE BEGIN Private defines */
注释下面的宏定义,加入两个宏定义:
#define BEEP_Pin GPIO_PIN_2
#define BEEP_GPIO_Port GPIOA
接下来配置 GPIO 引脚初始状态为低电平,打开 main.c 文件,找到static void MX_GPIO_Init(void)
方法,更改其中代码。找到
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, LED_TX_Pin|LED_A_Pin|LED_AP2P_Pin, GPIO_PIN_RESET);
将其更改为:
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, LED_TX_Pin|LED_A_Pin|LED_AP2P_Pin|BEEP_Pin, GPIO_PIN_RESET);
下面配置 GPIO 引脚为输出状态,找到
/*Configure GPIO pins : LED_TX_Pin LED_A_Pin LED_AP2P_Pin */
GPIO_InitStruct.Pin = LED_TX_Pin|LED_A_Pin|LED_AP2P_Pin;
更改为:
/*Configure GPIO pins : LED_TX_Pin LED_A_Pin LED_AP2P_Pin */
GPIO_InitStruct.Pin = LED_TX_Pin|LED_A_Pin|LED_AP2P_Pin|BEEP_Pin;
接下来写蜂鸣器的开关方法,没合适的地方放,直接写进 led.c 里吧。打开 led.c 文件,在最后添加如下代码:
void beepOn()
{
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET);
}
void beepOff()
{
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET);
}
void beep()
{
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET);
HAL_Delay(100);
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET);
}
现在还不能直接调用这两个方法,需要在头文件中声明,打开 led.h 文件,找到void ledFeedbackHandler(void);
,在其后添加两个方法:
void beepOn(void);
void beepOff(void);
void beep(void);
现在可以试试蜂鸣器响不响了,打开 main.c 文件,在while(1)
循环中找到 RFAL 初始化代码,改为:
if( rfalInitialize() == ERR_NONE )
{
logUsart("RFAL initialization succeeded..\n");
ledOn(LED_Field);
beepOn(); //这句是新添加的代码
}
也就是说,只有初始化成功,蜂鸣器才会响。
往下继续找到while(1)
,更改其上方代码:
logUsart("Start Find...\r\n");
rfalInitialize();
HAL_Delay(200);
beepOff(); //这句为新添加代码
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
将程序烧写进开发板,按 RESET 按钮,可听到蜂鸣器短促地响一声。
当上位机向单片机发送 HID 数据包时,单片机本质上是在中断中进行处理的。我们知道,一般情况下,在中断中不应当放耗时过长的处理程序,所以我们应当仅在中断中设置一个状态值,然后在main
方法的while
循环中不停地判断此状态值,然后根据状态值的不同进行相应的处理。
打开 main.h 文件,找到/* USER CODE BEGIN ET */
,在下方输入如下代码,以将四种状态声明为枚举:
/* USER CODE BEGIN ET */
typedef enum {
STATE_IDLE = 0, //空闲
STATE_FIND_CARD = 1, //寻卡
STATE_INI_CARD = 2, //值块格式化
STATE_INCREMENT = 3, //加值
} rechargeState;
/* USER CODE END ET */
接下来打开 usbd_custom_hid_if.c 文件,添加接收 HID 数据后的处理代码。
首先确保此文件包含以下头文件:
#include "usbd_custom_hid_if.h"
#include "logger.h"
#include "main.h"
#include "mf1.h"
找到/* USER CODE BEGIN EXPORTED_VARIABLES */
,在其下方添加以下代码以访问 main.c 里的变量:
extern rechargeState work_state;
extern uint8_t UID[4];
extern uint8_t amount[4];
找到CUSTOM_HID_OutEvent_FS
方法,更改此方法代码如下:
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
/* USER CODE BEGIN 6 */
UNUSED(event_idx);
UNUSED(state);
//接收到的数据放在 hhid 里面
USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)hUsbDeviceFS.pClassData;
//将数据原封不动地返回
if(work_state == STATE_IDLE)
{
uint8_t *buf = hhid->Report_buf;
if(buf[0] == 0x01) //寻卡
{ //设置为寻卡状态
work_state = STATE_FIND_CARD;
}
else if(buf[0] == 0x02) //卡初始化
{
if(compareUID(buf, 1, UID, 0)) //对比传过来的UID和之前读的是否一致
{ //设置为格式化值块状态
work_state = STATE_INI_CARD;
copyArray(buf, 1, UID, 0);
copyArray(buf, 5, amount, 0);
}
}
else if(buf[0] == 0x03) //加值
{
if(compareUID(buf, 1, UID, 0))
{ //设置为加值状态,增加的值存到main.c文件的amount变量中
work_state = STATE_INCREMENT;
copyArray(buf, 1, UID, 0);
copyArray(buf, 5, amount, 0);
}
}
}
/* Start next USB packet transfer once data processing is completed */
USBD_CUSTOM_HID_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
这里在接收到上位机数据包后,仅设置状态及保存数据,不做任何其它处理,访问标签的操作全部放到main
方法中
下面编写 Mifare S50 卡的操作程序并向上位机返回数据。
打开 main.c 文件,首先确保包含以下头文件:
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usb_device.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "platform.h"
#include "usbd_custom_hid_if.h"
#include "logger.h"
#include "spi.h"
#include "rfal_analogConfig.h"
#include "rfal_rf.h"
#include "st_errno.h"
#include "rfal_nfca.h"
#include "led.h"
#include "mcc.h"
#include "mf1.h"
#include "utils.h"
/* USER CODE END Includes */
接下来找到/* USER CODE BEGIN PV */
,更改代码如下:
/* USER CODE BEGIN PV */
uint8_t globalCommProtectCnt = 0;
rechargeState work_state = STATE_IDLE; //保存状态的变量
uint8_t UID[4]; //当前卡片的UID
uint8_t amount[4]; //当前卡片的余额
uint8_t buf[64] = {0}; //发送缓冲
/* USER CODE END PV */
在main
方法上方添加一个错误处理函数:
void SendError(uint8_t errCode)
{
buf[0] = 0xFF;
buf[1] = errCode;
USBD_CUSTOM_HID_SendReport_FS(buf, 64);
work_state = STATE_IDLE;
rfalNfcaPollerSleep();
ledOff(LED_A);
rfalFieldOff();
ledOff(LED_Field);
logUsart("error happen\r\n");
mccDeinitialise(true);
}
最后更改main
方法如下:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
//MX_LPUART1_UART_Init();
MX_USART1_UART_Init();
MX_SPI1_Init();
MX_USB_DEVICE_Init();
/* USER CODE BEGIN 2 */
spiInit(&hspi1);
logUsartInit(&huart1);
rfalAnalogConfigInitialize();
if( rfalInitialize() == ERR_NONE )
{
logUsart("RFAL initialization succeeded..\n");
ledOn(LED_Field);
beepOn();
}
else
{
while(1){
ledToggle(LED_A);
ledToggle(LED_B);
ledToggle(LED_F);
ledToggle(LED_V);
ledToggle(LED_Field);
HAL_Delay(40);
logUsart("RFAL initialization failed..\n");
}
}
ReturnCode err;
rfalNfcaSensRes sensRes;
rfalNfcaSelRes selRes;
rfalNfcaListenDevice nfcaDevList[10];
uint8_t devCnt;
uint8_t devIt;
uint8_t key = MCC_AUTH_KEY_A;
uint8_t mifareKey[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t sector = 1; //将读取的数据块所在扇区
uint8_t block = 4; //将读取的数据块的总块号
const uint32_t nonce = 0x12345678;
logUsart("Start Find...\r\n");
rfalInitialize();
HAL_Delay(200);
beepOff();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//HAL_Delay(1000);
if(work_state == STATE_IDLE) continue;
if(work_state == STATE_FIND_CARD)//寻卡
{
logUsart("start find card\r\n");
rfalFieldOff();//关闭感应场
rfalNfcaPollerInitialize(); //轮询初始化
rfalFieldOnAndStartGT(); //打开感应场
//检测NFCA标签
err = rfalNfcaPollerTechnologyDetection( RFAL_COMPLIANCE_MODE_NFC, &sensRes );
if(err != ERR_NONE)
{
SendError(err);
continue;
}
//如果存在NFCA标签,则执行防冲撞
err = rfalNfcaPollerFullCollisionResolution( RFAL_COMPLIANCE_MODE_NFC, 10, nfcaDevList, &devCnt );
if((err != ERR_NONE) || (devCnt == 0))
{
SendError(err);
continue;
}
ledOn(LED_A);
devIt = 0;
if(nfcaDevList[devIt].isSleep)
{
err = rfalNfcaPollerCheckPresence( RFAL_14443A_SHORTFRAME_CMD_WUPA, &sensRes );
if(err != ERR_NONE)
{
continue;
}
err = rfalNfcaPollerSelect( nfcaDevList[devIt].nfcId1, nfcaDevList[devIt].nfcId1Len, &selRes );
if(err != ERR_NONE)
{
continue;
}
}
//如果防冲撞操作中读取到了Mifare S50标签
if(nfcaDevList[devIt].type != RFAL_NFCA_T2T)
{
SendError(ERR_NO_MIFARECLASSIC);
continue;
}
if (!(nfcaDevList[devIt].selRes.sak & 0x08))
{
SendError(ERR_NO_MIFARECLASSIC);
continue;
}
logUsart("find Mifare S50 card");
mccInitialize();
copyArray(nfcaDevList[devIt].nfcId1, 0, UID, 0);
err = mccAuthenticate(key,sector*4,UID,4,mifareKey,nonce);
if(err != ERR_NONE)
{
SendError(err);
continue;
}
logUsart("Authenticate success\r\n");
beep();
}
else if(work_state == STATE_INI_CARD)
{
int32_t value = arrayToInt32(amount,0);
err = mccFormatValueBlock(block,value,block);
if(err != ERR_NONE)
{
SendError(err);
continue;
}
beep();
}
else if(work_state == STATE_INCREMENT)
{
int32_t value = arrayToInt32(amount,0);
err = mccIncrement(block,value);
if(err != ERR_NONE)
{
SendError(err);
continue;
}
beep();
}
buf[0] = 0x01;
copyArray(UID, 0, buf, 1);
int32_t num;
err = mccReadValue(block, &num);
if(err == ERR_NONE)
{
int32ToArray(num, buf, 5);
}
else
{
buf[0] = 0x02;
buf[5] = (uint8_t)err;
}
USBD_CUSTOM_HID_SendReport_FS(buf, 64);
work_state = STATE_IDLE;
//mccDeinitialise(true);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
需要注意,为了简化程序,这里写死了总块号为 4 的块进行充值,也就是第 2 扇区 0 号块。
原本以为上位机程序砍瓜切菜,但还是费了一些周折。原因是《USB HID 设备通信协议》这篇文章中的 HID.cs 文件中的代码只适用于文章串介绍的阅读器,不适用于其它设备,我把这事给忘了。调试了半天终于发现,改好了,所以代码读写需要一点点更改。
新建一个 WPF 应用程序,界面代码 MainWindow.xaml 如下:
<Window x:Class="Recharge.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Recharge"
mc:Ignorable="d"
Title="Mifare S50卡充值程序" Height="207.941" Width="300" Loaded="Window_Loaded">
<Grid Margin="2">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Row="3" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="btnFindCard" IsEnabled="False" Content="寻卡" Margin="8" Grid.Column="0" Click="btnFindCard_Click"/>
<Button x:Name="btnFormat" IsEnabled="False" Content="格式化" Margin="8" Grid.Column="1" Click="btnFormat_Click"/>
<Button x:Name="btnIncrement" IsEnabled="False" Content="充值" Margin="8" Grid.Column="2" Click="btnIncrement_Click"/>
</Grid>
<TextBlock Grid.Row="0" Grid.Column="0" Text="UID" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="卡内余额" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="充值金额" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBox x:Name="txtUID" Grid.Row="0" Grid.Column="1" Margin="8" IsReadOnly="True"/>
<TextBox x:Name="txtBalance" IsReadOnly="True" Grid.Row="1" Grid.Column="1" Margin="8"/>
<TextBox x:Name="txtIncrementValue" Grid.Row="2" Grid.Column="1" Margin="8"/>
<StatusBar Grid.Row="4" Grid.ColumnSpan="2">
<TextBlock x:Name="tbMsg"/>
</StatusBar>
</Grid>
</Window>
界面如下图如示:
新建一个 HID.cs 文件,代码如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace Recharge
{
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;
Array.Copy(data, 0, buffer, 1, 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);
byte[] newBuf = new byte[inputReportLength - 1];
Array.Copy(buff, 1, newBuf, 0, inputReportLength-1);
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
}
主窗体代码 MainWindow.xaml.cn 如下:
using System;
using System.Text;
using System.Windows;
namespace Recharge
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
HID hid;
byte[] uid = new byte[4];
int balance = 0;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
hid = new HID(1366, 22352, "");
if (hid.OpenDevice() == PortReturn.Success)
{
tbMsg.Text = "成功连接阅读器";
btnFindCard.IsEnabled = true;
}
else
{
tbMsg.Text = "阅读器连接失败";
}
}
private void btnFindCard_Click(object sender, RoutedEventArgs e)
{
byte[] frame = { 0x01 };
if (hid.Write(frame) == PortReturn.Success)
{
byte[] recv = hid.Read();
if (recv[0] == 0xFF)
{
tbMsg.Text = "寻卡失败,请检查标签是否已经放入感应场。";
btnFormat.IsEnabled = false;
btnIncrement.IsEnabled = false;
return;
}
btnFormat.IsEnabled = true;
btnIncrement.IsEnabled = true;
GetUIDAndBalance(recv);
}
else
{
tbMsg.Text += "通信错误";
}
}
private void btnFormat_Click(object sender, RoutedEventArgs e)
{ //格式化值块,将初始值置0
byte[] frame = new byte[9];
frame[0] = 0x02;
Array.Copy(uid, 0, frame, 1, 4); //拷贝UID
tbMsg.Text = "";
if (hid.Write(frame) == PortReturn.Success)
{
byte[] recv = hid.Read();
if (recv[0] == 0xFF)
{
tbMsg.Text = "格式化失败。";
btnIncrement.IsEnabled = false;
return;
}
GetUIDAndBalance(recv);
tbMsg.Text = "格式化成功,卡内余额已经清零。";
}
else
{
tbMsg.Text += "通信错误";
}
}
private void btnIncrement_Click(object sender, RoutedEventArgs e)
{
if (txtIncrementValue.Text == "")
{
tbMsg.Text = "请在充值金额编辑框内输入充值金额。";
return;
}
double yuan; //输入元
if (!double.TryParse(txtIncrementValue.Text, out yuan))
{
tbMsg.Text = "充值金额格式不正确,请重新输入!";
return;
}
if (yuan <= 0)
{
tbMsg.Text = "充值金额必须大于0,请重新输入!";
return;
}
if ((yuan * 100 + balance) > int.MaxValue)
{
tbMsg.Text = "充值后,卡内总金额超出范围,请重新输入!";
return;
}
int bean = (int)(yuan * 100); //转换为分
byte[] frame = new byte[9];
frame[0] = 0x03;
Array.Copy(uid, 0, frame, 1, 4);
byte[] bal = BitConverter.GetBytes(bean);
Array.Copy(bal, 0, frame, 5, 4);
tbMsg.Text = "";
if (hid.Write(frame) == PortReturn.Success)
{
byte[] recv = hid.Read();
if (recv[0] == 0xFF)
{
tbMsg.Text = "充值失败。" + recv[1].ToString();
return;
}
GetUIDAndBalance(recv);
tbMsg.Text = "成功充入" + Convert.ToString(((double)bean) / 100) + "元钱。";
}
else
{
tbMsg.Text += "通信错误";
}
}
private void GetUIDAndBalance(byte[] recv)
{
StringBuilder sb = new StringBuilder(4);
for (int i = 1; i < 5; i++)
{
sb.Append(recv[i].ToString("X2") + " ");
uid[i - 1] = recv[i];
}
txtUID.Text = sb.ToString();
btnFormat.IsEnabled = true;
btnIncrement.IsEnabled = true;
if (recv[0] == 0x01) //读取出值块的值的情况
{
balance = BitConverter.ToInt32(recv, 5);
txtBalance.Text = Convert.ToString(((double)balance) / 100);
}
else if (recv[0] == 0x02) //成功读取UID,但由于值块格式不对,导致读值失败的情况
{
txtBalance.Text = "";
tbMsg.Text = "Mifare S50卡格式不对,请先对卡片进行格式化。";
}
}
}
}
程序较简单,需要注意的是,如果运行过程中出现错误,需要重新寻卡操作。运行效果如下:
程序远远不够完善,最大的问题在于使用了同步的方法读写 USB 数据。如果仔细查看单片机代码,可知,有些操作如果出现错误,是不会返回任何结果的,此时会导致上位机程序阻塞,只能重启程序解决问题,这样肯定不行。
思考题一:就是将所有 USB 数据的读写放入线程中执行,每发一条命令如果在限定时间内(timeout)等不到返回,则关闭线程。
思考题二:修改代码,添加新协议,使程序能够指定扇区以及数据块进行值操作。
;