射频识别(RFID)技术

基础程序配置

作者:陈广 日期:2020-10-10


读卡器硬件已经做好,接下来就要写固件了。由于完全兼容 ST25R3911B-DISCO 开发板,所以可以直接下载此开发板的固件,改一下就可以直接使用了,不但 USB 驱动已经配置好了,还有完整的上位机通信的协议,可以直接使用它来操作读卡器。下载地址:

https://www.st.com/content/st_com/en/products/embedded-software/st25-nfc-rfid-software/stsw-st25r002.html

遗憾的是 STM 公司并没有提供此协议的完整文档,只是在固件源码中粗略地写了部分内容。要完全掌握此协议的使用,需要花大量时间读上位机和下位机源码。考虑到在实际项目中,大部分情况下只使用一种类型的标签,完全没必要使用这种大而全的协议。为灵活而高效地与上位机通信,自己配置并编写上位机程序是最佳选择。本文将所有基础配置写好,之后所有文章所以本文为起点进行开发。

STM32CubeMx 配置

打开 STM32CubeMx,单击【ACCESS TO MCU SELECTOR】按钮,找到【STM32L476RETx】MCU,双击生成一个新的项目。

配置晶振

点开左边【System Core】下的【RCC】栏,由于只焊接了 32.768KHz 低频晶振,高频晶振电路已经绘制,但未焊接,所以这里只配置【Low Speed Clock(LSE)】为【Crystal/Ceramic Resonator】。

图 1:配置晶振

配置 ST-Link 烧写口

我画的所有 STM32 开发板都只画 ST-Link 烧写口,因为 ST-Link 非常便宜,只需十来块钱便可在淘宝购买,而且只需接出 3 个引脚即可。点开左边【System Core】下的【SYS】栏,在【Debug】项中选择【Serial Wire】。

图 2:配置烧写口

配置时钟

在屏幕上方选中【Clock Configuration】选项卡,按下图所示进行配置:

图 3:配置时钟

实际上,配置 USB 以及 UART 后,这些图有些地方会点亮。为方便后面的配置,先配时钟会更好些。

配置 SPI

主控和 ST25R3911B 芯片之间使用 SPI 进行通信。点开左边【Connectivity】下的【SPI1】栏,在【Mode】项的下拉框中选择【Full-Duplex Master】,并按下图所示在【Configuration】窗口中进行配置:

图 4:配置SPI

配置 UART

点开左边【Connectivity】下的【USART1】栏,在【Mode】项的下拉框中选择【Asynchronous】,最终效果如下图所示。

图 5:配置UART

配置 LPUART

接下来配置低功耗串口 LPUART,点开左边【Connectivity】下的【SPI1】栏,在【Mode】项的下拉框中选择【Asynchronous】,设置波特率为 115200 Bits/s,效果如下图所示:

图 6:配置LPUART

配置 HID

STM32L476RET6 这块芯片比较高级,支持手机 OTG,不过这里我们只使用普通 USB 即可,首先点开左边【Connectivity】下的【USB_OTG_FS】栏,在【Mode】项的下拉框中选择【Device_Only】,注意下方【Speed】栏中的速度应为【Full Speed 12MBit/s】。

接下来点开左边【Middleware】栏下的【USB_DEVICE】项,在【Class For FS IP】项的下拉列框中选择【Custom Human Interface Device Class(HID)】项,效果如下图所示:

图 7:配置HID

选中下方【Device Descriptor】选项卡,可以自己的需求更改 USB 的一些参数。如产品标识、厂商标识,VID 和 PID 等。如下图所示:

图 8:配置HID参数

可以选择降低 USB 中断的优先级。点开左边【System Core】下的【NVIC】栏,在右边中断列表中找到【USB OTG FS global interrupt】,将其【Preemption Priority】列的值更改为 5。

图 9:配置USB中断

配置 GPIO

按下图所示对红框标注的引脚进行 GPIO 的配置:

图 10:配置GPIO

其中,左键在弹出菜单中选择图中蓝字内容,以确定引脚功能。右键在弹出菜单中选择【Enter User Label】项,将引脚标签更改为黑字所示内容。需要注意的是,标签内容一定要与图示中的一样,如果不一样,需要在后面操作中自行更改 platform.h 文件里的相应内容。

接下来打开 IRQ_3911 引脚的中断,点开左边【System Core】下的【GPIO】栏,在右边子栏中点开【NVIC】。给【EXTI line0 interrupt】项的【Enabled】列打上勾,如下图所示:

图 10:配置 IRQ_3911 中断

最后重新点开时钟配置窗口【Clock Configuration】,如果弹出提示框说帮你自动配置,点【Yes】即可。

生成代码

点击屏幕上方【Project Manager】选项卡,左侧选中【Project】选项卡,在【Toolchain/IDE】栏中选择【MDK-ARM】项,即使用 Keil5 作为开发工具,如下图所示:

图 11:项目管理

单击屏幕右上角【GENERATE CODE】按钮,生成代码。

引入 RFAL 库

STM 公司专门为 ST25R3911B 开发了 RF/NFC abstraction layer (RFAL库),将所有 ST25R3911B 的功能包装成函数,方便调用。此库最新版可在以下网址下载:

https://www.st.com/content/st_com/en/products/embedded-software/st25-nfc-rfid-software/stsw-st25rfal001.html

需要注意的是,最新版依照 ST25R3916 进行配置。为保险起见,我使用的是 ST25R3911B-DISCO 开发板固件中所包含的 RFAL。下载地址:

https://www.st.com/content/st_com/en/products/embedded-software/st25-nfc-rfid-software/stsw-st25r002.html

此开发包中包含有完整的 RFAL 库,包括帮助文档,它还包含有我们需要使用的到一些其它库。我将所有需要使用到的文件打包成一个文件夹【ReaderLib】,大家可在以下网址下载:

http://www.iotxfd.cn/down/RFID/ReaderLib.zip

拷贝库文件

以下内容中所指的路径【\ST25R_Reader】为 STM32CubeMx 生成代码所在文件夹。

首先下载上述链接中的压缩包,解压后,将【ReaderLib\rfal】文件夹拷贝至 【\ST25R_Reader\Middlewares\ST】子文件夹中。此为完整的 RFAL 库。

接下来,将【ReaderLib\mifare】文件夹拷贝至【\ST25R_Reader\Middlewares】文件夹中。虽然 RFAL 库大而全,但偏偏漏了 Mifare S50,无法访问 M1 卡,我在网上找到此卡的 ST25R3911B 驱动,不过不完整,后面还要自己完善功能。

最后,将【ReaderLib\Core\Src】文件夹中的文件拷贝至【\ST25R_Reader\Core\Src】中,并将【ReaderLib\Core\Inc】文件夹中的文件拷贝至【\ST25R_Reader\Core\Inc】中。

文件是拷贝完了,但革命还未成功,代码还有很多地方需要修改。

引入库文件

双击【\ST25R3911B\Reader\MDK-ARM】文件夹下的【Reader.uvprojx】文件,使用 Keil 打开项目。将上面拷贝的文件依次引入到项目之中。

  1. 右键单击【Application/User/Core】项,在弹出菜单中选中【Add Existing File to Group...】,选中【\Core\Src】文件夹下刚才拷贝的 .c 文件,单击【确定】按钮引入。如下图所示:

图 12:引入文件
  1. 右键单击【ST25R_Reader】项,在弹出菜单中选中【Add Group...】,添加一个新的组,并命名为【RFAL】。接下来在 RFAL 组中引入【rfal\Src】 文件夹中的 .c 文件,再引入【rfal\Src\st25r3911】文件夹中的 rfal_rfst25r3911.c 文件 效果如下图所示:

图 13:引入RFAL库
  1. 添加一个名为【ST25R3911B】的组,并向其中添加【rfal\Src\st25r3911】文件夹中的部分文件,如下图所示:

图 14:引入 ST25R3911B 库
  1. 添加一个名为【MifarerClassic】的组,并向其中添加【Middlewares\mifare】中的所有.c后缀名的文件,如下图所示:

图 14:引入 ST25R3911B 库
  1. 单击 keil 工具栏上的【Options for Target】(魔术棒图标)按钮,打开 Options for Target 窗口,打开【C/C++】选项卡,点击【Include Paths】最右边的按钮,添加以下几个 Include 路径:
..\Middlewares\mifare
..\Middlewares\ST\rfal\Inc
..\Middlewares\ST\rfal\Src
..\Middlewares\ST\rfal\Src\st25r3911

如下图所示:

图 15:添加 Include 路径
  1. 接下来,需要在 main.c 文件中添加几句代码,程序方能编辑成功。打开 main.c 文件,找到/* USER CODE BEGIN Includes *//* USER CODE END Includes */,在期间加入如下代码:
#include "platform.h"
  1. 找到/* USER CODE BEGIN PV */,在其后添加如下代码:
uint8_t globalCommProtectCnt = 0;

此变量在 platform.h 文件中使用。

  1. main()函数中找到代码MX_LPUART1_UART_Init();,将其注释掉。由于基本不会使用 LPUART,所以将这句代码注释,如果需要使用,则不注释即可。

  2. 打开main.h文件,找到/* USER CODE BEGIN Private defines */,在其后添加如下代码:

/* USER CODE BEGIN Private defines */
#define USE_LOGGER LOGGER_ON
#define LED_FIELD_Pin GPIO_PIN_8 
#define LED_FIELD_GPIO_Port GPIOA
/* USER CODE END Private defines */

接下来编译代码,如果成功,则继续下面的工作。

配置 HID

我们在《网络编程》系列文章中讲解过如何配置 HID,这里不同之处只是更换不同的 HID 报告描述符。这里直接拷贝 ST25R3911B-DISCO 开发板固件中的报告描述符,每次发送 64 个字节。

  1. 打开【usbd_custom_hid_if.c】文件,找到CUSTOM_HID_ReportDesc_FS声明,更改如下:
/** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 0 */
  0x06, 0x00, 0xFF,                   /* USAGE_PAGE (Vendor Defined Page 1) */
  0x09, 0x01,                         /* USAGE (Vendor Usage 1) */

  0xa1, 0x01,                         /* COLLECTION (Application) */
  0x19, 0x01,                         /*   USAGE_MINIMUM (0) */
  0x29, 0x40,                         /*   USAGE_MAXIMUM (64) */
  0x15, 0x00,                         /*   LOGICAL_MINIMUM (0) */
  0x26, 0xFF, 0x00,                   /*   LOGICAL_MAXIMUM (255) */
  0x75, 0x08,                         /*   REPORT_SIZE (8) */
  0x95, 0x40, 						            /*   REPORT_COUNT (64) */
  0x81, 0x00,                         /* */
  0x19, 0x01,                         /*   USAGE MINIMUM */
  0x29, 0x40,						              /*   USAGE_MAXIMUM */
  0x91, 0x00,                         /*   OUTPUT (Data,Var,Abs) (note: output from host)  */
  /* USER CODE END 0 */
  0xC0    /*     END_COLLECTION	             */
};
  1. 打开 usbd_conf.h 文件,找到USBD_CUSTOM_HID_REPORT_DESC_SIZE宏定义,将其值改为29U。也可以右键导航找到此宏定义。此宏靠上两个位置的宏为USBD_CUSTOMHID_OUTREPORT_BUF_SIZE,将其改为64U(根据报告描述符中的数据长度决定),其实这两个参数可以在 STM32CubeMX 中修改。

  2. 在资源管理器中找到 usbd_customhid.h 文件,将其只读属性去掉,然后在 Keil 中打开它,找到CUSTOM_HID_EPIN_SIZECUSTOM_HID_EPOUT_SIZE两个宏定义,将它们的值更改为0x40U

  3. 在【usbd_custom_hid_if.c】文件中找到USBD_CUSTOM_HID_SendReport_FS函数的定义,此时处于注释状态,将其释放出来。并将最前面的static关键字删除。

使用此函数,还需要头文件中声明。打开 usbd_custom_hid_if.h 文件,找到/* USER CODE BEGIN EXPORTED_FUNCTIONS */,在其后添加如下代码:

int8_t USBD_CUSTOM_HID_SendReport_FS(uint8_t *report, uint16_t len);
  1. 打开 usb_device.h 文件,在/* USER CODE BEGIN PV */下面添加一句代码:
extern USBD_HandleTypeDef hUsbDeviceFS;

配置中断

主控配置了一个外部中断引脚,连接至 ST25R3911B 的 IRQ 引脚,要使用些中断,必须更改中断服务函数。

  1. 打开 stm32l4xx_it.c 文件,在/* USER CODE BEGIN Includes */后面引入以下两个头文件:
#include "st25R3911_interrupt.h"
#include "led.h"
  1. 更改SysTick_Handler函数如下:
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
	HAL_SYSTICK_IRQHandler();
  /* USER CODE BEGIN SysTick_IRQn 1 */
	ledFeedbackHandler();
  /* USER CODE END SysTick_IRQn 1 */
}
  1. 更改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 */
	st25r3911Isr();
  /* USER CODE END EXTI0_IRQn 1 */
}

测试 UART 通信

配置已经完成,现在需要测试 UART 通信和 HID 通信是否可以正常执行。

  1. 首先使用以下代码引入logger.h头文件,这里有直接使用 UART 口向上位机打印字符串的函数。
#include "logger.h"
  1. 接下来找到/* USER CODE BEGIN 2 */,在其后添加如下代码以初始化 UART1 口向上位机打印字符串的功能:
logUsartInit(&huart1);
  1. 要使用logUsart,还需声明一个宏,打开 main.h 文件,找到/* USER CODE BEGIN Private defines */,在其后添加如下宏:
#define USE_LOGGER LOGGER_ON
#define LED_FIELD_Pin GPIO_PIN_8 
#define LED_FIELD_GPIO_Port GPIOA

后两个宏定义后面要用到,这里顺便加上去了。

  1. 找到while(1)上方的/* USER CODE BEGIN WHILE */,更改while(1)代码如下:
/* USER CODE BEGIN WHILE */
	uint8_t i = 5;
  while (1)
  {
    /* USER CODE END WHILE */
		HAL_Delay(2000);
		logUsart("Hello %d\r\n",i++);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
  1. 将开发板的 UART 口通过 TYPE-C 口连接至上位机,编译程序,按重启按钮,然后打开串口助手查看上位机是否可以接收到单片机 UART 发送的字符串。

测试 HID 通信

之前在讲 HID 的时候,我们知道,接收数据是在 usbd_custom_hid_if.c 文件里的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;
  //将数据原封不动地返回
  USBD_CUSTOM_HID_SendReport_FS((hhid->Report_buf), 64);
	
  /* Start next USB packet transfer once data processing is completed */
  USBD_CUSTOM_HID_ReceivePacket(&hUsbDeviceFS);

  return (USBD_OK);
  /* USER CODE END 6 */
}

将 TYPE-C 口接至单片机的 USB 口,连接至上位机,烧写程序,打开单片机多功能调试助手。查找并打开 NFCReader 设备,一定要勾选【Hex发送】和【Hex显示】两项(打开 NFCReader 设备后发送区会自动填入00~63这64个数字)。单击【端点2/HID发送】按钮,查看接收编辑框信息。

图 16:运行程序

由结果可知,发送的数据原封不动地返回给上位机。

读卡测试

接下来就是测试读卡功能了,虽然是全功能读卡器,但为简便起见,这里只读 NFCA 卡,即也可以读最常见的 Mifare S50 卡。我也忘了哪些代码是后面加上去的了,相关代码全部写上来吧。首先是头文件:

/* 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"
/* USER CODE END Includes */

接下来是成员变量和函数声明:

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef hlpuart1;
UART_HandleTypeDef huart1;

SPI_HandleTypeDef hspi1;

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

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_LPUART1_UART_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_SPI1_Init(void);

最后是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);
  }   
  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;
	
	logUsart("Start Find...\r\n");
	rfalInitialize();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		rfalFieldOff();//关闭感应场
		platformDelay(1000);
		rfalNfcaPollerInitialize(); //轮询初始化
		rfalFieldOnAndStartGT(); //打开感应场
		//检测NFCA标签
		err = rfalNfcaPollerTechnologyDetection( RFAL_COMPLIANCE_MODE_NFC, &sensRes ); 
		if(err == ERR_NONE)
		{ //如果存在NFCA标签,则执行防冲撞
			err = rfalNfcaPollerFullCollisionResolution( RFAL_COMPLIANCE_MODE_NFC, 10, nfcaDevList, &devCnt );
			if((err == ERR_NONE) && (devCnt > 0))
			{
				ledOn(LED_A);
				logUsart("NFC-A device(s) found %d\r\n", devCnt );
				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;
					}
				}
				//如果防冲撞操作中读取到了标签
				switch(nfcaDevList[devIt].type)
				{
					case RFAL_NFCA_T1T:
						logUsart("NFC-A T1T device found \r\n");
						break;
					case RFAL_NFCA_T2T:
						logUsart("NFC-A T2T device found \r\n");
						break;
					case RFAL_NFCA_T4T:
						logUsart("NFC-A T4T (ISO-DEP) device found \r\n");
						break;
					case RFAL_NFCA_T4T_NFCDEP:
					case RFAL_NFCA_NFCDEP:
						logUsart("NFC-A P2P (NFC-DEP) device found \r\n");
						break;
				}
				rfalNfcaPollerSleep();
			}
			
		}
		else
		{
			ledOff(LED_A);
		}
    /* USER CODE END WHILE */

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

接上 UART 接口,烧写程序,将一或多张 Mifare S50 卡放入感应场,结果如下图所示:

图 17:运行程序

过程有些复杂,我也有可能漏写了某些地方,所以将最终源码奉上。将来所有文章都会以此源码为起点进一步编写。

http://www.iotxfd.cn/down/RFID/ST25R_Reader_Templet.zip

;

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