认识RC522模块

RC522是一种常用的无线射频识别(RFID)模块,广泛用于门禁系统、身份识别、支付系统等领域。它基于13.56MHz的射频技术,与NFC(近场通信)技术兼容,能够读取和写入符合ISO/IEC 14443 A/MIFARE协议的标签和卡片。hardware

ic卡结构

ic卡,比如我们日常使用的校园卡,以最常见的MIFARE Classic IC卡为例:包含16个扇区,每个扇区由4个块组成,每个块可以存储16个字节的数据。每个扇区的最后一个块是尾块,包含了两个密钥(Key A和Key B)以及访问条件。这些密钥和访问条件用于控制对相应扇区的读写权限。

其中扇区0:第一个扇区(扇区0)的第一个块(块0)被称为制造商块,它包含了卡片的唯一标识符(UID)、制造商数据等信息,是出厂时就写入的,而且在大多数情况下无法更改。

其他扇区可用于存放个人信息:姓名、性别、出生日期等;账户信息:余额、最近交易记录等……

因此当你给校园卡充值后,需要通过某种形式的刷卡操作,将新的金额信息写入到卡内,这样卡内保存的金额信息才会更新。

例子:扇区1的布局

  • **块0 (扇区1的第1块)**:学生ID号
    • 例如:01 23 45 67(假设学生ID为1234567,前面的0用于填充,因为每个块是16字节,剩余的字节可能留空或用于其他目的)
  • **块1 (扇区1的第2块)**:学生基本信息
    • 例如:4A 6F 68 6E 20 44 6F 65(ASCII编码的”John Doe”作为学生的姓名)
    • 接着可能是出生年份:19 99(假设1999年出生)
    • 剩余的字节可能用于填充或其他简单的信息
  • **块2 (扇区1的第3块)**:图书馆借阅状态
    • 例如:00 03(表示当前借了3本书)
    • 后面跟着最后一次借书的日期:20 21 04 15(假设是2021年4月15日)
    • 剩余的字节可以存储书的ID或留空
  • **块3 (扇区1的第4块,控制块)**:控制块设置访问权限和存储密钥
    • 这个块不用于存储普通数据,而是定义了如何访问这个扇区的数据。包括两个密钥(Key A和Key B)及其对应的访问条件。

示例数据

假设数据如下:

  • 块001 23 45 67 00 00 00 00 00 00 00 00 00 00 00 00
  • 块14A 6F 68 6E 20 44 6F 65 19 99 00 00 00 00 00 00
  • 块200 03 20 21 04 15 00 00 00 00 00 00 00 00 00 00
  • 块3:密钥A、访问条件、密钥B(具体值依赖于系统安全策略)

硬件连接

首先,你需要将RC522模块和ESP8266连接起来。RC522模块的通信是通过SPI接口进行的。以下是一个典型的连接方式:

  • SDA(SS): 连接到ESP8266的GPIO4(或其他可用的CS引脚)
  • SCK: 连接到ESP8266的GPIO14(SPI的SCK)
  • MOSI: 连接到ESP8266的GPIO13(SPI的MOSI)
  • MISO: 连接到ESP8266的GPIO12(SPI的MISO)
  • IRQ: 不连接
  • GND: 连接到ESP8266的GND
  • RST: 连接到ESP8266的GPIO5(或其他可控制的引脚)
  • 3.3V: 连接到ESP8266的3.3V

代码

读取卡的内容,每个扇区的两组密码(Key A和Key B)用于不同的安全访问控制。这些密码可以独立设置,以控制对扇区内数据的读写权限。默认的密钥FF FF FF FF FF FF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <SPI.h>
#include <MFRC522.h>
#define RST_PIN 5 // 配置针脚
#define SS_PIN 4
MFRC522 mfrc522(SS_PIN, RST_PIN); // 创建新的RFID实例
MFRC522::MIFARE_Key key;


void setup() {
Serial.begin(9600); // 设置串口波特率为9600
while (!Serial); // 如果串口没有打开,则死循环下去不进行下面的操作
SPI.begin(); // SPI开始
mfrc522.PCD_Init(); // Init MFRC522 card
for (byte i = 0; i < 6; i++) {
key.keyByte[i] = 0xFF;
}
Serial.println(F("扫描卡开始进行读或者写"));
Serial.print(F("使用A和B作为键"));
dump_byte_array(key.keyByte, MFRC522::MF_KEY_SIZE);
Serial.println();
Serial.println(F("注意,会把数据写入到卡在#1"));
}


void loop() {
// 寻找新卡
if ( ! mfrc522.PICC_IsNewCardPresent())
return;
// 选择一张卡
if ( ! mfrc522.PICC_ReadCardSerial())
return;
// 显示卡片的详细信息
Serial.print(F("卡片 UID:"));
dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size);
Serial.println();
Serial.print(F("卡片类型: "));
MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
Serial.println(mfrc522.PICC_GetTypeName(piccType));
// 检查兼容性
if ( piccType != MFRC522::PICC_TYPE_MIFARE_MINI
&& piccType != MFRC522::PICC_TYPE_MIFARE_1K
&& piccType != MFRC522::PICC_TYPE_MIFARE_4K) {
Serial.println(F("仅仅适合Mifare Classic卡的读写"));
return;
}
// 我们只使用第二个扇区
// 覆盖扇区4
// byte sector = 1;
// byte blockAddr = 4;
byte dataBlock[] = {
};//写入的数据定义
byte trailerBlock = 7;
MFRC522::StatusCode status;
byte buffer[18];
byte size = sizeof(buffer);
// 原来的数据
Serial.println(F("显示原本的数据..."));
status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, &key, &(mfrc522.uid));
if (status != MFRC522::STATUS_OK) {
Serial.print(F("身份验证失败?或者是卡链接失败"));
Serial.println(mfrc522.GetStatusCodeName(status));
return;
}
// 显示整个扇区
for (byte sector = 0; sector < 16; sector++) {
Serial.println(F("显示所有扇区的数据"));
mfrc522.PICC_DumpMifareClassicSectorToSerial(&(mfrc522.uid), &key, sector);
Serial.println();
}
}

// 将字节数组转储为串行的十六进制值

void dump_byte_array(byte *buffer, byte bufferSize) {
for (byte i = 0; i < bufferSize; i++) {
Serial.print(buffer[i] < 0x10 ? " 0" : " ");
Serial.print(buffer[i], HEX);
}
}

read_data

修改卡的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <SPI.h>
#include <MFRC522.h>
#define RST_PIN 5 // 配置针脚
#define SS_PIN 4
MFRC522 mfrc522(SS_PIN, RST_PIN); // 创建新的RFID实例
MFRC522::MIFARE_Key key;
void setup() {
Serial.begin(9600); // 设置串口波特率为9600
while (!Serial); // 如果串口没有打开,则死循环下去不进行下面的操作
SPI.begin(); // SPI开始
mfrc522.PCD_Init(); // Init MFRC522 card
for (byte i = 0; i < 6; i++) {
key.keyByte[i] = 0xFF;
}
Serial.println(F("扫描卡开始进行读或者写"));
Serial.print(F("使用A和B作为键"));
dump_byte_array(key.keyByte, MFRC522::MF_KEY_SIZE);
Serial.println();
Serial.println(F("注意,会把数据写入到卡在#1"));
}
void loop() {
// 寻找新卡
if ( ! mfrc522.PICC_IsNewCardPresent())
return;
// 选择一张卡
if ( ! mfrc522.PICC_ReadCardSerial())
return;
// 显示卡片的详细信息
Serial.print(F("卡片 UID:"));
dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size);
Serial.println();
Serial.print(F("卡片类型: "));
MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
Serial.println(mfrc522.PICC_GetTypeName(piccType));
// 检查兼容性
if ( piccType != MFRC522::PICC_TYPE_MIFARE_MINI
&& piccType != MFRC522::PICC_TYPE_MIFARE_1K
&& piccType != MFRC522::PICC_TYPE_MIFARE_4K) {
Serial.println(F("仅仅适合Mifare Classic卡的读写"));
return;
}
// 我们只使用第二个扇区
// 覆盖扇区4
byte sector = 1;
byte blockAddr = 4;
byte dataBlock[] = {
0x01, 0x02, 0x03, 0x04, // 1, 2, 3, 4,
0x05, 0x06, 0x07, 0x08, // 5, 6, 7, 8,
0x00, 0x00, 0x00, 0x00, // 0,0,0,0
0x00, 0x00, 0x00, 0x00 // 0,0,0,0
};//写入的数据定义
byte trailerBlock = 7;
MFRC522::StatusCode status;
byte buffer[18];
byte size = sizeof(buffer);
// 原来的数据
Serial.println(F("显示原本的数据..."));
status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, &key, &(mfrc522.uid));
if (status != MFRC522::STATUS_OK) {
Serial.print(F("身份验证失败?或者是卡链接失败"));
Serial.println(mfrc522.GetStatusCodeName(status));
return;
}
// 显示整个扇区
Serial.println(F("显示所有扇区的数据"));
mfrc522.PICC_DumpMifareClassicSectorToSerial(&(mfrc522.uid), &key, sector);
Serial.println();
// 从块儿读取数据
Serial.print(F("读取块儿的数据在:")); Serial.print(blockAddr);
Serial.println(F("块 ..."));
status = (MFRC522::StatusCode) mfrc522.MIFARE_Read(blockAddr, buffer, &size);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("读卡失败,没有连接上 "));
Serial.println(mfrc522.GetStatusCodeName(status));
}
Serial.print(F("数据内容在第 ")); Serial.print(blockAddr); Serial.println(F(" 块:"));
dump_byte_array(buffer, 16); Serial.println();
Serial.println();
//开始进行写入准备
Serial.println(F("开始进行写入的准备..."));
status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, trailerBlock, &key, &(mfrc522.uid));
if (status != MFRC522::STATUS_OK) {
Serial.print(F("写入失败,没有连接上或者没有权限 "));
Serial.println(mfrc522.GetStatusCodeName(status));
return;
}
// Write data to the block
Serial.print(F("在第: ")); Serial.print(blockAddr);
Serial.println(F(" 块中写入数据..."));
dump_byte_array(dataBlock, 16); Serial.println();
status = (MFRC522::StatusCode) mfrc522.MIFARE_Write(blockAddr, dataBlock, 16);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("写入失败... "));
Serial.println(mfrc522.GetStatusCodeName(status));
}
Serial.println();
// 再次读取卡中数据,这次是写入之后的数据
Serial.print(F("读取写入后第")); Serial.print(blockAddr);
Serial.println(F(" 块的数据 ..."));
status = (MFRC522::StatusCode) mfrc522.MIFARE_Read(blockAddr, buffer, &size);
if (status != MFRC522::STATUS_OK) {
Serial.print(F("读取失败... "));
Serial.println(mfrc522.GetStatusCodeName(status));
}
Serial.print(F("块 ")); Serial.print(blockAddr); Serial.println(F("数据为 :"));
dump_byte_array(buffer, 16); Serial.println();
// 验证一下数据,要保证写入前后数据是相等的
// 通过计算块中的字节数量
Serial.println(F("等待验证结果..."));
byte count = 0;
for (byte i = 0; i < 16; i++) {
// 比较一下缓存中的数据(我们读出来的数据) = (我们刚刚写的数据)
if (buffer[i] == dataBlock[i])
count++;
}
Serial.print(F("匹配的字节数量 = ")); Serial.println(count);
if (count == 16) {
Serial.println(F("验证成功 :"));
} else {
Serial.println(F("失败,数据不匹配"));
Serial.println(F("也许写入的内容不恰当"));
}
Serial.println();
// 转储扇区数据
Serial.println(F("写入后的数据内容为::"));
mfrc522.PICC_DumpMifareClassicSectorToSerial(&(mfrc522.uid), &key, sector);
Serial.println();
// 停止 PICC
mfrc522.PICC_HaltA();
//停止加密PCD
mfrc522.PCD_StopCrypto1();
}

// 将字节数组转储为串行的十六进制值

void dump_byte_array(byte *buffer, byte bufferSize) {
for (byte i = 0; i < bufferSize; i++) {
Serial.print(buffer[i] < 0x10 ? " 0" : " ");
Serial.print(buffer[i], HEX);
}
}

write_data

当使用SPI通信时,MOSI(主设备输出/从设备输入)和MISO(主设备输入/从设备输出)引脚通常是固定的,不需要在软件中显式配置它们。这是因为SPI接口是由硬件控制的,而不是软件模拟,所以MOSI和MISO引脚已经在微控制器的硬件设计中预定义好了。

对于ESP8266来说,这些固定的SPI引脚通常是:

  • MOSI:D7 (GPIO13)
  • MISO:D6 (GPIO12)
  • SCK(时钟信号):D5 (GPIO14)

而SS(片选)引脚和RST(复位)引脚可以是任何可用的GPIO引脚,所以需要在软件中指定以正确初始化SPI设备。