作者:陈广 日期: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,®Val,1); //设置测量时产生中断的差值10
regVal = 0x24;
rfalChipWriteReg(0x31,®Val,1); //设置检测周期:300ms
regVal = 0x04;
rfalChipWriteReg(0x02,®Val,1); //使能唤醒模式
regVal = 0xFB;
rfalChipWriteReg(0x16,®Val,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 充值功能》的程序,然后在卡内充入若干金额。接下来烧写本文程序,连接串口,打开串口助手连接读卡器。刷卡,效果如下图所示: