1. 项目概述
最近在做一个智能门禁系统的原型开发,需要用到RFID卡片的识别功能。经过多方对比,最终选择了性价比较高的RC522 RFID模块作为读卡器,搭配STM32F103C8T6最小系统板来实现这个功能。这个组合在实际项目中非常常见,特别适合中小型物联网设备的开发。
RC522是NXP公司推出的一款高度集成的非接触式读写卡芯片,支持ISO14443A标准的MIFARE系列卡片。它的工作频率是13.56MHz,通信距离一般在5cm以内,非常适合门禁、考勤等场景。而STM32作为主控,负责与RC522通信、处理卡片数据以及实现业务逻辑。
2. 硬件连接与配置
2.1 硬件选型与接口定义
我使用的硬件配置如下:
- 主控芯片:STM32F103C8T6(Blue Pill开发板)
- RFID模块:RC522(带SPI接口版本)
- 卡片类型:MIFARE Classic 1K(UID可读)
RC522模块通常提供四种通信接口:SPI、I2C、UART和并行接口。考虑到STM32的SPI外设性能较好,且SPI通信速率高、稳定性好,我选择了SPI接口进行连接。具体引脚定义如下:
| RC522引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| SDA | PA4 | SPI片选 |
| SCK | PA5 | SPI时钟 |
| MOSI | PA7 | 主出从入 |
| MISO | PA6 | 主入从出 |
| IRQ | 不接 | 中断引脚 |
| GND | GND | 地线 |
| RST | PA1 | 复位引脚 |
| 3.3V | 3.3V | 电源 |
注意:RC522的工作电压是2.5V-3.3V,绝对不能接5V,否则会烧毁芯片。有些开发板的3.3V输出电流不足,可能会导致RC522工作不稳定,建议单独供电或确认电源质量。
2.2 硬件连接检查
在实际焊接或连接杜邦线时,有几个关键点需要注意:
- 先接GND,再接电源,最后接信号线
- SPI的时钟线(SCK)要尽量短,避免干扰
- 如果通信不稳定,可以在3.3V和GND之间加一个100uF的电解电容
- 模块天线部分不要被金属物体遮挡,会影响读卡距离
连接完成后,建议先用万用表检查各引脚连接是否正确,特别是电源和地线之间不能短路。
3. 软件开发环境搭建
3.1 开发工具准备
我使用的是Keil MDK作为开发环境,配合STM32标准外设库进行开发。也可以选择STM32CubeIDE或者PlatformIO等工具,根据个人习惯选择即可。
需要准备的软件组件:
- Keil MDK(或其它IDE)
- STM32F1xx标准外设库
- RC522的驱动代码(可以从GitHub上找到开源实现)
3.2 工程配置关键点
在Keil中新建工程时,需要注意以下几个配置:
- 选择正确的设备型号:STM32F103C8
- 在"Options for Target"中,勾选"Use MicroLIB"以减小代码体积
- 在C/C++选项卡的预定义符号中添加:USE_STDPERIPH_DRIVER
- 设置正确的晶振频率(根据实际硬件,一般是8MHz)
对于SPI外设的初始化,时钟配置很重要。我的配置如下:
- APB2时钟:72MHz
- SPI1时钟分频:8分频(SPI时钟=9MHz)
- 时钟极性:低电平
- 时钟相位:第一个边沿采样
- 数据大小:8位
- 模式:主机模式
4. RC522驱动实现
4.1 底层通信函数
首先需要实现SPI的读写函数,这是与RC522通信的基础。以下是关键代码片段:
c复制// SPI发送一个字节
void RC522_WriteByte(uint8_t data) {
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, data);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
SPI_I2S_ReceiveData(SPI1);
}
// SPI读取一个字节
uint8_t RC522_ReadByte(void) {
RC522_WriteByte(0xFF); // 发送dummy字节
return SPI_I2S_ReceiveData(SPI1);
}
// 向RC522寄存器写数据
void RC522_WriteReg(uint8_t addr, uint8_t val) {
RC522_CS_LOW();
RC522_WriteByte((addr << 1) & 0x7E); // 地址格式:0XXXXXX0
RC522_WriteByte(val);
RC522_CS_HIGH();
}
// 从RC522寄存器读数据
uint8_t RC522_ReadReg(uint8_t addr) {
uint8_t val;
RC522_CS_LOW();
RC522_WriteByte(((addr << 1) & 0x7E) | 0x80); // 地址格式:0XXXXXX1
val = RC522_ReadByte();
RC522_CS_HIGH();
return val;
}
4.2 RC522初始化流程
RC522的初始化需要按照特定顺序配置多个寄存器,以下是典型初始化代码:
c复制void RC522_Init(void) {
// 复位RC522
RC522_RST_HIGH();
delay_ms(1);
RC522_RST_LOW();
delay_ms(1);
RC522_RST_HIGH();
delay_ms(50);
// 关闭加密
RC522_WriteReg(RC522_RegTxMode, 0x00);
RC522_WriteReg(RC522_RegRxMode, 0x00);
// 设置定时器
RC522_WriteReg(RC522_RegTMode, 0x80);
RC522_WriteReg(RC522_RegTPrescaler, 0xA9);
RC522_WriteReg(RC522_RegTReloadH, 0x03);
RC522_WriteReg(RC522_RegTReloadL, 0xE8);
// 设置调制参数
RC522_WriteReg(RC522_RegTxASK, 0x40);
RC522_WriteReg(RC522_RegMode, 0x3D);
// 天线开启
RC522_AntennaOn();
}
4.3 卡片检测与识别
卡片检测采用轮询方式,定期发送请求命令检查是否有卡片在感应区内:
c复制uint8_t RC522_Request(uint8_t reqMode, uint8_t *TagType) {
uint8_t status;
uint16_t backBits;
RC522_WriteReg(RC522_RegBitFraming, 0x07);
RC522_WriteReg(RC522_RegComIrq, 0x7F);
RC522_WriteReg(RC522_RegFIFOLevel, 0x80);
RC522_WriteReg(RC522_RegCommand, RC522_CMD_Transceive);
RC522_WriteReg(RC522_RegComIrq, 0x00);
RC522_WriteReg(RC522_RegFIFOData, reqMode);
RC522_WriteReg(RC522_RegCommand, RC522_CMD_Transceive);
if(reqMode == RC522_REQIDL) {
RC522_WriteReg(RC522_RegBitFraming, 0x87);
}
uint16_t i = 2000;
do {
status = RC522_ReadReg(RC522_RegComIrq);
i--;
} while((i!=0) && !(status&0x01) && !(status&RC522_IRQ_Err));
if(i == 0 || (status & RC522_IRQ_Err)) {
return RC522_ERR;
}
backBits = RC522_ReadReg(RC522_RegError) & 0x07;
if(backBits != 0) {
return RC522_ERR;
}
status = RC522_ReadReg(RC522_RegControl);
RC522_WriteReg(RC522_RegControl, status | 0x80);
if(RC522_ReadReg(RC522_RegFIFOLevel) != 1) {
return RC522_ERR;
}
*TagType = RC522_ReadReg(RC522_RegFIFOData);
RC522_WriteReg(RC522_RegControl, status);
return RC522_OK;
}
5. 卡片UID读取实现
5.1 防冲突与UID获取
当检测到卡片后,需要进行防冲突处理并获取卡片UID:
c复制uint8_t RC522_Anticoll(uint8_t *serNum) {
uint8_t status;
uint8_t i;
uint8_t serNumCheck = 0;
uint16_t unLen;
RC522_WriteReg(RC522_RegBitFraming, 0x00);
serNum[0] = RC522_PICC_ANTICOLL1;
serNum[1] = 0x20;
RC522_WriteReg(RC522_RegFIFOData, serNum[0]);
RC522_WriteReg(RC522_RegFIFOData, serNum[1]);
RC522_WriteReg(RC522_RegCommand, RC522_CMD_Transceive);
RC522_WriteReg(RC522_RegBitFraming, 0x80);
uint16_t iWait = 2000;
do {
status = RC522_ReadReg(RC522_RegComIrq);
iWait--;
} while((iWait!=0) && !(status&0x01) && !(status&RC522_IRQ_Err));
RC522_WriteReg(RC522_RegBitFraming, 0x00);
if(iWait == 0 || (status & RC522_IRQ_Err)) {
return RC522_ERR;
}
unLen = RC522_ReadReg(RC522_RegFIFOLevel);
if(unLen != 5) {
return RC522_ERR;
}
for(i=0; i<unLen; i++) {
serNum[i] = RC522_ReadReg(RC522_RegFIFOData);
}
for(i=0; i<4; i++) {
serNumCheck ^= serNum[i];
}
if(serNumCheck != serNum[4]) {
return RC522_ERR;
}
return RC522_OK;
}
5.2 完整卡片识别流程
将上述函数组合起来,实现完整的卡片识别流程:
c复制uint8_t RC522_GetCardUID(uint8_t *uid) {
uint8_t status;
uint8_t str[MAX_LEN];
// 寻卡
status = RC522_Request(RC522_REQIDL, str);
if(status != RC522_OK) {
return status;
}
// 防冲突
status = RC522_Anticoll(uid);
if(status != RC522_OK) {
return status;
}
// 选择卡片
status = RC522_SelectTag(uid);
if(status != RC522_OK) {
return status;
}
return RC522_OK;
}
6. 实际应用与优化
6.1 主程序逻辑设计
在实际应用中,主程序通常采用轮询方式检测卡片:
c复制int main(void) {
uint8_t uid[5];
uint8_t status;
SystemInit();
SPI1_Init();
RC522_Init();
while(1) {
status = RC522_Check();
if(status == MI_OK) {
status = RC522_GetCardUID(uid);
if(status == MI_OK) {
printf("Card detected: ");
for(int i=0; i<4; i++) {
printf("%02X ", uid[i]);
}
printf("\n");
// 这里可以添加业务逻辑,比如门禁控制
}
}
delay_ms(100);
}
}
6.2 性能优化技巧
在实际项目中,我发现以下几个优化点可以显著提高系统性能:
- 调整SPI时钟频率:在保证稳定性的前提下,可以适当提高SPI时钟频率(最高到10MHz)
- 优化轮询间隔:根据实际需求调整卡片检测的间隔时间,平衡响应速度和CPU占用率
- 使用中断方式:如果有足够的GPIO,可以使用RC522的中断功能来替代轮询
- 电源管理:在不使用时可以关闭RC522的天线以节省功耗
6.3 常见问题排查
在开发过程中,我遇到过以下典型问题及解决方法:
-
无法检测到卡片
- 检查天线连接是否正常
- 确认RC522的电源电压稳定(3.3V)
- 调整天线匹配电路中的电容值(通常为27pF)
-
通信不稳定
- 检查SPI线缆是否过长
- 在电源引脚添加滤波电容
- 降低SPI时钟频率测试
-
UID读取错误
- 确保卡片类型是MIFARE Classic
- 检查防冲突算法实现是否正确
- 尝试减小读卡距离
-
模块发热严重
- 立即断电检查,可能是电源接反或电压过高
- 检查天线是否短路
7. 扩展功能实现
7.1 卡片数据读写
除了读取UID,RC522还可以读写MIFARE卡片的数据块。以下是数据读取的示例代码:
c复制uint8_t RC522_ReadBlock(uint8_t blockAddr, uint8_t *recvData) {
uint8_t status;
uint8_t i;
uint8_t sendData[2];
uint8_t unLen;
sendData[0] = RC522_PICC_READ;
sendData[1] = blockAddr;
RC522_CalculateCRC(sendData, 2, &sendData[2]);
status = RC522_ToCard(RC522_CMD_Transceive, sendData, 4, recvData, &unLen);
if((status != RC522_OK) || (unLen != 0x90)) {
return RC522_ERR;
}
for(i=0; i<16; i++) {
recvData[i] = RC522_ReadReg(RC522_RegFIFOData);
}
return RC522_OK;
}
7.2 多卡片处理
在实际应用中,可能会遇到多张卡片同时进入感应区的情况。这时需要改进防冲突算法:
c复制uint8_t RC522_Anticoll2(uint8_t *serNum, uint8_t *serNumCount) {
// 实现更复杂的防冲突算法
// ...
}
7.3 低功耗设计
对于电池供电的设备,可以这样优化功耗:
c复制void RC522_LowPowerMode(void) {
RC522_AntennaOff();
RC522_WriteReg(RC522_RegCommand, RC522_CMD_SOFT_POWER_DOWN);
}
void RC522_WakeUp(void) {
RC522_WriteReg(RC522_RegCommand, RC522_CMD_IDLE);
RC522_Init();
}
8. 项目总结与心得
经过这个项目的实践,我总结了以下几点经验:
-
SPI时序很重要:RC522对SPI时序要求比较严格,初始化时要确保时钟极性和相位设置正确。我最初因为这个问题调试了很久,后来发现是SPI模式设置不对。
-
天线设计影响大:读卡距离很大程度上取决于天线设计。如果发现读卡距离短,可以尝试调整天线匹配电路中的电容值。我在PCB设计时参考了官方推荐的天线布局,效果很好。
-
电源质量很关键:RC522对电源噪声比较敏感,建议在电源引脚就近放置一个10uF和一个0.1uF的电容。曾经因为电源问题导致读卡不稳定,添加电容后解决了。
-
UID处理要注意:有些卡片的UID会变化(特别是国产兼容卡),在实际应用中需要考虑这种情况。我遇到过一个案例,同一张卡在不同时间读出的UID后几位会变化,后来发现是卡片本身的问题。
-
调试技巧:在调试时,可以先使用现成的库验证硬件是否正常,然后再自己实现底层驱动。这样可以快速定位是硬件问题还是软件问题。
这个项目虽然看起来简单,但涉及到的知识点很多,包括SPI通信、RFID协议、低功耗设计等。通过实际动手实现,我对STM32的外设使用和RC522的工作原理有了更深入的理解。后续还可以考虑增加更多功能,比如卡片数据加密、多卡片同时识别等。