2. YH-RC522模块

2.1. YH-RC522 简介

YH-RC522 是野火设计的一款高度集成的非接触式(13.56MHz)读写卡芯片。它采用 了 NXP 公司的 MFRC522 为核心的处理芯片,此发送模块利用调制和解调的原理,支持各 种非接触式的通信协议。

详细的YH-RC522模块内容请参考:https://doc.embedfire.com/products/link/zh/latest/module/wireless_misc/rfid_rc522.html?highlight=rc522

YH-RC522模块

2.2. 产品特性参数

YH-RC522 模块产品特性

YH-RC522模块产品特性

特性

说明

读写器

支持ISO 14443A/MIFARE

通信方式

SPI通信

读写器模式下通信距离

50mm

工作电压

3.3V

2.3. YH-RC522 模块的引脚说明

YH-RC522模块的引脚说明

编号

名称

说明

1

VCC

电源正

2

RST

复位

3

IRQ

中断信号

4

GND

地线

5

MISO

主进从出数据引脚

6

MOSI

从进主出数据引脚

7

SCK

时钟

8

SDA

片选

2.4. YH-RC522 传感器工作原理

YH-RC522 是采用的一种先进的 RFID(Radio Fequency Identification,中文为无线射频识别)通信技术。 其工作原理其实很简单:ID 磁卡进入到磁场后,接受读写器发出的射频信号, 凭借感应电流所获得的能量发送出存储在芯片中的产品信息,读写器读取到信息并解码后,送至处理单元进行数据处理。

2.5. 实验现象

通过上面的上面的引脚编号连接到开发板,下载程序到开发板中,当没有 IC 卡靠近也就是未检测到IC卡时,串口会一直打印 “寻卡失败”的提示信息,当用 IC 卡靠近RC522时,串口会打印卡 ID 和余额,此时不松开卡的同时,按下开发板上的 KEY1 / KEY2 可以让余额增加/减少 100。

rc522_1

2.6. 例程介绍

本例程采用的软件SPI,即用IO口模拟SPI协议进行通信,软件SPI在《SPI基础知识》一文已经介绍,不再赘述,这里主要分析代码框架。

由于篇幅问题,这里只贴出部分代码,完整代码请参考YH-RC522模块例程。

在编写YH-RC522模块驱动时,也要考虑更改硬件环境的情况。我们把YH-RC522模块引脚相关的宏定义到 rc522_config.h”文件中, 在更改或移植的时候只用改宏定义就可以。

rc522_config.h(霸道开发板例程)
 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
// ... ...

/*********************************** RC522 引脚定义 *********************************************/
//RC522模块有除了电源还有6个数据引脚,其中IRQ不需要使用,悬空即可,剩下的5个数据引脚连接如下:
//如果RC522需要修改与STM32的连接,则修改这些IO即可,但必须连接到STM32的SPI引脚
//片选,即RC522模块的SDA引脚
#define               RC522_GPIO_CS_CLK_FUN                  RCC_APB2PeriphClockCmd
#define               RC522_GPIO_CS_CLK                      RCC_APB2Periph_GPIOA
#define               RC522_GPIO_CS_PORT                     GPIOA
#define               RC522_GPIO_CS_PIN                      GPIO_Pin_4
#define               RC522_GPIO_CS_Mode                     GPIO_Mode_Out_PP

//时钟,即RC522模块的SCK引脚,接STM32的SPI的SCK引脚
#define               RC522_GPIO_SCK_CLK_FUN                 RCC_APB2PeriphClockCmd
#define               RC522_GPIO_SCK_CLK                     RCC_APB2Periph_GPIOB
#define               RC522_GPIO_SCK_PORT                    GPIOB
#define               RC522_GPIO_SCK_PIN                     GPIO_Pin_13
#define               RC522_GPIO_SCK_Mode                    GPIO_Mode_Out_PP

// 数据输入,即即RC522模块的MOSI引脚,接STM32的SPI的MOSI引脚
#define               RC522_GPIO_MOSI_CLK_FUN                RCC_APB2PeriphClockCmd
#define               RC522_GPIO_MOSI_CLK                    RCC_APB2Periph_GPIOB
#define               RC522_GPIO_MOSI_PORT                   GPIOB
#define               RC522_GPIO_MOSI_PIN                    GPIO_Pin_15
#define               RC522_GPIO_MOSI_Mode                   GPIO_Mode_Out_PP

// 数据输出,即即RC522模块的MISO引脚,接STM32的SPI的MISO引脚
#define               RC522_GPIO_MISO_CLK_FUN                RCC_APB2PeriphClockCmd
#define               RC522_GPIO_MISO_CLK                    RCC_APB2Periph_GPIOB
#define               RC522_GPIO_MISO_PORT                   GPIOB
#define               RC522_GPIO_MISO_PIN                    GPIO_Pin_14
#define               RC522_GPIO_MISO_Mode                   GPIO_Mode_IN_FLOATING

//复位,即即RC522模块的RST引脚,接STM32的普通IO即可
#define               RC522_GPIO_RST_CLK_FUN                 RCC_APB2PeriphClockCmd
#define               RC522_GPIO_RST_CLK                     RCC_APB2Periph_GPIOB
#define               RC522_GPIO_RST_PORT                    GPIOB
#define               RC522_GPIO_RST_PIN                     GPIO_Pin_8
#define               RC522_GPIO_RST_Mode                    GPIO_Mode_Out_PP


/*********************************** RC522 函数宏定义*********************************************/
#define          RC522_CS_Enable()         GPIO_ResetBits ( RC522_GPIO_CS_PORT, RC522_GPIO_CS_PIN )
#define          RC522_CS_Disable()        GPIO_SetBits ( RC522_GPIO_CS_PORT, RC522_GPIO_CS_PIN )

#define          RC522_Reset_Enable()      GPIO_ResetBits( RC522_GPIO_RST_PORT, RC522_GPIO_RST_PIN )
#define          RC522_Reset_Disable()     GPIO_SetBits ( RC522_GPIO_RST_PORT, RC522_GPIO_RST_PIN )

#define          RC522_SCK_0()             GPIO_ResetBits( RC522_GPIO_SCK_PORT, RC522_GPIO_SCK_PIN )
#define          RC522_SCK_1()             GPIO_SetBits ( RC522_GPIO_SCK_PORT, RC522_GPIO_SCK_PIN )

#define          RC522_MOSI_0()            GPIO_ResetBits( RC522_GPIO_MOSI_PORT, RC522_GPIO_MOSI_PIN )
#define          RC522_MOSI_1()            GPIO_SetBits ( RC522_GPIO_MOSI_PORT, RC522_GPIO_MOSI_PIN )

#define          RC522_MISO_GET()          GPIO_ReadInputDataBit ( RC522_GPIO_MISO_PORT, RC522_GPIO_MISO_PIN )

// ... ...

我们通常把某模块的相关初始化配置放在一个函数中,函数名定义为xxx_Init,这样做会避免main函数内容过多,不方便阅读,而且当使用的模块较多时, 这样也方便管理。

这里使用RC522_Init初始化SPI配置,失能复位(不复位),失能片选(不选中模块)。

rc522_config.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void RC522_Init ( void )
 {
   /* SPI初始化 */
   RC522_SPI_Config ();

   /* 失能复位 */
   RC522_Reset_Disable();

   /* 失能片选 */
   RC522_CS_Disable();
 }

在主函数中初始化SysTick为 1us 中断一次,方便后面使用滴答定时器的延时函数。初始化LCD用于显示模块信息,如果不想使用LCD模块可以不用理这些代码或者注释掉相关代码即可。 初始化串口、按键,再调用 RC522_Init 函数初始化 RC522 模块,之后串口助手上位机会打印调试信息。然后是一些LCD的配置,不用深究。 接着调用 PcdReset 函数复位RC522模块,再设置工作方式’A’,其实只有一种工作方式,最后进入主循环执行 IC_test 函数来检测IC卡。

main.c
 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
/**
 * @brief  主函数
 * @param  无
 * @retval 无
 */
 int main(void)
 {
   /*滴答时钟初始化*/
   SysTick_Init ();

   /*LCD 初始化*/
   ILI9341_Init ();

   /* USART config */
   USART_Config();

   /* Key 初始化 */
   Key_GPIO_Config();

   /*RC522模块所需外设的初始化配置*/
   RC522_Init ();

   printf ( "WF-RC522 Test\n" );

   /*其中0、3、5、6 模式适合从左至右显示文字,*/
   ILI9341_GramScan ( 6 );
   LCD_SetFont(&Font8x16);
   LCD_SetColors(BLACK,BLACK);

   /* 清屏,显示全黑 */
   ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH);

   /********显示字符串示例*******/
   LCD_SetTextColor(RED);

   ILI9341_DispStringLine_EN(LINE(18),
       (char* )"Please put the IC card on WF-RC522 antenna area ...");

   LCD_SetTextColor(YELLOW);

   PcdReset ();

   /*设置工作方式*/
   M500PcdConfigISOType ( 'A' );

   while(1)
   {
     /*IC卡检测 */
     IC_test ();

   }

 }

我们来详细介绍一下主循环里面的这个 IC_test 测试函数。 在该函数中定义读写数据缓冲变量 readValue 和 writeValue,初始写入IC卡 writeValue 的值等于 100。 还有定义用于串口打印的字符串的缓存数组 cStr、ucArray_ID 用于存放IC卡的类型和IC卡序列号(UID)、ucStatusReturn 为返回状态。

在定义完这些变量之后,程序会进入一个死循环,会不断地重复执行死循环里面的代码,也就是说会不断的检测IC卡。 在看这部分代码之前,我们最好先来看下操作 RC522 和IC卡的具体流程,如下图所示,我们需要根据流程来操作才不会导致错误。

rc522_操作流程

首先我们需要开始寻卡,如果寻卡失败会再次寻卡,直到寻卡成功后才会执行 if ( ucStatusReturn == MI_OK ) 里面的代码。 寻到卡后通过防冲撞算法选择要操作的IC卡,密码校验成功后写入金额,然后写入金额成功后读出金额,并打印至串口以及在LCD上显示。

在每一个操作流程也会同时检测按键 1 和按键 2 是否按下,这两个按键按下会增加/减少 writeValue 的值, 然后在下一个操作流程里执行到 WriteAmount(0x11, writeValue); 语句的时候会将这个值写入IC卡。

main.c
 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
 uint8_t KeyValue[]={0xFF ,0xFF, 0xFF, 0xFF, 0xFF, 0xFF};   // 卡A密钥

 /**
   * @brief  IC测试函数
   * @param  无
   * @retval 无
   */
 void IC_test ( void )
 {
   uint32_t writeValue = 100;
   uint32_t readValue ;
   char cStr [ 30 ];
   uint8_t ucArray_ID [ 4 ];    /*先后存放IC卡的类型和UID(IC卡序列号)*/
   uint8_t ucStatusReturn;      /*返回状态*/

   /* 按照流程操作 RC522 ,否者会导致操作失败 */
   while ( 1 )
   {
     /*寻卡*/
     if ( ( ucStatusReturn = PcdRequest ( PICC_REQALL, ucArray_ID ) ) != MI_OK )
     {
       /*若失败再次寻卡*/
       printf ( "寻卡失败\n" );
       ucStatusReturn = PcdRequest ( PICC_REQALL, ucArray_ID ); //PICC_REQALL   PICC_REQIDL
     }

     if ( ucStatusReturn == MI_OK  )
     {
       //printf ( "寻卡成功\n" );
       /*防冲撞(当有多张卡进入读写器操作范围时,防冲突机制会从其中选择一张进行操作)*/
       if ( PcdAnticoll ( ucArray_ID ) == MI_OK )
       {
         PcdSelect(ucArray_ID);

         PcdAuthState( PICC_AUTHENT1A, 0x11, KeyValue, ucArray_ID );//校验密码
         WriteAmount(0x11,writeValue); //写入金额
         if(ReadAmount(0x11,&readValue) == MI_OK)    //读取金额
         {
           //writeValue +=100;
           sprintf ( cStr, "The Card ID is: %02X%02X%02X%02X",ucArray_ID [0], ucArray_ID [1], ucArray_ID [2],ucArray_ID [3] );
           printf ( "%s\r\n",cStr );  //打印卡片ID
           ILI9341_DispStringLine_EN(LINE(0) , (char* )cStr ); //在屏幕上面显示ID

           printf ("余额为:%d\r\n",readValue);
           sprintf ( cStr, "TThe residual amount: %d", readValue);
           ILI9341_DispStringLine_EN(LINE(1) , (char* )cStr ); //在屏幕上面显示金额
           PcdHalt();
         }

         /* 按键1检测 */
         if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON  )
         {
           printf("金额 +100\r\n");
           writeValue += 100;
         }
         /* 按键2检测 */
         if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON  )
         {
           printf("金额 -100\r\n");
           writeValue -= 100;
         }
       }
     }

   }
 }

我们所使用的IC卡(也就是M1卡),其内部总共有16个扇区,而每个扇区都有:3个数据块 + 1个控制块,因此一共64个块。 除了第 0 扇区的块 0 不可更改以外,其它块都是有办法更改的,这其中就包括数据块(一般用来保存“金额”这样的具体数据)、还有控制块(包含密码A、密码B以及存取控制)。 一般我们需要读写数据块,需要验证密码A或者密码B,有时候我们也有要更改密码的需求,出于安全性的考虑这些操作都受到控制块当中的存取控制字节(4字节)的影响。 这里不做详细地展开,具体的资料请查阅模块资料里面的文档。

在我们的例程里,我们提供了判断某个块是否是有效数据块的函数(IsDataBlock)、还有读取和写入字符串到数据块的函数(PcdReadString、PcdWriteString), 我们在使用这些函数是,都要注意按照前面所述的操作流程来编写程序,否者会导致错误。

rc522_function.c
 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
 /**
   * @brief  判断 ucAddr 是否数据块
   * @param  ucAddr,块绝对地址(0-63)
   * @retval 返回值 1:是数据块;0:不是数据块
   */
 char IsDataBlock( uint8_t ucAddr )
 {
   if(ucAddr == 0)
   {
     printf("第0扇区的块0不可更改,不应对其进行操作\r\n");
     return 0;
   }
   /* 如果是数据块(不包含数据块0) */
   if( (ucAddr<64) && (((ucAddr+1)%4) != 0) )
   {
     return 1;
   }

   printf("块地址不是指向数据块\r\n");
   return 0;
 }

 /**
   * @brief  写 pData 字符串到M1卡中的数据块
   * @param  ucAddr,数据块地址(不能写入控制块)
   * @param  pData,写入的数据,16字节
   * @retval 状态值= MI_OK,成功
   */
 char PcdWriteString ( uint8_t ucAddr, uint8_t * pData )
 {
   /* 如果是数据块(不包含数据块0),则写入 */
   if( IsDataBlock(ucAddr) )
   {
     return PcdWrite(ucAddr, pData);
   }

   return MI_ERR;
 }

 /**
   * @brief  读取M1卡中的一块数据到 pData
   * @param  ucAddr,数据块地址(不读取控制块)
   * @param  pData,读出的数据,16字节
   * @retval 状态值= MI_OK,成功
   */
 char PcdReadString ( uint8_t ucAddr, uint8_t * pData )
 {
   /* 如果是数据块(不包含数据块0),则读取 */
   if( IsDataBlock(ucAddr) )
   {
     return PcdRead(ucAddr, pData);
   }

   return MI_ERR;
 }

例程中还提供了修改密码A函数(ChangeKeyA),该函数是按照前面所述的操作流程来写的, 在默认的存取控制权限以及默认的密码B的情况下可以直接调用该函数来更改被用户忘记了的密码A。

rc522_function.c
 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
 /**
 * @brief 修改控制块 ucAddr 的密码A。注意 ucAddr 指的是控制块的地址。
 *        必须要校验密码B,密码B默认为6个0xFF,如果密码B也忘记了,那就改不了密码A了
 * @note  注意:该函数仅适用于默认的存储控制模式,若是其他的话可能出现问题
 * @param ucAddr:[控制块]所在的地址。M1卡总共有16个扇区(每个扇区有:3个数据块+1个控制块),共64个块
 * @param pKeyA:指向新的密码A字符串,六个字符,比如 "123456"
 * @retval 成功返回 MI_OK
 */
 char ChangeKeyA( uint8_t ucAddr, uint8_t *pKeyA )
 {
   uint8_t KeyBValue[]={0xFF ,0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // B密钥
   uint8_t ucArray_ID [ 4 ];    /*先后存放IC卡的类型和UID(IC卡序列号)*/
   uint8_t ucComMF522Buf[16];
   uint8_t j;

   /*寻卡*/
   while ( PcdRequest ( PICC_REQALL, ucArray_ID ) != MI_OK )
   {
     printf( "寻卡失败\r\n" );
     Delay_us(1000000);
   }

   printf ( "寻卡成功\n" );

   /* 防冲突(当有多张卡进入读写器操作范围时,防冲突机制会从其中选择一张进行操作)*/
   if ( PcdAnticoll ( ucArray_ID ) == MI_OK )
   {
     /* 选中卡 */
     PcdSelect(ucArray_ID);

     /* 校验 B 密码 */
     if( PcdAuthState( PICC_AUTHENT1B, ucAddr, KeyBValue, ucArray_ID ) != MI_OK )
     {
       printf( "检验密码B失败\r\n" );
     }

     // 读取控制块里原本的数据(只要修改密码A,其他数据不改)
     if( PcdRead(ucAddr,ucComMF522Buf) != MI_OK)
     {
       printf( "读取控制块数据失败\r\n" );
       return MI_ERR;
     }

     /* 修改密码A */
     for(j=0; j<6; j++)
       ucComMF522Buf[j] = pKeyA[j];

     if( PcdWrite(ucAddr,ucComMF522Buf) != MI_OK)
     {
       printf( "写入数据到控制块失败\r\n" );
       return MI_ERR;
     }

     printf( "密码A修改成功!\r\n" );
     PcdHalt();

     return MI_OK;
   }

   return MI_ERR;
 }