第一次接触RC522模块时,我被它小巧的体积和强大的功能所吸引。这个只有硬币大小的模块,竟然能通过无线电波读取几厘米外的卡片信息。作为嵌入式开发者,最让我兴奋的是它采用SPI接口与主控芯片通信,这意味着我们可以用常见的STM32系列单片机轻松驱动它。
SPI(Serial Peripheral Interface)是一种高速全双工的同步串行通信协议。在STM32与RC522的通信场景中,我们需要关注四个关键信号线:
实际接线时,我发现一个容易忽略的细节:RC522的工作电压是3.3V,而某些STM32开发板的IO口可能是5V电平。如果直接连接可能会损坏模块,建议使用电平转换电路或选择原生3.3V的STM32型号(如STM32F103C8T6)。
在我的STM32F103C8T6开发板上,我是这样连接RC522的:
| RC522引脚 | STM32引脚 | 备注 |
|---|---|---|
| SDA | PA4 | 片选信号,低电平有效 |
| SCK | PA5 | SPI时钟 |
| MOSI | PA7 | 主设备输出从设备输入 |
| MISO | PA6 | 主设备输入从设备输出 |
| RST | PB0 | 复位引脚 |
| IRQ | 不连接 | 中断引脚,本方案未使用 |
| GND | GND | 共地 |
| 3.3V | 3.3V | 电源输入 |
这里有个小技巧:如果使用硬件SPI,SCK、MOSI、MISO必须连接到STM32的SPI硬件引脚上,而SDA和RST可以任意选择普通GPIO。我在第一次尝试时错误地将SDA接到了PB12,结果SPI通信完全没反应,后来查阅数据手册才发现这个问题。
初始化SPI外设时,需要特别注意时钟相位和极性的配置。RC522模块通常工作在模式0(CPOL=0,CPHA=0),即时钟空闲时为低电平,数据在第一个时钟边沿采样。以下是完整的初始化代码:
c复制void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE);
// 配置SCK和MOSI为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置MISO为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置SPI参数
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
这段代码中,我将SPI时钟预分频设置为256,相当于约280kHz的通信速率。在实际测试中发现,过高的速率会导致通信失败,而280kHz既能保证稳定性又有足够的速度。
与RC522通信的核心是对其内部寄存器的读写操作。这里需要特别注意RC522的SPI通信协议:写寄存器时发送地址左移1位,读寄存器时发送(地址左移1位)|0x80。下面是我调试通过的读写函数:
c复制// 写寄存器
void RC522_WriteReg(uint8_t addr, uint8_t val)
{
RC522_CS_LOW(); // 使能片选
// 发送地址:bit7必须为0,bit6~1为寄存器地址,bit0为0表示写
SPI1_ReadWriteByte((addr<<1)&0x7E);
// 发送数据
SPI1_ReadWriteByte(val);
RC522_CS_HIGH(); // 关闭片选
}
// 读寄存器
uint8_t RC522_ReadReg(uint8_t addr)
{
uint8_t val;
RC522_CS_LOW(); // 使能片选
// 发送地址:bit7必须为1,bit6~1为寄存器地址,bit0为0
SPI1_ReadWriteByte((addr<<1)|0x80);
// 读取数据
val = SPI1_ReadWriteByte(0x00);
RC522_CS_HIGH(); // 关闭片选
return val;
}
在调试这些函数时,我遇到了一个典型问题:读出的寄存器值总是0xFF。经过示波器抓取波形发现,是因为片选信号切换太快,模块还没准备好数据。解决方法是在片选使能后增加1us的延时。
RC522有多个重要寄存器需要正确配置才能正常工作:
以下是初始化配置的典型代码:
c复制void RC522_Init(void)
{
RC522_Reset(); // 硬件复位
// 关闭天线
RC522_WriteReg(TxControlReg, 0x00);
// 配置定时器
RC522_WriteReg(TModeReg, 0x8D);
RC522_WriteReg(TPrescalerReg, 0x3E);
RC522_WriteReg(TReloadRegL, 30);
RC522_WriteReg(TReloadRegH, 0);
// 配置调制参数
RC522_WriteReg(TxAutoReg, 0x40);
RC522_WriteReg(ModeReg, 0x3D);
// 开启天线
RC522_WriteReg(TxControlReg, 0x03);
}
当有多张RFID卡同时进入读卡区域时,RC522需要通过防冲突机制来区分它们。这个过程类似于在一个嘈杂的房间里,主持人让每个人依次报出自己的名字。具体实现代码如下:
c复制uint8_t RC522_Request(uint8_t reqMode, uint8_t *TagType)
{
uint8_t status;
uint16_t backBits;
RC522_WriteReg(BitFramingReg, 0x07); // 设置帧格式
// 发送寻卡命令
status = RC522_ToCard(PCD_TRANSCEIVE, &reqMode, 1, TagType, &backBits);
if((status != MI_OK) || (backBits != 0x10))
{
status = MI_ERR;
}
return status;
}
uint8_t RC522_Anticoll(uint8_t *serNum)
{
uint8_t status, i, serNumCheck=0;
uint16_t unLen;
RC522_WriteReg(BitFramingReg, 0x00); // 清除防冲突位
// 发送防冲突指令
uint8_t buf[2] = {PICC_ANTICOLL, 0x20};
status = RC522_ToCard(PCD_TRANSCEIVE, buf, 2, buf, &unLen);
if(status == MI_OK)
{
// 校验序列号
for(i=0; i<4; i++)
{
serNum[i] = buf[i];
serNumCheck ^= buf[i];
}
if(serNumCheck != buf[i])
{
status = MI_ERR;
}
}
return status;
}
在实际测试中,我发现当两张卡同时靠近读卡器时,防冲突机制并不总是能完美工作。后来发现是因为天线区域电磁场分布不均匀,调整天线匹配电路后问题得到改善。
成功防冲突后,我们需要选择特定的卡片并读取其UID(唯一标识符)。这个过程就像是在人群中叫出一个人的名字,等他回应后再询问他的身份证号码:
c复制uint8_t RC522_SelectTag(uint8_t *serNum)
{
uint8_t i, status;
uint16_t unLen;
uint8_t buf[9];
buf[0] = PICC_ANTICOLL;
buf[1] = 0x70;
buf[6] = 0;
// 计算校验字节
for(i=0; i<4; i++)
{
buf[i+2] = serNum[i];
buf[6] ^= serNum[i];
}
// 计算CRC校验
RC522_CalculateCRC(buf, 7, &buf[7]);
status = RC522_ToCard(PCD_TRANSCEIVE, buf, 9, buf, &unLen);
return (status == MI_OK) ? MI_OK : MI_ERR;
}
uint8_t RC522_ReadUID(uint8_t *uid)
{
// 先寻卡
if(RC522_Request(PICC_REQIDL, uid) != MI_OK)
{
return MI_ERR;
}
// 防冲突获取UID
if(RC522_Anticoll(uid) != MI_OK)
{
return MI_ERR;
}
// 选择卡片
if(RC522_SelectTag(uid) != MI_OK)
{
return MI_ERR;
}
return MI_OK;
}
读取到的UID通常有4字节或7字节两种格式,具体取决于卡片类型。Mifare S50卡是4字节UID,而某些新式卡片为了增强安全性采用了7字节UID。