作者:陈广 日期:2020-12-3
14443 读完后,就轮到15693了,RFAL 开发包并不是可以很方便直接使用的开发包,访问特定标签,需在其基础上再包装一层,之前的 Mifare Classic 就是需要自己做这个包装的。在 ST25R3911B-DISCO 开发板固件中,完整包装了 15693 各种操作的方法,我们直接拷贝过来就可以使用了,省了很多事。
这次直接使用《实现 Mifare S50 充值功能》这篇文章作为基础,继续编写代码。首先下载以下文件:
解压后,将 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 次传输方能完成。本协议为半双工协议,由上位机主动发起指令。
0x01(1字节) + 寻找到的卡数量(1字节)+ UID(8字节*卡数量)
收到上位机寻卡请求,成功操作后,返回所有卡的UID。为降低协议复杂度,限定寻找到的卡数量最多为 7 张,这样数据包长度就不会超过 64 个字节。
0x02(1字节)+ 数据长度(1字节,小于或等于111) + 数据(n字节,n<=62)
收到上位机读数据请求,成功操作后,返回数据。由于 I-CODE SLI 标签存储容量为 112 字节,最大可能会分两个数据包返回数据。后一个数据包格式如下:
0x03(1字节)+ 数据(n字节,n<=49)
数据从0号块开始存储,0号块的第0字节用于存放数据长度(0号块0字节不计算在内)。
0x03(1字节) 操作成功,如成功向卡片写入数据,则返回此数据
0xFF(1字节) + 错误码(1字节)
向上位机返回操作失败信息。
0x01(1字节)
寻卡命令
0x02(1字节) + UID(8字节) 向指定 UID 的标签读取数据,数据从0块开始读取。
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>
效果如下图所示:
新建一个 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();
}
}
}
}
程序设计得非常简单,如果运行过程中出现错误,需要重新寻卡操作。运行效果如下:
程序相当不完善,最大的问题在于使用了同步的方法读写 USB 数据。如果仔细查看单片机代码,可知,有些操作如果出现错误,是不会返回任何结果的,此时会导致上位机程序阻塞,只能重启程序解决问题,这样肯定不行。
思考题一:就是将所有 USB 数据的读写放入线程中执行,每发一条命令如果在限定时间内(timeout)等不到返回,则关闭线程。
思考题二:修改代码,实现在标签内保存图片的功能,并可以在上位机中将图片读出并显示。
;