MFRC522是高度集成的非接触式(13.56MHz)读写卡芯片,此发送模块利用调制和调节的原理,并将它们完全集成到各种非接触式通信方法和协议中。它支持ISO14443A/MIFARE。关于电路的工作原理,我也不能详解,大家可以自己查资料,这里仅针对通信过程做一个详细的介绍,希望对看到的人有所帮助。
好言归正传,关于RC522的程序我买这个小模块时,淘宝店家给的程序,应该也是一个大家通用的程序。我以此为模版,移植到我的STM32上面。(不过后来发现网上已经有基于stm32的例程了。)
首先来了解RC522的数字通信接口问题。RC522支持SPI、I2C和UART接口。我手里的模块使用的是SPI接口。那么,使用不同的通信接口是如何选择的呢?在RC522的数据手册中给出了检测方式。(实际上就是通过检测I2C与EA管脚状态进行识别)。
RC522的SPI总线接口有其自身的时序要求。它只能工作于从模式,最高传输速率为10 Mbps,数据与时钟相位关系满足“空闲态时钟为低电平,在时钟上升沿同步接收和发送数据,在下降沿数据转换”的约束关系。
本文中配置STM32的SPI1工作于主模式下,时钟小于10Mbps,接收和发送数据都在时钟上升沿发生。提前说一下,针对SPI的时钟,通过SPI_InitStructure.SPI_BaudRatePrescaler= SPI_BaudRatePrescaler_n;这句话来配置,其中n=2、4、8、16、32、64、128、256.SPI1的输入时钟是72MHz,72/8=9<10,所以n的取值必须要大于8,我在开始调试程序时,使用的SPI_BaudRatePrescaler_8,但调试发现,只能实现read功能,其它功能都报错。后来改成SPI_BaudRatePrescaler_256.然后其它功能都正常工作了。所以通信速率低一点好像更好!
接着看RC522程序的初始化。
PcdReset();
PcdAntennaOff();
PcdAntennaOn();
M500PcdConfigISOType( 'A' );
首先是PcdReset();函数,函数使用RC522的RESET引脚配合PCD_RESETPHASE复位命令实现对于RC522芯片的复位操作。然后通过PcdAntennaOff();函数关闭天线,通过PcdAntennaOn();函数打开天线,相当于对天线的初始化。最后M500PcdConfigISOType( 'A' );针对ISO14443A型卡进行初始化。程序比较多,都是固定的格式,这里就不再详细叙述。
接下来就是PICC的识别过程。
简单点说的话就是
寻卡---------->防冲突---------->选卡--------->操作卡
在开始寻卡之前我们先说一些用的知识
在对卡进行操作时,主要用到两类命令 它们分别是:
//RC522芯片的操作命令。
#definePCD_IDLE 0x00 //取消当前命令
#definePCD_AUTHENT 0x0E //验证密钥
#definePCD_RECEIVE 0x08 //接收数据
#definePCD_TRANSMIT 0x04 //发送数据
#definePCD_TRANSCEIVE 0x0C //发送并接收数据
#definePCD_RESETPHASE 0x0F //复位
#definePCD_CALCCRC 0x03 //CRC计算
//Mifare_One卡片命令字
#definePICC_REQIDL 0x26 //寻天线区内未进入休眠状态
#definePICC_REQALL 0x52 //寻天线区内全部卡
#definePICC_ANTICOLL1 0x93 //防冲撞
#definePICC_ANTICOLL2 0x95 //防冲撞
#definePICC_AUTHENT1A 0x60 //验证A密钥
#definePICC_AUTHENT1B 0x61 //验证B密钥
#definePICC_READ 0x30 //读块
#definePICC_WRITE 0xA0 //写块
#definePICC_DECREMENT 0xC0 //扣款
#definePICC_INCREMENT 0xC1 //充值
#definePICC_RESTORE 0xC2 //调块数据到缓冲区
#definePICC_TRANSFER 0xB0 //保存缓冲区中数据
#definePICC_HALT 0x50 //休眠
PCD是接近式卡。PICC是接近式耦合设备。在通信过程中实际上是使用PCD命令控制RC522发出PICC命令与卡进行交互。关于Mifare1卡的说明请自行查看晚上资料,需要说明的是搞懂卡片的存储器组织结构和读写控制对于理解卡片的工作原理非常有好处,大家要认真看。
好,接下来我们来看一下PICC的寻卡过程。
首先,通过PcdRequest(PICC_REQIDL, &CardRevBuf[0] )函数我们进行寻卡操作。它内部的操作函数是
charPcdRequest(unsigned char req_code,unsigned char *pTagType)
{
char status;
unsigned int unLen;
unsigned char ucComMF522Buf[MAXRLEN];
ClearBitMask(Status2Reg,0x08);//寄存器包含接收器和发送器和数据模式检测器的状态标志
WriteRawRC(BitFramingReg,0x07);//不启动数据发送
SetBitMask(TxControlReg,0x03);//TX1、TX2输出信号将传递经发送数据调制的13.56MHz的能量载波信号。
ucComMF522Buf[0] = req_code;
status =PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,1,ucComMF522Buf,&unLen);//通过522发送req_code命令,并接收返回数据,存到ucComMF522Buf中
if ((status == MI_OK) && (unLen== 0x10))//这个为啥是0x10,因为是2个字节共16bit
{
*pTagType =ucComMF522Buf[0];
*(pTagType+1) =ucComMF522Buf[1];//获取卡类型
}
else
{
status = MI_ERR;
}
return status;
}
函数将我们的寻卡命令PICC_REQIDL装填如要发送的数组,通过PcdComMF522函数发送出去,如果此时在PCD有效范围内没有寻找到卡,则函数返回MI_ERR,若函数返回MI_OK,并且ulen为0x10(16bit)为两个字节则说明寻卡成功,返回的两字节被装填入CardRevBuf数组。实际上这两个数组表示的是所寻到卡的类型,它们与字节的对应关系如下
// 0x4400 = Mifare_UltraLight
// 0x0400 = Mifare_One(S50)
// 0x0200 = Mifare_One(S70)
// 0x0800 = Mifare_Pro(X)
// 0x4403 = Mifare_DESFire
另外再说一下寻卡的命令有两个分别是
#definePICC_REQIDL 0x26 //寻天线区内未进入休眠状态
#definePICC_REQALL 0x52 //寻天线区内全部卡
它们两个有什么区别呢?
第一条命令是读取完卡后还会再次读取。(除非在某次读取完成后系统进入休眠(Halt))。第二条命令是读取完卡后会等待卡离开开线作用范围,直到再次进入。
如果寻卡成功后,程序将进入防冲突操作。PcdAnticoll(&CardRevBuf[2] )这个函数用于防冲突操作。它内部的操作函数是
charPcdAnticoll(unsigned char *pSnr)
{
char status;
unsigned char i,snr_check=0;
unsigned int unLen;
unsigned char ucComMF522Buf[MAXRLEN];
ClearBitMask(Status2Reg,0x08); //寄存器包含接收器和发送器和数据模式检测器的状态标志
WriteRawRC(BitFramingReg,0x00);//不启动数据发送,接收的LSB位存放在位0,接收到的第二位放在位1,定义发送的最后一个字节的位数为8
ClearBitMask(CollReg,0x80);//所有接收的位在冲突后将被清除。
ucComMF522Buf[0] = PICC_ANTICOLL1;
ucComMF522Buf[1] = 0x20;
status =PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,2,ucComMF522Buf,&unLen);
if (status == MI_OK)
{
for (i=0; i<4; i++)
{
*(pSnr+i) = ucComMF522Buf[i];
snr_check ^=ucComMF522Buf[i];
}
if (snr_check !=ucComMF522Buf[i])//返回四个字节,最后一个字节为校验位
{
status =MI_ERR;
}
}
SetBitMask(CollReg,0x80);
return status;
}
防冲突操作就是将防冲突命令通过PcdComMF522函数与PICC卡进行交互。防冲突命令是两个字节,其中第一字节为Mifare_One卡的防冲突命令字PICC_ANTICOLL1 (0x93),第二个字节为0x20。关于这两个字节在ISO14443中有解释,这里做一下介绍。
在防冲突环节使用的命令由连个字节组成:
选择代码SEL(1个字节)。 SEL规定了串联级别CLn。
有效位的数目NVB(1个字节)。NVB规定了PCD所发送的CLn的有效位的数目。
注:只要NVB没有规定40个有效位,若PICC保持在READY状态中,该命令就被称为ANTICOLLISION命令。如果NVB规定了UID CLn的40个数据位(NVB=‘70’),则应添加CRC_A。该命令称为SELECT命令。如果PICC已发送了完整的UID,则它从READY状态转换到ACTIVE状态并在其SAK-响应中指出UID(唯一标识符)完整。否则,PICC保持在READY状态中并且该PCD应以递增串联级别启动一个新的防冲突环。
SEL的编码为
NVB的编码(有效比特的数)
所以我们选择了SEL为093表明串联级别1,NVB为0x20表明PCD发送字节数为整两个字节。该值定义了该PCD将不发送UID CLn的任何部分。因此该命令迫使工作场内的所有PICC以其完整的UID CLn表示响应。
对于不同串联级别PCD发送命令与PICC的响应如下图所示,我们主要看single那一列就好。
当我们发送93与0x20后,PICC返回5个字节其中前4个字节是UID,最后一个字节是校验它是4个先前字节的“异或”值。
对于多个卡片同时进入有效区域的防冲突操作:由于是非接触式的,同一时间天线作用范围内可能不只一张卡时,即有多于一张的MIFARE 1卡发回了卡序列号应答,则发生了冲突。此时,由于每张卡的卡序列号各不相同,MCM接收到的信息(即卡序列号)至少有1位既是0又是1(即该位的前、后半部都有副载波调制),MCM找到第1个冲突位将其置1(排除该位为0的卡),然后查第2个,依次排除,最后不再有冲突的SN即为被选中的卡。具体的过程在后面叙述
接下来是进行选卡操作。选卡操作执行的函数为PcdSelect( &CardRevBuf[2] )。它具体的操作过程是
char PcdSelect(unsigned char *pSnr)
{
char status;
unsigned char i;
unsigned int unLen;
unsigned char ucComMF522Buf[MAXRLEN];
ucComMF522Buf[0] = PICC_ANTICOLL1;
ucComMF522Buf[1] = 0x70;
ucComMF522Buf[6] = 0;
for (i=0; i<4; i++)
{
ucComMF522Buf[i+2] = *(pSnr+i);
ucComMF522Buf[6] ^= *(pSnr+i);
}
CalulateCRC(ucComMF522Buf,7,&ucComMF522Buf[7]);//计算CRC装填至ucComMF522Buf[7]
ClearBitMask(Status2Reg,0x08);//寄存器包含接收器和发送器和数据模式检测器的状态标志
status = PcdComMF522(PCD_TRANSCEIVE,ucComMF522Buf,9,ucComMF522Buf,&unLen);
if ((status == MI_OK) && (unLen == 0x18))//成功,返回SAK,包括1字节的SAK和2字节的CRC_A
{
status = MI_OK;
}
else
{
status = MI_ERR;
}
return status;
}
上面有说道,当NVB为0x70时(7个字节包含2个字节命令,4个字节UID和一个字节异或校验字节),在后面添加2字节CRC_A则为选择操作。所以通过PcdComMF522命令总共发送9个字节。当选择成功,即UID相配时,PICC以SAK(选择确认)来响应,所谓选择确认就是
PCD应校验位b3以判定UID是否完整。位b3和b6的编码在表7-8中给出。
如果UID不完整,PICC应保持READY状态并且PCD应以递增的串联级别来初始化新的防冲突环。如果UID完整,PICC应发送带有清空的串联比特的SAK并从READY状态转换到ACTIVE状态。当提供了附加信息时,PICC应设置SAK的第6位b6。
在ISO14443中说明的整个防冲突和选择环节的过程为
步骤1:PCD为选择的防冲突类型和串联级别分配了带有编码的SEL。
步骤2:PCD分配了带有值为‘20’的NVB。
步骤3:PCD发送SEL和NVB。
步骤4:工作场内的所有PICC应使用它们的完整的UID CLn响应。
步骤5:假设场内的PICC拥有唯一序列号,那么,如果一个以上的PICC响应,则冲突发生。如果没有冲突发生,则步骤6到步骤10可被跳过。
步骤6:PCD应识别出第一个冲突的位置。
步骤7:PCD分配了带有值的NVB,该值规定了UID CLn有效比特数。这些有效位应是PCD所决定的冲突发生之前被接收到的UID CLn的一部分再加上(0)b或(1)b。典型的实现是增加(1)b。
步骤8:PCD发送SEL和NVB,后随有效位本身。
步骤9:只有PICC的UID CLn中的一部分等于PCD所发送的有效位时,PICC才应发送其UID CLn的其余部分。
步骤10:如果出现进一步的冲突,则重复步骤6~9。最大的环数目是32。
步骤11:如果不出现进一步的冲突,则PCD分配带有值为‘70’的NVB。
步骤12:PCD发送SEL和NVB,后随UID CLn的所有40个位,后面又紧跟CRC_A校验和。
步骤13:它的UID CLn与40个比特匹配,则该PICC以其SAK表示响应。
步骤14:如果UID完整,则PICC应发送带有清空的串联级别位的SAK,并从READY状态转换到ACTIVE状态。
步骤15:PCD应检验SAK(选择确认)的串联比特是否被设置,以决定带有递增串联级别的进一步防冲突环是否应继续进行。
如果PICC的UID是已知的,则PCD可以跳过步骤2~10来选择该PICC,而无需执行防冲突环。
接下就就是进行读写卡操作了。
在根据图N中所示,在进行读卡的操作前要对要进行操作的扇区进行密码验证操作。
即PcdAuthState( PICC_AUTHENT1A, 4, CardKeyABuf, &CardRevBuf[2] )
验证对应扇区的KEYA是否与对应扇区的尾块中的KEYA相同。即三轮认证。
MIFARE 1 卡的密码认证方式:
三次相互认证的令牌原理框图
(A) 环:由MIFARE 1卡片向读写器发送一个随机数据RB。
(B) 环:由读写器收到RB后向MIFARE 1卡片发送一个令牌数据TOKEN AB,其中包含了用读写器中存放的密码加密后的RB及读写器发出的一个随机数据RA。
(C) 环:MIFARE 1卡片收到 TOKEN AB 后,用卡中的密码对TOKEN AB的加密的部分进行解密得到RB',并校验第一次由(A)环中MIFARE 1卡片发出去的随机数RB是否与(B)环中接收到的TOKEN AB中的RB'相一致;若读写器与卡中的密码及加密/解密算法一致,将会有RB=RB',校验正确,否则将无法通过校验。
(D) 环:如果(C)环校验是正确的,则MIFARE 1卡片用卡中存放的密码对RA加密后发送令牌TOKEN BA给读写器。
(E) 环:读写器收到令牌TOKEN BA后,用读写器中存放的密码对令牌TOKEN BA中的RA(随机数)进行解密得到RA';并校验第一次由(B)环中读写器发出去的随机数RA是否与(D)环中接收到的TOKEN BA中的RA' 相一致;同样,若读写器与卡中的密码及加密/解密算法一致,将会有RA=RA',校验正确,否则将无法通过校验。
如果上述的每一个环都为“真”,且都能正确通过验证,则整个的认证过程将成功。读写器将允许对刚刚认证通过的卡片上的这个扇区进入下一步的操作(读/写等操作)。
认证通过后即可进行读写操作
PcdRead( 4, CardReadBuf )//4为数据块,是第一扇区的第0块
PcdWrite( 4, CardWriteBuf ) //4为数据块,是第一扇区的第0块
在使用读写函数时,我看函数内部发现,每次读写完成都会返回4个字节的应答,并且第一个字节是低4位是1010。我上网查了一些东西也没有查出来,有知道的朋友可以知会一声
以上即为PICC与PCD简单的通讯过程的简介。
另外关于KEYA与KEYB的使用,及数值块与读写块的问题可参见《Mifare 1非接触IC卡技术说明》以及http://blog.chinaunix.net/uid-22683402-id-1771315.html中的介绍。
本文的参考资料有
1、ISO14443协议英文版
2、ISO14443协议中文版
3、Mifare1技术说明(M1卡说明文档)
4、MFRC522中文
5、有关S50的AB密钥浅谈
个人整理资料,难免有错误的地方,欢迎批评指正!