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
2.3. 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。
2.6. 例程介绍¶
本例程采用的软件SPI,即用IO口模拟SPI协议进行通信,软件SPI在《SPI基础知识》一文已经介绍,不再赘述,这里主要分析代码框架。
由于篇幅问题,这里只贴出部分代码,完整代码请参考YH-RC522模块例程。
在编写YH-RC522模块驱动时,也要考虑更改硬件环境的情况。我们把YH-RC522模块引脚相关的宏定义到 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配置,失能复位(不复位),失能片选(不选中模块)。
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卡。
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卡的具体流程,如下图所示,我们需要根据流程来操作才不会导致错误。
首先我们需要开始寻卡,如果寻卡失败会再次寻卡,直到寻卡成功后才会执行 if ( ucStatusReturn == MI_OK ) 里面的代码。 寻到卡后通过防冲撞算法选择要操作的IC卡,密码校验成功后写入金额,然后写入金额成功后读出金额,并打印至串口以及在LCD上显示。
在每一个操作流程也会同时检测按键 1 和按键 2 是否按下,这两个按键按下会增加/减少 writeValue 的值, 然后在下一个操作流程里执行到 WriteAmount(0x11, writeValue); 语句的时候会将这个值写入IC卡。
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), 我们在使用这些函数是,都要注意按照前面所述的操作流程来编写程序,否者会导致错误。
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。
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;
}
|