射频识别(RFID)技术

Mifare S50 刷卡程序

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


之前在《实验:值块读写操作》这篇文章中,使用网购现成的读写器编写了一个刷卡程序。实现原理是上位机不停地向读写器发送指令,让读卡器寻卡,如果找到卡,则扣钱。此时,上位机需要频繁地通过串口向单片机发送信息,单片机也需要不停地处理信息,这多少会造成资源的浪费。另外,现实生活中,刷卡的地方不可能拖一台电脑在旁边吧!可以使用树莓派之类的设备做上位机向云端发送数据。问题在于,如果树莓派都不想用怎么办呢?因为完全可以加一个 NB-IOT 芯片向云端发送数据。自己做开发板完全可以解决这些问题,想要什么功能都可以实现,其乐无穷啊!

设计概述

首先,ST25R3911B 这块芯片支持唤醒模式,它以低功耗模式运行,定时唤醒寻卡,当感应到卡片时,可以向主控芯片发送一个中断。这样主控就没有必要频繁地通过 SPI 向 ST25R3911B 芯片发送寻卡指令,只需在收到中断时再指示 ST25R3911B 进行寻卡等一系列操作。主控也只有在寻找到 Mifare S50 的情况下,才会向上位机发送数据。原来上位机是主动发送数据的一方,现在角色发生转换,读卡器变为主动发送数据一方,上位机只需侦听即可。甚至于可以不用上位机,接一个 NB-IOT 直接上云,或通过 WIFI 或以太网上局域网即可。

ST253911B 在唤醒模式下有三种方式可以检测卡片的存在:相位、幅度和电容传感器。电容传感器是通过天线内部的两个铜片进行检测的,我这块不太灵敏,不懂是铜片面积不够大,还是电路设计错误。不管怎么说,电容传感器不太好用,因为把手放上去和放卡片的效果一样。使用相位或幅度的情况下,手放上去不会有任何效果。另外幅度的灵敏度更高,所以最终我使用了幅度检测的方法。

由于,我的假设是没有上位机,所以不做 HID 通信,刷卡时只是简单地通过串口发送卡ID、余额、刷卡金额等文字信息。

刷卡程序编写

这一次,我选择将刷卡代码单独放在一个文件中实现,减少 main.c 文件的代码量。新建一个 swing_card.h 文件,输入代码如下:

#ifndef SWING_CARD_H
#define SWING_CARD_H

#include "platform.h"
#include "st_errno.h"

void setSt25R3911WakeupMode(uint8_t offset);
ReturnCode swingCard(int32_t value);
uint8_t GetSt25R3911Amplitude(void);
bool searchMifareS50(void);

#endif /*SWING_CARD_H */

将其保存至 Core/Inc 文件夹下。

新建一个 swing_card.c 文件,输入代码如下:

#include "swing_card.h"
#include "logger.h"
#include "st_errno.h"
#include "rfal_nfca.h"
#include "led.h"
#include "mcc.h"
#include "mf1.h"
#include "rfal_chip.h"
#include "utils.h"

rfalNfcaSensRes      sensRes;
rfalNfcaSelRes       selRes;
rfalNfcaListenDevice nfcaDevList[10];
uint8_t              devCnt;
uint8_t              devIt;

/**
  * @brief  让RFID芯片进入唤醒模式
  * @param  offset:Amplitude基准值,可调用rfalChipMeasureAmplitude方法获取
  * @retval 错误码
  */
void setSt25R3911WakeupMode(uint8_t offset)
{
  rfalChipWriteReg(0x33,&offset,1); //设置振幅基准值
  uint8_t regVal = 0xA8;
  rfalChipWriteReg(0x32,&regVal,1); //设置测量时产生中断的差值10
  regVal = 0x24;
  rfalChipWriteReg(0x31,&regVal,1); //设置检测周期:300ms
  regVal = 0x04;
  rfalChipWriteReg(0x02,&regVal,1); //使能唤醒模式
  regVal = 0xFB;
  rfalChipWriteReg(0x16,&regVal,1); //除由幅度测量引起中断外,屏敝其它所有中断
}

/**
  * @brief  获取当前Amplitude(振幅)值,请在读卡感应场上方不存在卡片的情况下调用,否则获取的值不准确
  * @retval Amplitude值
  */
uint8_t GetSt25R3911Amplitude()
{
  uint8_t offset;
  rfalChipMeasureAmplitude(&offset);
  return offset;
}

void closeField()
{
  rfalNfcaPollerSleep();
  ledOff(LED_A);
  rfalFieldOff();
  ledOff(LED_Field);
  mccDeinitialise(true);
}

/**
  * @brief  减去指定值
  * @param  value:减去的值
  * @retval 错误码
  */
ReturnCode swingCard(int32_t value)
{
  ReturnCode           err;

  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;  

  if(searchMifareS50())
  {
    logUsart("find Mifare S50 card:%s\r\n", hex2Str(nfcaDevList[devIt].nfcId1, 4));
  }
  else
  {
    return ERR_NOTFOUND;
  }
  
  mccInitialize();
  err = mccAuthenticate(key,sector*4,nfcaDevList[devIt].nfcId1,4,mifareKey,nonce);
  if(err != ERR_NONE)
  {
    closeField();
    return err;
  }
  
  int32_t remain;
  err = mccReadValue(block, &remain); //读取卡内剩余的钱
  if(err != ERR_NONE)
  {
    closeField();
    return err;
  }
  logUsart("card remain = %1.2f\r\n", (double)remain/100);
  if(remain < value) //当剩余的钱不够扣除时,返回错误
  {
    closeField();
    return ERR_REMAIN_NO_ENOUGH;
  }
  //扣钱
  err = mccDecrement(block,value);
  if(err != ERR_NONE)
  {
    closeField();
    return err;
  }
  
  mccReadValue(block,&remain);
  logUsart("decrement %1.2f, remain %1.2f\r\n",(double)value/100, (double)remain/100);
  closeField();
  return ERR_NONE;
}

/**
  * @brief  寻找 Mifare S50 卡
  * @retval 找到卡片则返回true,否则返回false
  */
bool searchMifareS50()
{
  ReturnCode           err;

  rfalFieldOff();//关闭感应场
  err = rfalNfcaPollerInitialize(); //轮询初始化
  err = rfalFieldOnAndStartGT(); //打开感应场
  ledOn(LED_Field);
  //检测NFCA标签
  err = rfalNfcaPollerTechnologyDetection( RFAL_COMPLIANCE_MODE_NFC, &sensRes ); 
  if(err != ERR_NONE)
  {
    closeField();
    return false;
  }
  //如果存在NFCA标签,则执行防冲撞
  err = rfalNfcaPollerFullCollisionResolution( RFAL_COMPLIANCE_MODE_NFC, 10, nfcaDevList, &devCnt );
  if((err != ERR_NONE) || (devCnt == 0))
  {
    closeField();
    return false;
  }
  ledOn(LED_A);
  devIt = 0;
  if(nfcaDevList[devIt].isSleep)
  {
    err = rfalNfcaPollerCheckPresence( RFAL_14443A_SHORTFRAME_CMD_WUPA, &sensRes );
    if(err != ERR_NONE)
    {
      closeField();
      return false;
    }
    err = rfalNfcaPollerSelect( nfcaDevList[devIt].nfcId1, nfcaDevList[devIt].nfcId1Len, &selRes ); 
    if(err != ERR_NONE)
    {
      closeField();
      return false;
    }
  }
  //如果防冲撞操作中读取到了Mifare S50标签
  if(nfcaDevList[devIt].type != RFAL_NFCA_T2T)
  {
    closeField();
    return false;
  }
  if (!(nfcaDevList[devIt].selRes.sak & 0x08))
  {
    closeField();
    return false;
  }
  return true;
}

将其保存至 Core/Src 文件夹下,并引入到【Applicatin/User/Core】组中。

setSt25R3911WakeupMode方法的功能是让 ST253911B 芯片进入唤醒模式,RFAL 开发包并没有现成方法实现这个功能,只能自行操作寄存器实现,好在 RFAL 有现成的操作寄存器的方法。offset参数是振幅的基准值,当有卡片进入读卡器感应场时,振幅值会减少,当减少的值超过一定范围时就会产生一个中断。此差值的设置范围为 0~15,如果此值设置得太小,则有可能在没有卡片进入时频繁产生中断,设置太大,则卡片需要贴得很近才能产生中断。我将此值设置为 10。其实设置为 15 也没有问题,因为我测试过,感应场没有卡片时,振幅值为 151,放入卡片,振幅值为 90 左右,差值远远超过 15。

寻卡功能和扣钱功能分开编写,主要原因是需要检测卡片是否离开,否则的话,卡片长时间放在读卡器上,会不停地扣钱,这显然是不被允许的。原本是想不断检测振幅值来判断卡片是否离开,但这样会产生一些怪异的振幅数据,并不准确,最后只能使用寻卡方法来实现了。

处理中断

前面说过,在唤醒模式下,当有卡片靠近感应场时,会产生一个中断,此时,需要主控作出响应。此处的处理方式和之前写的充值程序一样,当主控的中断服务程序收到中断时,仅去设置一个标志,表示收到了这个中断,而真正的处理程序则放在 main.c 的while(1)循环中进行。

打开 stm32l4xx_it.c 文件,找到/* USER CODE BEGIN EV */,添加如下代码:

/* USER CODE BEGIN EV */
extern uint8_t wakeupMode;
/* USER CODE END EV */

找到EXTI0_IRQHandler方法,更改代码如下:

void EXTI0_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI0_IRQn 0 */

  /* USER CODE END EXTI0_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
  /* USER CODE BEGIN EXTI0_IRQn 1 */
  ledToggle(LED_AP2P);
  if(wakeupMode)
  {
    wakeupMode = false;
  }
  st25r3911Isr();
  /* USER CODE END EXTI0_IRQn 1 */
}

主函数

最后是 main.c 的代码。首先确保包含以下头文件:

/* 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"
#include "rfal_chip.h"
#include "swing_card.h"
/* USER CODE END Includes */

然后找到/* USER CODE BEGIN PV */,更改代码如下:

/* USER CODE BEGIN PV */
uint8_t globalCommProtectCnt = 0;
bool wakeupMode = false;
/* USER CODE END PV */

最后更改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..\r\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..\r\n");
    }
  }
  
  rfalInitialize();
  HAL_Delay(200);
  beepOff();
  
  uint8_t amOffset = 151; //振幅基准值
  int32_t incrementValue = 200; //每次刷卡减去的钱的数目(以分为单位)
  setSt25R3911WakeupMode(amOffset); //进入唤醒模式
  wakeupMode = true;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_Delay(100);
    if(!wakeupMode)
    {
      rfalInitialize();
      ReturnCode err;
      err = swingCard(incrementValue); //扣钱
      if(err == ERR_NONE)
      {
        logUsart("swing card success\r\n");
        beep();
      }
      else
      {
        logUsart("swing card failded:%d\r\n",err);
      }
      while(searchMifareS50())
      {
        HAL_Delay(100);
      }

      setSt25R3911WakeupMode(amOffset); //重新进入唤醒模式
      wakeupMode = true;
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

需要注意的是setSt25R3911WakeupMode方法中,振幅基准值直接设定为 151,这个值必须是在生产环境中,在感应场内没有卡的情况下测出来的。原本是想在每次进入唤醒模式时测量并写入寄存器的,但总是出问题,只有直接写入固定值才能正常执行。没有时间去深究其中原因,那就写入固定值吧。不同的环境、不同的电路这个值肯定是不一样的,真实项目中可以添加一个按钮来测量此值并记录,也可通过上位机发送命令来测量并保存这个值。

运行效果

烧写上篇文章《实现 Mifare S50 充值功能》的程序,然后在卡内充入若干金额。接下来烧写本文程序,连接串口,打开串口助手连接读卡器。刷卡,效果如下图所示:

图 1:运行结果
;

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