射频识别(RFID)技术

ISO 15693 标签读写操作

作者:陈广 日期:2020-12-3


14443 读完后,就轮到15693了,RFAL 开发包并不是可以很方便直接使用的开发包,访问特定标签,需在其基础上再包装一层,之前的 Mifare Classic 就是需要自己做这个包装的。在 ST25R3911B-DISCO 开发板固件中,完整包装了 15693 各种操作的方法,我们直接拷贝过来就可以使用了,省了很多事。

这次直接使用《实现 Mifare S50 充值功能》这篇文章作为基础,继续编写代码。首先下载以下文件:

iso15693_3.zip

解压后,将 iso15693_3.h 文件拷贝至【\Core\Inc】文件夹下,并将 iso15693_3.c 文件拷贝至【iso15693_3.c】文件夹下。使用 keil 打开之前配置的模板,或写好的 Mifare 程序,在【Application/User/Core】组中包含 iso15693_3.c。这两个文件在 ST25R3911B-DISCO 开发板固件中也可以找到。

协议设计

这类程序的编写肯定是协议先行,本次将要在 I-CODE SLI 标签中写入二进制数据,由于 I-CODE SLI 标签的用户区容量为 112 字节,而我们之前编写 HID 描述符规定一次传输 64 字节,所以协议设计上会有些麻烦,要考虑到可能一次操作需要 2 次传输方能完成。本协议为半双工协议,由上位机主动发起指令。

读卡器发往上位机

  1. 0x01(1字节) + 寻找到的卡数量(1字节)+ UID(8字节*卡数量)
    收到上位机寻卡请求,成功操作后,返回所有卡的UID。为降低协议复杂度,限定寻找到的卡数量最多为 7 张,这样数据包长度就不会超过 64 个字节。

  2. 0x02(1字节)+ 数据长度(1字节,小于或等于111) + 数据(n字节,n<=62)
    收到上位机读数据请求,成功操作后,返回数据。由于 I-CODE SLI 标签存储容量为 112 字节,最大可能会分两个数据包返回数据。后一个数据包格式如下: 0x03(1字节)+ 数据(n字节,n<=49) 数据从0号块开始存储,0号块的第0字节用于存放数据长度(0号块0字节不计算在内)。

  3. 0x03(1字节) 操作成功,如成功向卡片写入数据,则返回此数据

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

上位机发往读卡器

  1. 0x01(1字节)
    寻卡命令

  2. 0x02(1字节) + UID(8字节) 向指定 UID 的标签读取数据,数据从0块开始读取。

  3. 0x03(1字节) + UID(8字节) + 数据长度(1字节,小于或等于128)+ 数据(n字节,n<=54)
    向指定 UID 的标签写入数据,由于 I-CODE SLI 标签用户可用存储容量为 112 字节,第一个字节用于存储数据长度,所以最大存储长度为 111 字节。最大可能会分两个数据包发送数据。后一个数据包格式如下: 0x04(1字节)+ 数据(n字节,n<=57)

读写器固件代码设计

打开 main.h 文件,找到/* USER CODE BEGIN ET */,修改枚举所表示的状态名称如下:

typedef enum {
    STATE_IDLE                  = 0, //空闲
    STATE_FIND_CARD             = 1, //寻卡
    STATE_READ_CARD             = 2, //读卡
    STATE_WRITE_CARD            = 3  //写卡
} operState;

接下来打开 usbd_custom_hid_if.c 文件,添加接收 HID 数据后的处理代码。

首先确保此文件包含以下头文件:

#include "usbd_custom_hid_if.h"
#include "logger.h"

找到/* USER CODE BEGIN EXPORTED_VARIABLES */,更改其下方代码添加访问 main.c 里的变量:

extern operState work_state;
extern uint8_t UID[8];
extern uint8_t writeBuff[129];

找到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) //读数据
    {                 
      copyArray(buf, 1, UID, 0, 8);
      work_state = STATE_READ_CARD;
    }
    else if(buf[0] == 0x03) //写数据
    {
      copyArray(buf, 1, UID, 0, 8); //拷贝UID
      int writeLen = buf[9];
      if(writeLen <= 112)
      {
        writeBuff[0] = writeLen; //拷贝数据长度
        if(writeLen > 0)
        {
          if(writeLen <= 54)
          {          
            copyArray(buf, 10, writeBuff, 1, writeLen);
            work_state = STATE_WRITE_CARD;
          }
          else
          {
            copyArray(buf, 10, writeBuff, 1, 54);
          }
        }
      }
    }
    else if(buf[0] == 0x04) //超过54字节的后续数据
    {
      uint8_t len = writeBuff[0] - 54;
      copyArray(buf, 1, writeBuff, 55, len);
      work_state = STATE_WRITE_CARD;
    }
  }
  
  /* Start next USB packet transfer once data processing is completed */
  USBD_CUSTOM_HID_ReceivePacket(&hUsbDeviceFS);

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

和之前一样,在接收到上位机数据包后,仅设置状态及保存数据,不做任何其它处理,访问标签的操作全部放到main方法中

处理上位机请求并返回数据

接下来编写 I-CODE SLI 卡的操作程序并向上位机返回数据。

打开 main.c 文件,首先确保包含以下头文件:

#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 "iso15693_3.h"
#include "led.h"

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

/* USER CODE BEGIN PV */
uint8_t globalCommProtectCnt = 0;
operState work_state = STATE_IDLE; //保存状态的变量
uint8_t UID[8]; //当前卡片的UID
uint8_t writeBuff[112]; //写数据缓冲,第一个字节表明数据长度
uint8_t buf[64] = {0}; //发送缓冲
/* USER CODE END PV */

更改main方法上方的错误处理函数:

/* USER CODE BEGIN 0 */
void SendError(uint8_t errCode)
{
  buf[0] = 0xFF;
  buf[1] = errCode;
  USBD_CUSTOM_HID_SendReport_FS(buf, 64);
  work_state = STATE_IDLE;
  ledOff(LED_V);
  rfalFieldOff();
  ledOff(LED_Field);
  logUsart("error happen, error code:%d\r\n", errCode);
  iso15693Deinitialize(false);
}
/* USER CODE END 0 */

最后更改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);
    beep();
  }   
  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;
  iso15693ProximityCard_t cards[20];
  uint8_t cardsFound;
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {    
    /* USER CODE END WHILE */
    if(work_state == STATE_IDLE) continue;
    if(work_state == STATE_FIND_CARD)//寻卡
    {      
      err = iso15693Initialize(false,false);
      if(err != ERR_NONE)
      {
        SendError(err);
        continue;
      }
      ledOn(LED_Field);
      err = iso15693Inventory(ISO15693_NUM_SLOTS_1,0,NULL,cards,
          sizeof(cards)/sizeof(iso15693ProximityCard_t),&cardsFound);
      if(err != ERR_NONE)
      {
        SendError(err);
        continue;
      }
      if(cardsFound > 0)
      {
        ledOn(LED_V);
        buf[0] = 0x01;
        //最多只取其中7张卡片的UID传回
        int count = (cardsFound > 7) ? 7 : cardsFound;
        buf[1] = count;
        for(int i=0; i<count; i++)
        {
          copyArray(cards[i].uid, 0, buf, i*8+2, 8);
        }
        USBD_CUSTOM_HID_SendReport_FS(buf, 64);
        beep();
      }
      work_state = STATE_IDLE;
    }
    else if(work_state == STATE_READ_CARD) //读卡
    {
      logUsart("start read...\r\n");
      //首先找到卡片
      iso15693PiccMemoryBlock_t memBlock;
      int index = -1;
      for(int i=0; i<cardsFound; i++)
      {
        if(compareUID(cards[i].uid, 0, UID, 0))
        {
          index = i;
          break;
        }
      }
      if(index == -1) //指定UID的卡片不在感应场中
      {
        SendError(ERR_NOTFOUND);
        continue;
      }
      //首先读取0块并取出第一个字节中的长度
      memBlock.actualSize = 4;
      memBlock.blocknr = 0;
      err = iso15693ReadSingleBlock(cards+index, &memBlock);
      if(err != ERR_NONE)
      {
        SendError(err);
        continue;
      }
      //然后根据长度将所有数据读出
      uint8_t len = memBlock.data[0];
      uint8_t numBlocks = len/4+((len%4==0)?0:1);
      uint8_t readBuff[112];
      uint16_t actLen;
      //多块读取最多只能读17个数据块,超过会报CRC错误
      uint8_t count = numBlocks <= 17 ? numBlocks : 17;
      err = iso15693ReadMultipleBlocks(cards+index,0,count,0,readBuff,112,&actLen);
      if(err != ERR_NONE)
      {
        SendError(err);
        continue;
      }
      //如果超过17个块,则读取后面的块
      if(numBlocks > 17)
      {
        count = numBlocks - 17;
        uint8_t temp[40];
        err = iso15693ReadMultipleBlocks(cards+index,17,count,0,temp,40,&actLen);
        if(err != ERR_NONE)
        {
          SendError(err);
          continue;
        }
        copyArray(temp,0,readBuff,68,len-68);
      }
      //将读出的数据发送至上位机
      buf[0]=0x02;
      buf[1]=len;
      if(len <= 62)
      {
        copyArray(readBuff,1,buf,2,len);
        USBD_CUSTOM_HID_SendReport_FS(buf, 64);
      }
      else
      { //分段写入
        copyArray(readBuff,1,buf,2,62);
        USBD_CUSTOM_HID_SendReport_FS(buf, 64);
        HAL_Delay(10);
        buf[0]=0x03;
        copyArray(readBuff,63,buf,1,len-62);
        USBD_CUSTOM_HID_SendReport_FS(buf, 64);
      }
      beep();
      work_state = STATE_IDLE;
    }
    else if(work_state == STATE_WRITE_CARD) //写卡
    {
      logUsart("start write...\r\n");
      int index = -1;
      //从之前寻卡结果中找到将要写入数据的卡片
      for(int i=0; i<cardsFound; i++)
      {
        if(compareUID(cards[i].uid, 0, UID, 0))
        {
          index = i;
          break;
        }
      }
      if(index == -1) //指定UID的卡片不在感应场中
      {
        SendError(ERR_NOTFOUND);
        continue;
      }
      //分块写入数据,共分为 27 个块,每个块 32bit(4个字节)
      int count = writeBuff[0] + 1;
      int writeIndex = 0;
      iso15693PiccMemoryBlock_t memBlock;
      memBlock.actualSize = 4;
      bool success = true;
      while(writeIndex < count)
      { //分块写入
        for(int i=0;i<4;i++)
        {
          memBlock.blocknr = writeIndex / 4;
          memBlock.data[i] = writeBuff[writeIndex++];
        }
        err = iso15693WriteSingleBlock(cards+index, 0, &memBlock);
        if(err != ERR_NONE)
        {
          SendError(err);
          success=false;
          break;
        }
      }
      if(success)
      { //返回写入成功信息
        buf[0]=0x04;
        USBD_CUSTOM_HID_SendReport_FS(buf, 64);
        beep();
      }
      work_state = STATE_IDLE;
    }
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

需要注意,这次设计的写卡只支持写一个数据,即下一次写入的数据会覆盖上一次的数据。数据的写入永远是从 0 号块开始,第一个字节表示数据长度。

上位机程序设计

本以为前面有 14443 的模板,写这个程序应当是相当轻松的了,实践证明我还是想得简单了。编写这种需要配合的上下位机还是挺困难的,大部分时间花在调试上了。

界面设计

新建一个 WPF 应用程序,界面代码 MainWindow.xaml 如下:

<Window x:Class="RW15693.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:RW15693"
        mc:Ignorable="d"
        Title="15693读写程序" Height="265.474" Width="382.205" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition/>
            <RowDefinition Height="32"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid Grid.Row="2" Grid.ColumnSpan="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button x:Name="btnRead" IsEnabled="False" Content="读卡" Margin="30,0,30,8" Grid.Column="0" Click="btnRead_Click"/>
            <Button x:Name="btnWrite" IsEnabled="False" Content="写卡" Margin="30,0,30,8" Grid.Column="1" Click="btnWrite_Click"/>
        </Grid>
        <Button x:Name="btnFindCard" Grid.Row="0" Grid.Column="0" Content="寻卡" Margin="8" Click="btnFindCard_Click"/>
        <ComboBox x:Name="cbUID" Grid.Row="0" Grid.Column="1" Margin="8"/>
        <TextBox x:Name="txtContent" Grid.Row="1" Grid.ColumnSpan="2" TextWrapping="Wrap" Margin="8"/>
        <StatusBar Grid.Row="3" Grid.ColumnSpan="2">
            <TextBlock x:Name="tbMsg"/>
        </StatusBar>
    </Grid>
</Window>

效果如下图所示:

图 1:运行结果

HID 驱动

新建一个 HID.cs 文件,代码拷贝《实现 Mifare S50 充值功能》这篇文章中的 HID.cs。

读写代码编写

主窗体代码 MainWindow.xaml.cn 如下:

using System;
using System.Text;
using System.Windows;

namespace RW15693
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        HID hid;
        byte[] uid = new byte[8];

        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 };
            cbUID.Items.Clear();
            if (hid.Write(frame) == PortReturn.Success)
            {
                byte[] recv = hid.Read();
                if (recv[0] == 0xFF)
                {
                    tbMsg.Text = "寻卡失败,请检查标签是否已经放入感应场。";
                    btnRead.IsEnabled = false;
                    btnWrite.IsEnabled = false;
                    return;
                }
                else if (recv[0] == 0x01)
                {
                    btnRead.IsEnabled = true;
                    btnWrite.IsEnabled = true;
                    int count = recv[1];
                    StringBuilder sb = new StringBuilder(24);
                    for (int i = 0; i < count; i++)
                    {
                        sb.Clear();
                        for (int j = i * 8 + 2; j < i * 8 + 10; j++)
                        {
                            sb.Append(recv[j].ToString("X2"));
                            sb.Append(" ");
                        }
                        sb.Remove(23, 1); //移除最后一个空格
                        cbUID.Items.Add(sb.ToString());
                    }
                    cbUID.SelectedIndex = 0;
                }
            }
            else
            {
                tbMsg.Text = "通信错误";
            }
        }

        private void btnRead_Click(object sender, RoutedEventArgs e)
        {
            byte[] frame = new byte[9];
            frame[0] = 0x02;
            string[] uidStrs = cbUID.Text.Split(' ');
            byte[] uid = new byte[8];
            for (int i = 0; i < 8; i++)
            {
                uid[i] = Convert.ToByte(uidStrs[i], 16);
            }
            Array.Copy(uid, 0, frame, 1, 8);
            if (hid.Write(frame) == PortReturn.Success)
            {
                byte[] recv = hid.Read();
                if (recv[0] != 0x02)
                {
                    tbMsg.Text = "读卡失败,请检查标签是否已经放入感应场。";
                    btnRead.IsEnabled = false;
                    btnWrite.IsEnabled = false;
                    return;
                }
                int len = recv[1];
                byte[] readBuff = new byte[len];
                if (len <= 62)
                {
                    Array.Copy(recv, 2, readBuff, 0, len);
                }
                else
                {
                    Array.Copy(recv, 2, readBuff, 0, 62);
                    recv = hid.Read();
                    txtContent.Text += "\r\n";
                    if (recv[0] != 0x03)
                    {
                        tbMsg.Text = "读卡失败,请检查标签是否已经放入感应场。";
                        btnRead.IsEnabled = false;
                        btnWrite.IsEnabled = false;
                        return;
                    }
                    Array.Copy(recv, 1, readBuff, 62, len - 62);
                }
                string str = Encoding.Unicode.GetString(readBuff);
                txtContent.Text = str;
                tbMsg.Text = "成功读取标签中的数据!";
            }
            else
            {
                tbMsg.Text = "通信错误";
            }
        }

        private void btnWrite_Click(object sender, RoutedEventArgs e)
        {
            if (txtContent.Text == "")
            {
                MessageBox.Show("请在编辑框内输入将要写入标签的数据!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }
            byte[] writeByte = Encoding.Unicode.GetBytes(txtContent.Text);
            if (writeByte.Length > 111)
            {
                MessageBox.Show("写入标签的数据不能超过 111 字节!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }
            byte[] frame = new byte[64];
            frame[0] = 0x03;
            //加入UID
            string[] uidStrs = cbUID.Text.Split(' ');
            byte[] uid = new byte[8];
            for (int i = 0; i < 8; i++)
            {
                uid[i] = Convert.ToByte(uidStrs[i], 16);
            }
            Array.Copy(uid, 0, frame, 1, 8);
            //加入数据长度
            frame[9] = (byte)writeByte.Length;
            //加入数据
            if (writeByte.Length <= 54)
            {
                Array.Copy(writeByte, 0, frame, 10, writeByte.Length);
                if (hid.Write(frame) != PortReturn.Success)
                {
                    tbMsg.Text = "通信错误";
                    return;
                }
            }
            else
            {
                //发送第一段
                Array.Copy(writeByte, 0, frame, 10, 54);
                if (hid.Write(frame) != PortReturn.Success)
                {
                    tbMsg.Text = "通信错误";
                    return;
                }
                //发送第二段
                Array.Clear(frame, 0, 64);
                frame[0] = 0x04;
                Array.Copy(writeByte, 54, frame, 1, writeByte.Length - 54);
                if (hid.Write(frame) != PortReturn.Success)
                {
                    tbMsg.Text = "通信错误";
                    return;
                }
            }
            byte[] recv = hid.Read();
            if (recv[0] == 0x04)
            {
                tbMsg.Text = "数据写入成功!";
            }
            else if (recv[0] == 0xFF)
            {
                tbMsg.Text = "数据写入失败,错误码:" + recv[1].ToString();
            }
        }
    }
}

程序设计得非常简单,如果运行过程中出现错误,需要重新寻卡操作。运行效果如下:

图 1:运行结果

思考题

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

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

思考题二:修改代码,实现在标签内保存图片的功能,并可以在上位机中将图片读出并显示。

;

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