RFID-RC522模块快速上手(适配江科大STM32)

目录

概述

一、RC522底层通信逻辑

1、SPI接口

2、寄存器操作

3、初始化

4、寻卡流程

二、识别卡号UID

1、代码流程讲解

2、识别卡号工程效果

三、识别密码

1、S50内存结构

2、读写权限

3、验证密码流程

4、修改密码流程

5、识别密码工程效果

四、引脚分配及代码声明

1、引脚分配表

2、声明

五、演示程序效果


源代码资料包下载:通过网盘分享的文件:RFID-RC522模块.zip

链接: https://pan.baidu.com/s/1UQr64eCGWGejA3_39rgWgg?pwd=CHEN 提取码: CHEN

概述

本文主要讲解如何快速上手RC522刷卡模块,分为四个部分:RC522底层通信逻辑、RC522识别卡片卡号、RC522识别和修改卡片密码。先讲清楚卡号和密码的概念,以便选择快速上手的模板。卡号顾名思义就是每张卡的编号,每张卡都大概率不一样,如果你的项目比较简单,可以直接利用卡号来区分不同的卡,达到刷卡开锁的效果。密码存储在每张卡里固定的某个位置,需要手动读写密码,当然,先要验证密码成功后才可以修改密码,同时卡中除了密码还可以存储其他数据,这些数据也是需要先成功验证密码后才可以读写的。识别密码的版本会稍微复杂一些,可根据实际情况来选择你需要的版本。

一、RC522底层通信逻辑

这部分内容需要结合RC522的数据手册来学习,数据手册我也一同放在压缩包内。

1、SPI接口

RC522一般使用SPI进行通信,当然也同时支持I2C与串口通信,SPI使用模式0,时序图如下

根据时序图我们就可以编写底层SPI的时序函数了,这里我用的江科大的软件模拟SPI框架,实际使用时发现引脚操作之间要适当加入一点延时,否则可能会通信失败。操作引脚函数不再赘述了。

cpp 复制代码
/* 写SDA(片选引脚) */
void RC522_W_SDA(uint8_t Byte)
{ GPIO_WriteBit(RC522_GPIO_SDA_PORT, RC522_GPIO_SDA_PIN, (BitAction)Byte); }
/* 写RST(硬件复位引脚) */
void RC522_W_RST(uint8_t Byte)
{ GPIO_WriteBit(RC522_GPIO_RST_PORT, RC522_GPIO_RST_PIN, (BitAction)Byte); }
/* 写SCK */
void RC522_W_SCK(uint8_t Byte)
{ GPIO_WriteBit(RC522_GPIO_SCK_PORT, RC522_GPIO_SCK_PIN, (BitAction)Byte); }
/* 写MOSI */
void RC522_W_MOSI(uint8_t Byte)
{ GPIO_WriteBit(RC522_GPIO_MOSI_PORT, RC522_GPIO_MOSI_PIN, (BitAction)Byte); }
/* 读MISO */
uint8_t RC522_R_MISO(void)
{ return GPIO_ReadInputDataBit(RC522_GPIO_MISO_PORT, RC522_GPIO_MISO_PIN); }

下面是SPI时序函数,同时加入一定的延时。SPI模式0、数据高位先行这些在数据手册中都有标明。

cpp 复制代码
/* 开始时序 */
void RC522_SPI_Start(void)
{
	Delay_us(2);
	RC522_W_SDA(0);    //拉低片选信号
	Delay_us(2);
}

/* 停止时序 */
void RC522_SPI_Stop(void)
{
	Delay_us(2);
	RC522_W_SDA(1);    //拉高片选信号
	Delay_us(2);
}

/* 交换一个字节 */
uint8_t RC522_SPI_SwapByte(uint8_t Byte)
{
	uint8_t i;
	for(i = 0; i < 8; i++)
	{
		// SCK低电平时,置MOSI
		if(Byte & 0x80) { RC522_W_MOSI(1); }
		else            { RC522_W_MOSI(0); }
		Delay_us(2);
		
		// SCK上升沿采样MISO
		RC522_W_SCK(1);
		Delay_us(2);
		
		// 读取MISO电平,数据左移一位
		Byte <<= 1;
		if(RC522_R_MISO()) { Byte |= 0x01; }
		Delay_us(2);
		
		// SCK下降沿:准备下一位数据
		RC522_W_SCK(0);
		Delay_us(2);
	}
	return Byte;
}

2、寄存器操作

SPI时序搭建好了就是如何控制RC522了,对RC522的控制主要通过读写寄存器来实现,数据手册中也用了大量的篇幅来说明每个寄存器的作用,如果读不透全文,也起码要知道它的读写逻辑。数据手册38页写明了SPI接口的主从机配置、数据高位先行、SCK上升沿读数据,下降沿写数据,配合上文提到的时序图即可得知为SPI的模式0。

38页还写到主机读数据的逻辑,第一帧写地址,下一帧读该地址的数据,以此类推。

同理还有写数据逻辑,先写地址再写数据,可连续写。

接着往下翻39页写道地址字节的说明,最高位为1时为读该地址数据,最高位为0时为写该地址数据。中间6位为需要读写的地址,最低位标明RFU永远为0。

RFU是什么?在寄存器位概述的最后,也就是第7页有说明,为厂商保留的寄存器,我们看作固定值即可。

那么截至目前我们还不明确的就是地址位到底在哪里了,搞明白这点我们就完全理解如何与RC522通信了。我们先总览目录,可以看到厂商已经帮我们将寄存器分类好了从PAGE0-PAGE3,根据分类可以大致猜测命令、状态、通信可能是我们需要仔细研究的,配置部分可能与初始化有关,我们点击跳转到概述继续查看。

概述部分有写地址,但还不够明确,同时可以看到这部分有些每个寄存器对应的功能,我们继续下拉找寄存器的详细描述。

第7页开始寄存器的详细秒数,我们继续观察寄存器地址的规律,PAGE0的寄存器地址为0x00-0x0F,PAGE3的地址为0x30-0x3F,那所有地址也就是0x00-0x3F,正好用6位表示。

0x3F对应二进制0011_1111,那么逻辑就清晰了,先地址向左移位1位,最高位控制读写,最低位恒为0。我们先把读写寄存器的函数封装好。

cpp 复制代码
 * @brief  向RC522指定寄存器写入一个字节
 * @param  Address: 寄存器地址(0x00~0x3F)
 * @param  value:   待写入的数据
 * @note   写指令格式:bit7=0, bit6~bit1=寄存器地址, bit0=0
 */
void RC522_WriteData(uint8_t Address, uint8_t value)
{
	Address = ((Address << 1) & 0x7E);  // 地址左移1位,bit7清零(写指令)
	RC522_SPI_Start();
	RC522_SPI_SwapByte(Address);        // 发送寄存器地址+读写位
	RC522_SPI_SwapByte(value);          // 发送写入数据
	RC522_SPI_Stop();
}

/**
 * @brief  从RC522指定寄存器读取一个字节
 * @param  Address: 寄存器地址(0x00~0x3F)
 * @return 寄存器中的数据
 * @note   读指令格式:bit7=1, bit6~bit1=寄存器地址, bit0=0
 */
uint8_t RC522_ReadData(uint8_t Address)
{
	uint8_t Byte;
	Address = ((Address << 1) & 0x7E) | 0x80;  // 地址左移1位,bit7置1(读指令)
	RC522_SPI_Start();
	RC522_SPI_SwapByte(Address);                // 发送寄存器地址+读写位
	Byte = RC522_SPI_SwapByte(0xFF);            // 发空字节,同时接收数据
	RC522_SPI_Stop();
	return Byte;
}

接下来再读寄存器的详解,发现寄存器多且复杂,往往一个寄存器中的一个位就控制了一个功能,我并没有读懂,所以找了其他示例代码来作为参考并整理。这里我们还要再建立对RC522某一寄存器的某一位清零和置位的函数,方便后续操作。

cpp 复制代码
/**
 * @brief  置位寄存器指定bit
 * @param  reg:  寄存器地址
 * @param  mask: 需要置1的位掩码
 */
static void SetBitMask(uint8_t reg, uint8_t mask)
{
    uint8_t tmp = RC522_ReadData(reg);
    RC522_WriteData(reg, tmp | mask);
}

/**
 * @brief  清零寄存器指定bit
 * @param  reg:  寄存器地址
 * @param  mask: 需要清0的位掩码
 */
static void ClearBitMask(uint8_t reg, uint8_t mask)
{
    uint8_t tmp = RC522_ReadData(reg);
    RC522_WriteData(reg, tmp & ~mask);
}

3、初始化

通过研究网上其他的源代码可以发现,RC522的初始化必须要先硬件复位拉高RST引脚,然后配置基础寄存器,最后开启射频天线。实测最后不开启射频天线也可以正常工作,但保险起见还是加上。记得在你的RC522_Init()函数中加上这些初始化程序。

cpp 复制代码
//复位引脚失能,模块开始工作
RC522_W_RST(1);
// 基础寄存器配置:Mifare模式、定时器、CRC初始值等
RC522_WriteData(ModeReg, 0x3D);        // CRC初始值0x6363,Mifare模式
RC522_WriteData(TReloadRegL, 30);      // 定时器重载值低字节
RC522_WriteData(TReloadRegH, 0);       // 定时器重载值高字节
RC522_WriteData(TModeReg, 0x8D);       // 定时器自动重装载
RC522_WriteData(TPrescalerReg, 0x3E);  // 定时器预分频
RC522_WriteData(TxAutoReg, 0x40);      // 自动检测天线阻抗
RC522_AntennaOn();  // 开启射频天线

4、寻卡流程

初始化过后模块开始正常工作,但仍需调用寻卡函数来识别卡片,寻卡时自动传回对应卡的卡号,而如果要读写卡内部的数据还需要进行数据交互的确认和密码的验证,每张卡出厂时便自带了密码。所以接下来的部分按照你的实际情况来选择即可,如果你仅需要区分几张卡片的话看第二部分识别卡号的代码即可,如果你想要设置密码或者向卡内写入数据,那么就看第三部分识别密码的代码。

二、识别卡号UID

1、代码流程讲解

注:这里只做大概流程的讲解,详细代码请看链接资源包中的完整工程。

识别卡号的流程就相对来说简单很多,调用寻卡函数,然后再调用防冲突检测读取卡号函数即可。

cpp 复制代码
/**
 * @brief  寻卡:检测感应区内的卡片
 * @param  req_code: 寻卡方式 PICC_REQIDL/PICC_REQALL
 * @param  pTagType: 输出卡片类型码(2字节)
 * @return 状态码 MI_OK=寻卡成功
 */
uint8_t RC522_Request(uint8_t req_code, uint8_t *pTagType)
/**
 * @brief  防冲突检测,读取卡片UID
 * @param  pSnr: 输出卡片序列号(4字节UID + 1字节BCC校验,共5字节)
 * @return 状态码 MI_OK=成功
 */
uint8_t RC522_Anticoll(uint8_t *pSnr)

实际使用的话直接按这个流程即可,UID就自动被存放在Buf数组里了,非常方便。

cpp 复制代码
//调用示例
status = RC522_Request(PICC_REQALL, Buf);//寻卡
if (status == MI_OK)	//成功寻到卡
{
    Delay_ms(100);
	status = RC522_Anticoll(Buf);    //防冲突读取卡序列号
}

无论是寻卡函数还是读取卡号函数底层逻辑都是调用RC522_ToCard函数让RC522与卡片交互数据,区别在于对RC522的配置以及传入卡片的InData命令不同。我们只做配置和命令,数据是由RC522与卡片自动进行交互的。所以这也是RC522使用方法如此晦涩难懂的原因。

cpp 复制代码
/**
 * @brief  通过RC522与ISO14443A卡片进行数据交互
 * @param  Command:    RC522命令字(PCD_AUTHENT / PCD_TRANSCEIVE)
 * @param  pInData:    发送给卡片的数据缓冲区
 * @param  InLenByte:  发送数据的字节长度
 * @param  pOutData:   接收卡片返回数据的缓冲区
 * @param  pOutLenBit: 接收数据的位长度(输出参数)
 * @return 状态码 MI_OK=成功,其他为失败
 */
static uint8_t RC522_ToCard(uint8_t Command,
                            uint8_t *pInData,
                            uint8_t InLenByte,
                            uint8_t *pOutData,
                            unsigned int *pOutLenBit)

2、识别卡号工程效果

烧录资源包中的 "RC522识别卡号" 工程后,板载LED闪烁表示程序正常运行,模块每隔1s寻卡一次,把卡放到模块上,屏幕第三行显示对应卡号。

三、识别密码

注:这里只做大概流程的讲解,详细代码请看链接资源包中的完整工程。

识别密码首先要了解卡内部的数据存储结构,我们买模块商家赠送的白色卡片和蓝色复旦卡都属于M1卡类型,大部分型号都是1K的内存容量,该型号被称为S50。

1、S50内存结构

S50一般都将总内存划分为16个"扇区",每个扇区分为4个"块",按顺序4个块(块0~块3)中的最后一个块(块3)被称为尾块,尾块存储密码等数据,前三个块存单纯的用户数据。每个块存储16字节数据,一个字节为8bit,8位二进制数。

搞明白卡内部有几个扇区、几个块、每个块几字节、扇区和块的对应关系后,我们就要来了解最特殊的"尾块"了,每个扇区中的块3都为该扇区的尾块,内部存储密码和控制位。尾块中的高6位称作KEYA,低6位称作KEYB。中间4位为卡片的存取控制位,我们不做了解也禁止修改这4位,否则可能导致该扇区锁死。1字节也就是1Byte也就是8位二进制数,也就是uint8_t等于1Byte。

2、读写权限

好的,那么接下来我们要了解KEYA和KEYB的权限。我们验证密码就是验证的KEYA和KEYB,无论验证KEYA还是KEYB成功之后都可以读写前3个块区的数据 ,前三个块就是正常的存储空间因此也被称为数据块。验证KEYA和KEYB的区别在于对尾块的操作权限,成功验证KEYA即获得管理员权限,可以读出和写入尾块中的"读取控制位"和"KEYB"。而"KEYA"永远无法被读取,读出来的永远是0x000000而不是真实值,但可以写入新的"KEYA"。成功验证KEYB仅获得用户权限,仅能够读取(不可写入)尾块中的"读取控制位",而"KEYA"和"KEYB"都无法读取到真实值,同时也不可写入新的值。验证KEYA和KEYB成功后的读写权限如下," r " 表示可以读取到真实值(不可读取则读到的数全为0), " w " 表示可以写入新的值(不可写入则写入的数不会起任何效果), " - " 表示不可读不可写。

|----------|------------|----------|-----------|----------|
| 验证密码 | 块0~块2 | KEYA | 读取控制位 | KEYB |
| KEYA | r/w | w | r/w | r/w |
| KEYB | r/w | - | r | - |

同时值得注意的是,每个扇区的密码是独立的,验证一个扇区的密码后仅能对该扇区的块进行操作,其他扇区还是上锁的状态。第一个扇区的第一个块,也就是扇区0的块0存储着这张卡的出厂信息,无论什么权限都只可读不可写。所以我们尽量使用后面的扇区,不要使用扇区0。

那么现在我们就对卡的密码和内部数据有了一个大概的了解。KEYA不可修改,所以密码其实就是KEYB,那么想要修改密码、读取密码就需要使用KEYA验证,拿到最高读写选取从而读写KEYB。而仅仅只是验证密码或读写前三个块的普通数据的话就使用KEYB验证,如果函数返回值为MI_OK即为密码正确,可以读写前三个块的数据;反之则密码错误,什么也干不了。

权限类型AUTH_ADMIN即为管理员权限,对应密钥为KEYA;权限类型AUTH_USAR即为用户权限,对应密钥为KEYB,也就是我们的密码。

cpp 复制代码
/**
 * @brief  验证指定扇区块的密码
 * @param  auth_mode: 权限类型 AUTH_ADMIN / AUTH_USER
 * @param  sector:    扇区号 0~15
 * @param  block:     扇区内块号 0~3,可用 BLOCK_DATAx / BLOCK_TRAIL(尾块)
 * @param  pKey:      权限对应的6字节密钥
 * @param  pSnr:      卡片UID(4字节)
 * @return 状态码 MI_OK=验证通过
 * @note   同个扇区任意一块验证通过后,整个扇区都处于授权状态,无需重复验证
 */
uint8_t RC522_AuthState(uint8_t auth_mode, uint8_t sector, uint8_t block, uint8_t *pKey, uint8_t *pSnr)

3、验证密码流程

仅验证密码的流程为先调用寻卡函数,寻到卡后调用获取卡号函数,然后调用数据交互函数选定该卡片,然后调用验证扇区块密码函数,参数为用户权限(KEYB),扇区1,尾块(块3),KEYB对应的密钥(6字节数组),对应卡号。判断返回值,验证成功后即可读取前三块的数据了。最后使用完要让卡片进入休眠状态。

cpp 复制代码
status = RC522_Request(PICC_REQALL, card_uid);	//寻卡
if(status == MI_OK)		//寻到卡
{
    status = RC522_Anticoll(card_uid);		//获取卡号UID
	status = RC522_SelectTag(card_uid);		//选定卡片进行数据交互
    //用KEYB(普通权限)验证
    status = RC522_AuthState(AUTH_USER, 1, BLOCK_TRAIL, card_password, card_uid);
	if(status == MI_OK)	//验证成功
    {
        //读写前三块的数据
        // ......
        RC522_Halt();		//使用完卡片后让卡片进入休眠状态
    }
}

4、修改密码流程

前面的流程与验证密码一致,到验证密码函数时,参数为管理员权限(KEYA),扇区1,尾块(块3),KEYA对应的密钥(6字节数组,KEYA出厂默认0xFFFFFFFFFFFF),对应卡号。判断返回值,验证成功后即可获得最高读取权限。然后读取尾块16字节数据,注意这里读出来的KEYA永远是0(假数据)。然后我们手动构造完整尾块数据,高6字节写入我们前面验证的KEYA密钥,低6字节写入我们新的KEYB对应的密码,中间4字节千万不要动。然后向卡片回写完整的尾块数据,参数为扇区1,尾块(块3),要写入的16字节数组的头指针。最后还是让卡片进入休眠状态。这样所有数据都没变,唯独改变了KEYB的数据,完成了更换密码的操作。

cpp 复制代码
status = RC522_Request(PICC_REQALL, card_uid);	//寻卡
if(status == MI_OK)		//寻到卡
{
    status = RC522_Anticoll(card_uid);		//获取卡号UID
	status = RC522_SelectTag(card_uid);		//选定卡片进行数据交互
    //用KeyA验证(只有KeyA有权限修改密码)
    status = RC522_AuthState(AUTH_ADMIN, 1, BLOCK_TRAIL, Admin_KeyA, card_uid);	
	if(status == MI_OK)	//验证成功
    {
        RC522_ReadBlock(1, BLOCK_TRAIL, block_buf);		//读出完整尾块16字节数据
					
		//手动构造完整尾块:真实KeyA + 原存取控制 + 新KeyB
        // 前6字节:手动填真实KeyA,绝对不能用读出来的
		memcpy(block_buf + 0, Admin_KeyA, 6);
        // 中间4字节:保留读出来的原值,不动
		memcpy(block_buf + 10, card_password, 6); // 后6字节:新密码
					
		RC522_WriteBlock(1, BLOCK_TRAIL, block_buf);	//回写尾块数据
		OLED_ShowString(1, 14, "OK");		//显示修改成功
        RC522_Halt();		//使用完卡片后让卡片进入休眠状态
    }
}

5、识别密码工程效果

烧录资源包中的 "RC522识别密码" 工程后,放上卡片,按下Key1后修改该卡密码为123456,修改成功后右上角显示OK,第二行显示UID。放上卡片,按下Key2后识别卡片密码(KEYB)是否为123456,验证成功第三行显示OK,第四行显示密码。按下Key3后刷新屏幕显示。

四、引脚分配及代码声明

1、引脚分配表

|----------|----------|
| LED | PC13 |
| Key1 | PB11 |
| Key2 | PB10 |
| Key3 | PB1 |

|----------|-----------|
| OLED | STM32 |
| SCL | PB8 |
| SDA | PB9 |

|-----------|-----------|
| RC522 | STM32 |
| SDA | PB0 |
| SCK | PA7 |
| MOSI | PA6 |
| MISO | PA5 |
| RST | PA4 |

2、声明

该代码并非完全原创,按键使用的是江科大的非阻塞式单击按键代码框架,我进行了一些改动,RC522程序部分参照网上的开源资料,OLED代码同样使用的是江科大STM32教程的代码,我进行了兼容中文的改造,显示字符串可以混合显示中英文(UTF-8)。引脚定义都在.h文件当中,按键使用低电平触发,RC522的IRQ引脚悬空。

五、演示程序效果

上电初始化完成后,OLED 屏幕居中显示 "演示程序",设备处于锁定待机状态,此时只有 Key3 按键可响应,按下 Key3 即可进入功能选择主菜单。

进入主菜单后,屏幕从上到下依次显示 LOCK、Add Card、Delete Card、View Card 四个选项,左侧以 ">" 光标标记当前选中项。按下 Key1 光标向上移动,到达顶部后循环跳转至最底部;按下 Key2 光标向下移动,到达底部后循环跳转至最顶部;按下 Key3 则进入当前光标选中的对应功能。

选中 LOCK 选项并确认后,设备会退回锁定待机界面,屏幕显示 "HELLO!"。

选中 Add Card 选项进入添加授权卡功能后,第一行显示 Back 返回项,第二行默认显示 "Place Card" 提示放置卡片,光标默认停留在第二行。设备会自动周期性检测卡片,识别到 IC 卡后,第二行切换显示卡片的 UID 编号,同时第三行出现 Add 选项、第四行出现 Rescan 选项。选中 Add 并确认后,当前卡片会被加入授权列表,重复卡片会被自动跳过,操作完成后界面回到放卡提示状态,光标自动复位到第二行;选中 Rescan 并确认后,清除当前卡号显示,重新进入寻卡等待状态,光标复位到第二行;选中 Back 并确认后,返回主菜单,光标自动定位在 Add Card 选项上。

选中 Delete Card 选项进入删除授权卡功能后,第一行显示 Back 返回项,下方三行依次展示 Card1、Card2、Card3 三个授权卡槽的卡号,空卡槽显示为 "----",光标默认停留在第二行。移动光标选中对应卡槽并确认后,该卡槽内的授权卡号会被删除,界面同步刷新卡槽状态为空。选中 Back 并确认后,返回主菜单,光标自动定位在 Delete Card 选项上。

选中 View Card 选项进入查看卡片功能后,第一行显示 Back 返回项,第二行显示 "Scanning...",光标默认停留在第二行。设备自动周期性检测卡片,识别到卡片后,第三行实时显示卡片 UID,第四行同步显示授权校验结果:已授权卡片显示 "Auth: OK",未授权卡片显示 "Auth: NO"。未检测到新卡片时,屏幕保持上一次的识别结果不变。选中 Back 并确认后,返回主菜单,光标自动定位在 View Card 选项上。

设备运行全程,板载 LED 以 500 毫秒为周期交替闪烁,用于指示系统处于正常运行状态。