【STM32】STM32+RC522-射频IC识别模块
看了很多文章都没有详细讲过RC522和IC卡,这里总结一下,做个笔记
一、RC522
RFID-RC522 是一种常见的射频识别(RFID)模块,用于近距离无线通信。以下是一些基本信息:
主要特点
常用功能
- 读取卡片 UID: 可以获取 RFID 卡的唯一标识符。
- 读写数据块: 支持对 MIFARE 卡片的数据块进行读写操作(需要密钥认证)。
- 防碰撞机制: 可识别多个卡片并与目标卡通信。
- 功耗低: 支持节能模式。
硬件接口
RC522 模块有以下引脚:
常见应用
二、IC卡
关于详细的IC卡的介绍可以看下面这个链接,讲的非常详细
【【教程】手把手教你玩转IC卡(门禁卡、电梯卡、饭卡、水卡、校园卡、一卡通)】https://www.bilibili.com/video/BV11h4y1T7Pm?vd_source=065123a0ee559bc650cb8a6e66c8de94
我们生活中常见的一般都是cuid卡,它有16个扇区,依次为0扇区-15扇区,每个扇区又分为4个区块,一个区块有16个字节,所以一个扇区有64个字节,卡总共有1024个字节,就是1KB。
卡的0扇区的第一个区块包含了卡号,校验值,卡类型,卡类型代号,生产厂商信息。第一个区块的前四个字节是卡号,所以我们的卡号是00 00 00 00 – FF FF FF FF的范围内,如果卡号是唯一的,那么就有16^8=4294967296个卡号,42亿多个,一时半会儿也用不完,如果用完了也可以像ipv4->ipv6那样扩展,将六个字节作为卡号,那不就取之不尽用之不竭了吗。我们的身份证的卡号就是6个字节的卡号。当 4 字节 UID 不足以满足需求时,可以使用 7 字节或 10 字节的 UID。这类似于互联网的 IPv4 向 IPv6 的扩展,虽然理论上有限,但可以通过增加长度来大幅度扩展编号空间。
做一个简易门禁系统可以通过验证uid(卡号)来验证身份,但是问题就是卡号很容易被复制,所以我们就可以用上其他扇区。每个扇区的第四个区块是控制块,在这个区块中,前六个字节是密钥A,后六个字节是密钥B,中间四个字节一般是厂商写好固定的FF 07 80 69。我们想要增加门禁系统的安全性,就可以将01扇区(第2扇区)的第1区块的第一个字节设置为FF,如果你这个字节是FF那你就是我们小区的,如果不是FF就不是我们小区的。在我们读取这个区块的内容的时候,需要验证密钥A或者秘钥B才能访问这个扇区里面的数据,所以我们还可以修改密钥实现简单的加密效果,这里配上后面的代码更好理解。
三、通信前的准备
我们使用stm32个四个GPIO模拟SPI与RC522进行通信
1、SPI协议层
/*引脚配置*/
#define RC522_CS(x) GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)(x))
#define RC522_SCK(x) GPIO_WriteBit(GPIOB, GPIO_Pin_13, (BitAction)(x))
#define RC522_DO(x) GPIO_WriteBit(GPIOB, GPIO_Pin_15, (BitAction)(x))
#define RC522_DI() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14)
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 通过 RC522 向设备发送一个字节的数据
// 参数说明 uint8_t data - 要发送的数据字节
// 返回参数 void
// 使用示例 RC522_SendByte(0x3F); // 发送一个字节 0x3F 到 RC522
// 备注信息 该函数通过控制 SCK 引脚来逐位发送数据,每发送一个位,SCK 会先拉高再拉低。
// 发送完成后,SCK 会返回低电平,等待下一位数据发送。
//-------------------------------------------------------------------------------------------------------------------
void RC522_SendByte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 发送高位
RC522_DO(data & 0x80);
data <<= 1;
// 拉高 SCK
RC522_SCK(1);
RC522_Delay(1);
// 拉低 SCK
RC522_SCK(0);
RC522_Delay(1);
}
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 从 RC522 读取一个字节的数据
// 参数说明 void
// 返回参数 uint8_t - 读取到的字节数据
// 使用示例 uint8_t byte = RC522_ReadByte(); // 从 RC522 读取一个字节的数据
// 备注信息 该函数通过 SCK 引脚控制 SPI 时钟,读取 MISO 引脚的数据,逐位构建接收到的数据字节。
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_ReadByte(void)
{
uint8_t data = 0;
for (uint8_t i = 0; i < 8; i++)
{
// 拉高 SCK
RC522_SCK(1);
RC522_Delay(1);
// 读取 MISO 引脚
data <<= 1;
if (RC522_DI())
{
data |= 0x01;
}
// 拉低 SCK
RC522_SCK(0);
RC522_Delay(1);
}
return data;
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 向 RC522 寄存器写入一个字节的数据
// 参数说明 uint8_t addr - 寄存器地址
// uint8_t value - 要写入的数据
// 返回参数 void
// 使用示例 RC522_WriteReg(0x01, 0x0F); // 向 RC522 的地址为 0x01 的寄存器写入值 0x0F
// 备注信息 该函数通过 SPI 总线向指定寄存器写入数据。发送时会先发送寄存器地址,地址最低位为 0 表示写操作,然后发送要写入的值。
//-------------------------------------------------------------------------------------------------------------------
void RC522_WriteReg(uint8_t addr, uint8_t value)
{
RC522_CS(0); // 片选拉低
RC522_SendByte((addr << 1) & 0x7E); // 地址左移,最低位 0 表示写
RC522_SendByte(value); // 写入值
RC522_CS(1); // 片选拉高
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 从 RC522 寄存器读取一个字节的数据
// 参数说明 uint8_t addr - 寄存器地址
// 返回参数 uint8_t - 读取到的寄存器数据
// 使用示例 uint8_t value = RC522_ReadReg(0x01); // 读取 RC522 地址为 0x01 的寄存器数据
// 备注信息 该函数通过 SPI 总线读取 RC522 寄存器的数据。
// 发送时会先发送寄存器地址,地址最低位为 1 表示读操作,然后读取返回值。
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_ReadReg(uint8_t addr)
{
uint8_t value;
RC522_CS(0); // 片选拉低
RC522_SendByte(((addr << 1) & 0x7E) | 0x80); // 地址左移,最低位 1 表示读
value = RC522_ReadByte(); // 读取值
RC522_CS(1); // 片选拉高
return value;
}
void RC522_Init(void)
{
RC522_GPIO_Init();
// 软件复位
RC522_WriteReg(0x01, 0x0F);
RC522_Delay(10);
// 配置寄存器 (根据 RC522 数据手册配置)
RC522_WriteReg(RC522_ModeReg, 0x3D);
RC522_WriteReg(RC522_TReloadRegL, 0x1E);
RC522_WriteReg(RC522_TReloadRegH, 0x00);
RC522_WriteReg(RC522_TModeReg, 0x8D);
RC522_WriteReg(RC522_TPrescalerReg, 0x3E);
RC522_WriteReg(RC522_TxASKReg, 0x40);
RC522_AntennaOn();
}
/* 延迟函数 */
void RC522_Delay(uint16_t time)
{
Delay_us(time);
}
2、寄存器重定义
芯片手册中有RC522所有寄存器的介绍和地址,在RC522.h文件中添加RC522寄存器的重定义
#define DEF_FIFO_LENGTH 64 //FIFO size=64byte
#define MAXRLEN 18
/* RC522 寄存器定义 */
// 页面 0:命令和状态
#define RC522_RFU00 0x00 // 保留
#define RC522_CommandReg 0x01 // 命令寄存器
#define RC522_ComIEnReg 0x02 // 中断使能寄存器
#define RC522_DivIEnReg 0x03 // 中断使能寄存器(分频器)
#define RC522_ComIrqReg 0x04 // 中断请求寄存器
#define RC522_DivIrqReg 0x05 // 中断请求寄存器(分频器)
#define RC522_ErrorReg 0x06 // 错误标志寄存器
#define RC522_Status1Reg 0x07 // 通信状态寄存器 1
#define RC522_Status2Reg 0x08 // 通信状态寄存器 2
#define RC522_FIFODataReg 0x09 // FIFO 数据寄存器
#define RC522_FIFOLevelReg 0x0A // FIFO 水位寄存器
#define RC522_WaterLevelReg 0x0B // 水平线设置寄存器
#define RC522_ControlReg 0x0C // 控制寄存器
#define RC522_BitFramingReg 0x0D // 位控制寄存器
#define RC522_CollReg 0x0E // 冲突检测寄存器
#define RC522_RFU0F 0x0F // 保留
// 页面 1:命令配置
#define RC522_Reserved10 0x10 // 保留
#define RC522_ModeReg 0x11 // 模式寄存器
#define RC522_TxModeReg 0x12 // 发射模式寄存器
#define RC522_RxModeReg 0x13 // 接收模式寄存器
#define RC522_TxControlReg 0x14 // 发射控制寄存器
#define RC522_TxASKReg 0x15 // 发射调制寄存器
#define RC522_TxSelReg 0x16 // 发射选择寄存器
#define RC522_RxSelReg 0x17 // 接收选择寄存器
#define RC522_RxThresholdReg 0x18 // 接收阈值寄存器
#define RC522_DemodReg 0x19 // 解调寄存器
#define RC522_RFU1A 0x1A // 保留
#define RC522_RFU1B 0x1B // 保留
#define RC522_MfTxReg 0x1C // MIFARE 发射寄存器
#define RC522_MfRxReg 0x1D // MIFARE 接收寄存器
#define RC522_RFU1E 0x1E // 保留
#define RC522_SerialSpeedReg 0x1F // 串行通信速度寄存器
// 页面 2:CRC、计时器
#define RC522_Reserved20 0x20 // 保留
#define RC522_CRCResultRegH 0x21 // CRC 结果高字节
#define RC522_CRCResultRegL 0x22 // CRC 结果低字节
#define RC522_RFU23 0x23 // 保留
#define RC522_ModWidthReg 0x24 // 调制宽度寄存器
#define RC522_RFU25 0x25 // 保留
#define RC522_RFCfgReg 0x26 // 接收配置寄存器
#define RC522_GsNReg 0x27 // 选择寄存器
#define RC522_CWGsPReg 0x28 // 发射功率寄存器
#define RC522_ModGsPReg 0x29 // 调制功率寄存器
#define RC522_TModeReg 0x2A // 定时模式寄存器
#define RC522_TPrescalerReg 0x2B // 定时预分频寄存器
#define RC522_TReloadRegH 0x2C // 定时重载高字节
#define RC522_TReloadRegL 0x2D // 定时重载低字节
#define RC522_TCounterValRegH 0x2E // 定时器计数器高字节
#define RC522_TCounterValRegL 0x2F // 定时器计数器低字节
// 页面 3:测试寄存器
#define RC522_Reserved30 0x30 // 保留
#define RC522_TestSel1Reg 0x31 // 测试选择 1 寄存器
#define RC522_TestSel2Reg 0x32 // 测试选择 2 寄存器
#define RC522_TestPinEnReg 0x33 // 测试针脚使能寄存器
#define RC522_TestPinValueReg 0x34 // 测试针脚值寄存器
#define RC522_TestBusReg 0x35 // 测试总线寄存器
#define RC522_AutoTestReg 0x36 // 自检寄存器
#define RC522_VersionReg 0x37 // 版本寄存器
#define RC522_AnalogTestReg 0x38 // 模拟测试寄存器
#define RC522_TestDAC1Reg 0x39 // 测试 DAC1 寄存器
#define RC522_TestDAC2Reg 0x3A // 测试 DAC2 寄存器
#define RC522_TestADCReg 0x3B // 测试 ADC 寄存器
#define RC522_RFU3C 0x3C // 保留
#define RC522_RFU3D 0x3D // 保留
#define RC522_RFU3E 0x3E // 保留
#define RC522_RFU3F 0x3F // 保留
#define RC522_Apezer 0x66 // 防伪标签,作者WX寄存器
/* RC522 命令定义 */
#define PCD_IDLE 0x00 // 进入空闲模式
#define PCD_AUTHENT 0x0E // 认证命令
#define PCD_RECEIVE 0x08 // 接收数据
#define PCD_TRANSMIT 0x04 // 发送数据
#define PCD_TRANSCEIVE 0x0C // 发送并接收数据
#define PCD_RESETPHASE 0x0F // 复位阶段
#define PCD_CALCCRC 0x03 // CRC 计算命令
/* Mifare_One 命令寄存器定义 */
#define PICC_REQIDL 0x26 // 请求处于空闲状态的卡片 (不响应未被选中的卡片)
#define PICC_REQALL 0x52 // 请求所有卡片(选择多个卡片)
#define PICC_ANTICOLL1 0x93 // 防碰撞检测命令(第一部分)
#define PICC_ANTICOLL2 0x95 // 防碰撞检测命令(第二部分)
#define PICC_AUTHENT1A 0x60 // 授权 A 密钥
#define PICC_AUTHENT1B 0x61 // 授权 B 密钥
#define PICC_READ 0x30 // 读取数据命令
#define PICC_WRITE 0xA0 // 写入数据命令
#define PICC_DECREMENT 0xC0 // 递减命令
#define PICC_INCREMENT 0xC1 // 增加命令
#define PICC_RESTORE 0xC2 // 恢复数据命令
#define PICC_TRANSFER 0xB0 // 传输数据命令(将数据传输到卡片的块中)
#define PICC_HALT 0x50 // 停止命令(使卡片停止并进入休眠状态)
#define MI_OK 0x00 // 操作成功
#define MI_ERR 0x01 // 操作失败
#define MI_NOTAGERR 0x02 // 没有卡片
#define MI_WRONG 0x03 // 错误的命令
3、RC522_ToCard
该函数是利用rc522和卡通信的核心函数,该函数的第一个参数command是要执行的命令,第二个参数是发送的命令数组,第三个是发送命令数组的长度,第四个参数用于接收从RC522返回的数据,第五个参数是返回数据数组的长度
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 与 MFRC522 通信,执行特定命令(如认证或数据传输)
// 参数说明 command : 要执行的命令(如 PCD_AUTHENT 或 PCD_TRANSCEIVE)
// sendData : 发送到 MFRC522 的数据指针
// sendLen : 发送数据的长度
// backData : 接收数据的指针(用于存储从 MFRC522 返回的数据)
// backLen : 接收数据的长度指针(用于返回数据位数)
// 返回参数 uint8_t : 执行状态
// MI_OK - 成功
// MI_ERR - 错误
// MI_NOTAGERR - 未检测到卡片
// 使用示例 uint8_t status;
// uint8_t sendData[] = {0x30, 0x00};
// uint8_t backData[16];
// uint16_t backLen;
// status = RC522_ToCard(PCD_TRANSCEIVE, sendData, 2, backData, &backLen);
// 备注信息 - PCD_AUTHENT:用于验证密钥
// - PCD_TRANSCEIVE:用于发送和接收数据
// - 如果返回 MI_OK,backData 和 backLen 会包含从卡片返回的数据
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_ToCard(uint8_t command, uint8_t *sendData, uint8_t sendLen, uint8_t *backData, uint16_t *backLen)
{
uint8_t status = MI_ERR;
uint8_t irqEn = 0x00;
uint8_t waitIRq = 0x00;
uint8_t n, lastBits;
uint16_t i;
// 根据命令设置中断使能位和等待标志位
if (command == PCD_AUTHENT) {
irqEn = 0x12; // 允许认证相关中断
waitIRq = 0x10; // 等待认证完成中断
} else if (command == PCD_TRANSCEIVE) {
irqEn = 0x77; // 允许所有与传输相关的中断
waitIRq = 0x30; // 等待发送完成或接收完成中断
}
RC522_WriteReg(RC522_ComIEnReg, irqEn | 0x80); // 设置中断使能寄存器
ClearBitMask(RC522_ComIrqReg, 0x80); // 清除中断标志位
RC522_WriteReg(RC522_CommandReg, PCD_IDLE); // 进入空闲模式
RC522_WriteReg(RC522_FIFOLevelReg, 0x80); // 清空 FIFO 数据缓冲区
// 将数据写入 FIFO
for (i = 0; i < sendLen; i++) {
RC522_WriteReg(RC522_FIFODataReg, sendData[i]);
}
// 发起命令
RC522_WriteReg(RC522_CommandReg, command);
if (command == PCD_TRANSCEIVE) {
SetBitMask(RC522_BitFramingReg, 0x80); // 开始发送数据
}
// 等待命令完成
i = 1500; // 超时计数器
do {
n = RC522_ReadReg(RC522_ComIrqReg); // 读取中断标志寄存器
i--;
} while ((i != 0) && !(n & 0x01) && !(n & waitIRq));
ClearBitMask(RC522_BitFramingReg, 0x80); // 停止发送数据
// 检查结果
if (i != 0) { // 如果未超时
if (!(RC522_ReadReg(RC522_ErrorReg) & 0x1B)) { // 无传输错误
status = MI_OK;
if (n & irqEn & 0x01) {
status = MI_NOTAGERR; // 没有检测到卡片
}
if (command == PCD_TRANSCEIVE) {
n = RC522_ReadReg(RC522_FIFOLevelReg); // 读取 FIFO 中接收数据的字节数
lastBits = RC522_ReadReg(RC522_ControlReg) & 0x07; // 读取最后几位
if (lastBits) {
*backLen = (n - 1) * 8 + lastBits; // 数据位数
} else {
*backLen = n * 8;
}
// 从 FIFO 中读取接收到的数据
if (n > 0 && n < 16) {
for (i = 0; i < n; i++) {
backData[i] = RC522_ReadReg(RC522_FIFODataReg);
}
}
}
} else {
status = MI_ERR;
}
}
return status;
}
四、通信
1、RC522_Request (寻卡)
第一个参数为命令,第二个参数用于接收返回的数据,当发出寻卡命令时,会返回两个字节的数据,这两个字节对应这卡类型的代码,也就是卡片中第一个区块的第七个和第八个字节 0x4400 – Mifare_UltraLight 0x0400 – Mifare_One(S50)
0x0200 – Mifare_One(S70) 0x0800 – Mifare_Pro(X) 0x4403 – Mifare_DESFire我们身边的门禁卡一般都是0400,发送命令数组就一个字节,是这个函数的第一个参数
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 寻卡操作,检测 MFRC522 感应区内的卡片
// 参数说明 reqMode : 寻卡方式
// 0x52 - 寻感应区内所有符合 14443A 标准的卡
// 0x26 - 寻未进入休眠状态的卡
// backData : 指向返回卡片类型代码的指针
// 0x4400 - Mifare_UltraLight
// 0x0400 - Mifare_One(S50)
// 0x0200 - Mifare_One(S70)
// 0x0800 - Mifare_Pro(X)
// 0x4403 - Mifare_DESFire
// 返回参数 uint8_t : 执行状态
// MI_OK - 成功
// MI_ERR - 错误
// 使用示例 uint8_t status;
// uint8_t tagType[2];
// status = RC522_Request(PICC_REQALL, tagType);
// 备注信息 - 在调用本函数之前,需要确保 MFRC522 已初始化且天线已打开
// - 返回的 backData 包含两字节的卡片类型代码
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_Request(uint8_t reqMode, uint8_t *backData)
{
uint8_t status;
uint8_t command = PCD_TRANSCEIVE;
uint8_t send_command[2];
uint8_t back_Data[MAXRLEN];
uint16_t recvBits;
ClearBitMask(RC522_Status2Reg, 0x08);
RC522_WriteReg(RC522_BitFramingReg, 0x07);
SetBitMask(RC522_TxControlReg, 0x03);
RC522_WriteReg(RC522_ControlReg, 0x00); // 清除之前的中断
send_command[0] = reqMode;
// 发送请求并等待响应
status = RC522_ToCard(command, send_command, 1, back_Data, &recvBits);
if ((status == MI_OK) && (recvBits == 0x10))
{
*backData = back_Data[0];
*(backData+1) = back_Data[1];
}
else
{
status = MI_ERR;
}
return status;
}
2、RC522_Anticoll(防冲突)
这个函数会返回卡片的4字节uid和1字节的和校验,也就是卡的第一个区块的前五个字节。snr_check ^= back_Data[i];将4个字节的uid进行异或就可以得到第五个字节的和校验,发送命令数组包含两个字节,第一个字节是防冲突命令,第二个字节是NVB参数-0x20,调用RC522_ToCard就可以获得返回的数据
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 防冲突命令,获取卡片 UID
// 参数说明 uint8_t *backData - 存储返回的数据
// 返回参数 uint8_t - 返回的状态,MI_OK 表示成功
// 使用示例 uint8_t status = RC522_Anticoll(backData);
// 备注信息 该函数用于从卡片读取 UID,防止冲突并返回 UID。
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_Anticoll(uint8_t *pSnr)
{
uint8_t status;
uint8_t i, snr_check = 0;
uint8_t command = PCD_TRANSCEIVE;
uint8_t send_command[2] = {PICC_ANTICOLL1, 0x20}; // 防冲突命令 + NVB 参数
uint8_t back_Data[MAXRLEN];
uint16_t recvBits;
ClearBitMask(RC522_Status2Reg, 0x08); // 清除 Status2Reg 寄存器的 TempSensclear 位,确保温度传感器不会影响后续操作
RC522_WriteReg(RC522_BitFramingReg, 0x00); // 配置 BitFramingReg 寄存器,使帧的最后字节全部传输 8 位,无接收对齐偏移
ClearBitMask(RC522_CollReg, 0x80); // 清除 CollReg 寄存器的 ValuesAfterColl 位,确保碰撞后寄存器不保留冲突位数据
// 启动防冲突命令
status = RC522_ToCard(command, send_command, 2, back_Data, &recvBits);
if (status == MI_OK)
{
for (i = 0; i < 4; i++)
{
*(pSnr+i) = back_Data[i];
snr_check ^= back_Data[i];
}
if (snr_check != back_Data[i])
{
status = MI_ERR;
}
else
*(pSnr + 4) = snr_check;
}
SetBitMask(RC522_CollReg, 0x80);
return status;
}
3、RC522_SelectTag(选卡)
这个函数会返回三个字节,第一个字节卡类别代号,后面两个字节是CRC校验和,发送的命令数组有九个字节,第一个字节是选择命令,第二个字节是NVB参数-0x70,第三个到第七个字节是卡片的uid和异或校验(卡片的前五个字节),第八个和第九个字节是前7个字节的CRC校验(CRC校验由RC522的硬件完成),这样就构成了选卡的9字节的命令
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 RC522_SelectTag - 选择指定的 RFID 标签
// 参数说明 uint8_t *serNum - 指向 5 字节序列号数组的指针,用于指定要选择的标签
// 返回参数 uint8_t - 状态码 (MI_OK 表示成功,其他值表示失败)
// 使用示例 uint8_t status = RC522_SelectTag(serialNumber);
// if (status == MI_OK) { /* 选择成功 */ }
// 备注信息 此函数向 RFID 标签发送选择命令 (Select) 并读取返回的响应数据。
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_SelectTag(uint8_t *serNum)
{
uint8_t status;
uint8_t i;
uint8_t command = PCD_TRANSCEIVE;
uint8_t send_command[9];
uint8_t back_Data[MAXRLEN];
uint16_t recvBits;
ClearBitMask(RC522_Status2Reg, 0x08);
RC522_WriteReg(RC522_BitFramingReg, 0x00);
ClearBitMask(RC522_CollReg, 0x80);
send_command[0] = PICC_ANTICOLL1;
send_command[1] = 0x70;
for (i = 0; i < 5; i++)
{
send_command[i+2] = *(serNum+i);
}
CalculateCRC(send_command, 7, &send_command[7]);
status = RC522_ToCard(command, send_command, 9, back_Data, &recvBits);
if ((status == MI_OK) && (recvBits == 0x18))
{
for (i = 0; i < 3; i++)
{
*(serNum+i) = back_Data[i];
}
}
return status;
}
4、RC522_Auth(验证密钥)
以验证第二扇区的密钥A为例,第一个参数authMode是验证模式,可以是PICC_AUTHENT1A(验证密钥A),也可以是PICC_AUTHENT1B(验证密钥B),第二个参数BlockAddr是区块的地址,比如说我想读取第二扇区的第一区块,那么这个区块的地址就是04,第三个参数Sectorkey就是密钥了,IC卡白卡的密钥出厂都是六个0xFF,当我们想提高安全性时,就可以将数据放在某个扇区里,再修改这个扇区的密钥,实现简易的加密效果。验证完密钥,就可以读取这个扇区里区块的数据了,也可以向这个扇区的区块里写入数据。每个扇区的第四个区块是控制块,密钥就放在这第四个区块里面,利用RC522也是可以修改密钥的,有后门指令,这个后面再说,当然使用手机的NFC软件修改更加简单。
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 验证指定扇区的密钥
// 参数说明 authMode - 验证模式,PICC_AUTHENT1A 或 PICC_AUTHENT1B
// BlockAddr - 扇区内的数据块地址(0~63,对于 16 字节的 Mifare 卡)
// Sectorkey - 密钥数组,长度为 6
// serNum - 卡片的 UID(长度为 4 字节)
// 返回参数 验证状态:MI_OK 表示成功,MI_ERR 表示失败
// 使用示例 uint8_t status = MFRC522_Auth(PICC_AUTHENT1A, 4, keyA, uid);
// 备注信息 执行认证后,可以读取或写入对应扇区的块,需确保卡片选中(调用 SelectTag)
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_Auth(uint8_t authMode, uint8_t BlockAddr, uint8_t *Sectorkey, uint8_t *serNum)
{
uint8_t sendData[12];
uint8_t back_Data[MAXRLEN];
uint8_t status;
uint16_t recvBits;
// 构建认证指令
sendData[0] = authMode; // 认证模式
sendData[1] = BlockAddr; // 数据块地址
for (uint8_t i = 0; i < 6; i++)
{
sendData[2 + i] = *(Sectorkey+i); // 密钥
}
for (uint8_t i = 0; i < 4; i++)
{
sendData[8 + i] = *(serNum+i); // UID 的前 4 字节
}
// 发送认证指令
status = RC522_ToCard(PCD_AUTHENT, sendData, 12, back_Data, &recvBits);
// 检查认证状态
if ((status != MI_OK) || (!(RC522_ReadReg(RC522_Status2Reg) & 0x08)))
{
status = MI_ERR; // 验证失败
}
return status; // 返回验证状态
}
5、RC522_Read(读取区块)
这个函数是读取区块中16个字节的函数,第一个参数是区块的地址,第二个参数值数组的首地址,用于接收返回的16字节数据
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 从M1卡中读取指定数据块
// 参数说明 blockAddr:指定读取的块地址 (0~63)
// data:存储读取数据的缓冲区指针(长度至少16字节)
// 返回参数 uint8_t:操作状态,MI_OK 表示成功,其他值表示失败
// 使用示例 uint8_t buffer[16];
// uint8_t status = RC522_Read(10, buffer); // 读取第10块的数据
// 备注信息 读取前需要完成认证操作 (使用 RC522_Auth 函数认证目标块)
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_Read(uint8_t blockAddr, uint8_t *data)
{
uint8_t status;
uint8_t command = PCD_TRANSCEIVE; // 发送与接收模式
uint8_t send_command[4]; // 用于发送读块命令的数组
uint16_t recvBits; // 接收到的位数
uint8_t back_Data[MAXRLEN]; // 存储返回的数据
// RC522_Reset();
RC522_WriteReg(RC522_BitFramingReg, 0x00);
ClearBitMask(RC522_CollReg, 0x80);
// 准备读命令(命令+块地址)
send_command[0] = PICC_READ; // 读命令(0x30)
send_command[1] = blockAddr; // 指定块地址
CalculateCRC(send_command, 2, &send_command[2]); // 计算读命令的 CRC 并附加
// 发送读命令到卡片
status = RC522_ToCard(command, send_command, 4, back_Data, &recvBits);
// 检查是否读取成功
if ((status == MI_OK) && (recvBits == 0x90)) // 数据块长度为 16 字节(128 位)
{
for (uint8_t i = 0; i < 16; i++)
{
data[i] = back_Data[i]; // 保存数据到指定缓冲区
}
}
else
{
status = MI_ERR; // 返回错误状态
}
return status;
}
6、RC522_Write(写入区块)
这个函数是向某个区块写入16字节的数据,第一个参数是区块地址,第二个参数是要写入的数据数组的首地址
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 写入指定区块的数据
// 参数说明 uint8_t blockAddr 区块地址(0-63)
// uint8_t *writeData 写入的数据指针(长度为16字节)
// 返回参数 uint8_t 写入状态 (MI_OK 或 MI_ERR)
// 使用示例 uint8_t data[16] = {0x01, 0x02, ... , 0x10};
// RC522_Write(5, data); // 写入第5区块数据
// 备注信息 确保先认证目标区块所属扇区,再调用本函数写入数据
//-------------------------------------------------------------------------------------------------------------------
uint8_t RC522_Write(uint8_t blockAddr, uint8_t *writeData)
{
uint8_t status;
uint8_t send_command[18];
uint16_t recvBits;
uint8_t backData[MAXRLEN];
// 构建写入命令
send_command[0] = PICC_WRITE; // 写入命令
send_command[1] = blockAddr; // 区块地址
CalculateCRC(send_command, 2, &send_command[2]);
// 发送写命令
status = RC522_ToCard(PCD_TRANSCEIVE, send_command, 4, backData, &recvBits);
if ((status != MI_OK) || (recvBits != 4) || ((backData[0] & 0x0F) != 0x0A))
{
status = MI_ERR; // 写命令失败
}
// 发送16字节数据
if (status == MI_OK)
{
memcpy(send_command, writeData, 16);
CalculateCRC(send_command, 16, &send_command[16]);
// 发送数据
status = RC522_ToCard(PCD_TRANSCEIVE, send_command, 18, backData, &recvBits);
if ((status != MI_OK) || (recvBits != 4) || ((backData[0] & 0x0F) != 0x0A))
{
status = MI_ERR; // 写数据失败
}
}
return status;
}
五、说明
根据官方提供的命令流程图,与IC卡建立通信需要经过寻卡—->防冲突—->选卡—->验证密钥,经过以上流程后,就可以读取或者写入区块数据,其中官方的3 Pass Authenticcation spelific sector(三次握手验证),在实际中只要验证密钥A就可以访问扇区中区块的数据了
作者:A-pezer