射频识别(RFID)技术

实现 Mifare S50 充值功能

作者:陈广 日期:2020-11-5


之前已经配置好读卡器的基础软件,现在可以实现一些实用功能了。本想实现公交卡的刷卡功能,但转念一想,在刷卡之前,总得先往里面充值吧。于是就有了这篇文章,先实现一个简单的充值功能,练练手。

Mifare S50 是国内最常用的卡,但它比较特殊,只使用到了 ISO 14443-3 协议,不支持 ISO 14443-4。在使用 14443-3 实现防冲撞功能后,其它功能就需要使用 S50 自己的命令来处理了。这些命令使用的是标签生产厂商自行开发的协议,并非国际标准。这就是 ST25R3911B-DISCO 开发板不支持 Mifare S50 的原因,好在找到了现成的代码,要不让我自己写,就太痛苦了。

虽然有现成代码,但只实现了证实和读操作,其它的只能自己实现了。读了几天的 S50 数据手册,一点点试,幸运的是,虽然花掉不少时间,最终总算是实现了,没有杨白劳。国外我看不少人在找 ST25R3911B 的 Mifare Classic 开发库,STM 公司就是不提供,现在我这里有了。当然,并未实现所有的功能,程序写得很不健壮,如果商用,需要大规模修改,毕竟不是商业软件,仅用于教学,越简单越好,实现我需要的功能即可。

需求分析

充值功能比较简单,为使程序更为直观,易用,我们使用上位机进行操作,这时就需要设计一个简单的通信协议。首先来分析一下整个充值程序需要实现的功能。

  1. 卡片放入读卡器感应场,上位机发送读卡命令,读卡器读卡,向上位机返回 UID。这里分两种情况:
    • 读取指定块值,如果成功,则在上位机显示卡内途额。
    • 读取指定块值,如果失败(块未进行格式化),则允许上位机以指定金额对相应块进行格式化。
  2. 在标签存在于读卡器感应场的情况下,单击充值按钮,将指定金额写入卡片内。
  3. 充值完成后,读卡器读取卡内余额,并发送至上位机显示。

通信协议的设计

由于实现的功能非常简单,为让读者更容易理解及学习,协议设计能省则省,数据包长度以及 CRC 校验全不要了。要学习完整,功能强大的协议,请参考之前 ISO 14443 相关文章。

读卡器发往上位机

  1. 0x01(1字节) + UID(4字节)+ 卡内余额(4字节)
    收到上位机读卡、格式化以及充值请求,成功操作后,返回卡的UID以及卡内余额。

  2. 0x02(1字节)+ UID(4字节)+ 错误码(1字节)
    在收到上位机读卡请求,成功读取UID,但标签值块格式不对,无法读取余额时,返回此数据包

  3. 0xFF(1字节) + 错误码(1字节)
    向上位机返回操作失败信息。

上位机发往读卡器

  1. 0x01(1字节)
    寻卡命令
  2. 0x02(1字节) + UID(4字节) + 格式化初始金额(4字节)
    读卡器按指定金额格式化卡片的 1 号扇区的 0 号块(绝对块号:4)。这里统一指定存钱的块号,如果允许让用户指定块号进行操作,则协议会复杂不少。
  3. 0x03(1字节) + UID(4字节) + 增加金额(4字节)
    读卡器将增加的金额加入 4 号块。

读写器固件代码设计

配置蜂鸣器

我们首先实现单片机端代码。由于读卡器进化到了 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 数据包处理

当上位机向单片机发送 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>

界面如下图如示:

图 1:界面设计

HID 驱动

新建一个 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卡格式不对,请先对卡片进行格式化。";
            }
        }
    }
}

程序较简单,需要注意的是,如果运行过程中出现错误,需要重新寻卡操作。运行效果如下:

图 2:运行效果

思考

程序远远不够完善,最大的问题在于使用了同步的方法读写 USB 数据。如果仔细查看单片机代码,可知,有些操作如果出现错误,是不会返回任何结果的,此时会导致上位机程序阻塞,只能重启程序解决问题,这样肯定不行。

思考题一:就是将所有 USB 数据的读写放入线程中执行,每发一条命令如果在限定时间内(timeout)等不到返回,则关闭线程。

思考题二:修改代码,添加新协议,使程序能够指定扇区以及数据块进行值操作。

;

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