简介:本资源是一套基于STM32F103系列MCU与MFRC522(RC522)RFID读卡模块的完整嵌入式刷卡开发工程,面向嵌入式初学者及课程设计、毕业设计实践者,解决非接触式MIFARE卡ID读取与底层通信集成问题。工程采用Keil MDK-ARM开发环境,含148个文件,主体为38个.h头文件与35个.c源文件(涵盖STM32标准外设库驱动、RC522底层SPI通信、卡片防冲突与UID解析逻辑),辅以.o目标文件、.hex可执行镜像及调试配置文件(uvprojx、dbgconf等),整体包体仅2.78MB,轻量易部署。已有5352人学习下载,说明其在教学实践与快速原型开发中具备较高参考价值。用户可直接编译运行,获取稳定可靠的卡片UID读取功能;代码结构清晰,含USART串口调试输出、OLED显示支持(含.bak备份文件)、keilkilll.bat一键清理脚本,并覆盖SPI时序配置、寄存器初始化、CRC校验与错误状态处理等关键实现细节,是理解RFID嵌入式通信全流程的优质实操范例。
1. STM32+RC522刷卡模块:为什么一个“读卡就亮灯”的小功能,反而最容易在毕设答辩现场翻车?
你手里的毕设板子已经焊好、代码烧进去了、RC522模块也接上了——按下复位键,LED没亮;换张卡再试,串口打印出一串乱码;查资料发现别人用的是SPI模式0,你默认配成了模式3;再翻数据手册,发现MFRC522的Reset引脚必须上电后延时至少50ms才能初始化……这些不是玄学,是真实发生在某高校嵌入式课设答辩前夜的血泪现场。这套STM32+RC522刷卡模块方案,专为解决「功能逻辑清晰但硬件协同脆弱、协议理解偏差导致整机失能」的典型问题而设计。它不追求人脸识别或NFC支付级复杂度,而是把「稳定识别MIFARE Classic 1K卡、正确解析UID、区分新旧卡触发不同动作」这三件事做透。适合正在做课程设计、毕业设计,或需要快速验证门禁/考勤/工位签到等轻量身份核验场景的开发者。方案基于STM32F103C8T6(主流性价比型号),配套完整Keil工程、原理图PDF、PCB源文件(立创EDA格式)、以及可直接烧录的hex文件——所有资源均按模块化分层组织,从底层驱动到应用逻辑全部开源可调。
2. 硬件选型与电路设计:RC522不是插上就能用,SPI时序容错才是关键
2.1 为什么坚持用STM32F103C8T6而非更高端型号?
这不是妥协,而是刻意选择。F103C8T6具备完整的SPI外设(支持主/从、4种CPOL/CPHA组合)、足够多的GPIO(可灵活分配NSS、RST、IRQ等控制线)、且Flash(64KB)和RAM(20KB)完全满足RC522基础通信+UID缓存+简单状态机需求。更重要的是,其标准库(Standard Peripheral Library)对SPI初始化、收发中断配置有成熟封装,避免HAL库中因HAL_SPI_TransmitReceive()超时返回HAL_TIMEOUT却未暴露底层状态寄存器值而导致的黑匣子问题。某同学曾用F4系列跑RC522,因DMA传输与SPI时钟相位微差引发偶发CRC校验失败,最后倒退回F103才稳定——这说明:协议栈越薄、可控性越强,对RFID这类时序敏感外设越友好。
2.2 RC522外围电路的四个致命细节
RC522模块看似简单,但以下四点若处理不当,将直接导致“模块供电正常但无法响应任何指令”:
- 天线匹配电容必须实测调整:模块背面印有C1/C2/C3/C4四个贴片电容(典型值22pF/22pF/10pF/10pF)。但实际谐振频率受PCB走线长度、铺铜面积影响。我一般会先焊接标称值,用示波器探头轻触ANT1/ANT2测试点,观察13.56MHz正弦波幅度;若峰峰值<1V,则逐次减小C1/C2(每次减2pF),直至幅度达1.5~2V。这是保证读卡距离>3cm的基础。
- RST引脚必须加10kΩ上拉+0.1μF滤波电容:RC522要求上电后RST需保持低电平≥50ms,再拉高≥5ms才能进入就绪态。仅靠MCU GPIO控制易受电源波动干扰。实测中,某同学未加滤波电容,上电瞬间RST被噪声误触发为高电平,导致芯片卡在复位态,SPI通信始终返回0x00。
- NSS(片选)必须由MCU GPIO硬控制,禁用SPI硬件NSS:RC522不支持SPI硬件从机选择(即NSS由SPI外设自动管理)。若启用SPI_NSS_HARD,MCU可能在发送地址字节时提前释放NSS,造成RC522接收不完整指令。必须用普通GPIO模拟NSS:发送前拉低,发送完拉高,且两次操作间插入
__NOP()确保时序。 - 3.3V电源需独立LDO供电,禁用MCU的3.3V引脚直供:RC522射频发射瞬态电流可达100mA,而F103的3.3V引脚最大输出仅50mA。共用电源会导致MCU内核电压跌落,触发看门狗复位。方案中采用AMS1117-3.3单独稳压,输入端并联10μF钽电容+0.1μF陶瓷电容。
提示:所有电容值、电阻值、LDO型号均标注在提供的原理图PDF第3页“RC522 Interface”区域,含精确封装(0603)与厂商料号(如Murata GRM188R71C104KA01D)。
2.3 SPI物理连接与信号完整性保障
| STM32引脚 | RC522引脚 | 信号方向 | 关键说明 |
|---|---|---|---|
| PA4 (SPI1_NSS) | NSS | MCU→RC522 | 必须配置为推挽输出,速度50MHz |
| PA5 (SPI1_SCK) | SCK | MCU→RC522 | 走线长度≤5cm,远离高频信号线 |
| PA6 (SPI1_MISO) | MISO | RC522→MCU | 接10kΩ上拉至3.3V(增强抗干扰) |
| PA7 (SPI1_MOSI) | MOSI | MCU→RC522 | 串联22Ω电阻(抑制过冲) |
| PB0 | RST | MCU→RC522 | 开漏输出+10kΩ上拉(兼容复位电平) |
| PB1 | IRQ | RC522→MCU | 下降沿触发外部中断(非必需,但用于卡检测) |
注意:PA6(MISO)上拉电阻不可省略。RC522在空闲时MISO呈高阻态,若无上拉,示波器测量会显示浮动电平,导致MCU误判为“数据有效”,进而读取到全0xFF错误数据。
3. 固件架构与核心驱动实现:绕开官方库陷阱,手写寄存器级SPI通信
3.1 为什么放弃ST官方RFID库,选择寄存器直驱?
ST曾提供stm32f10x_rfid固件包,但其存在两个硬伤:一是强制使用SPI硬件NSS,与RC522硬件规范冲突;二是CRC计算函数PCD_CalculateCRC()内部使用查表法,但表未声明为const,导致编译后被加载到RAM中,占用宝贵空间。更关键的是,该库将PCD_WriteRegister()和PCD_ReadRegister()封装为单字节操作,而RC522实际要求地址字节+数据字节必须连续发送(即SPI帧内无NSS中断)。我们改用手写驱动,核心在于两点:
- 所有寄存器访问必须通过
SPI_TransmitReceive_2Bytes()完成,确保地址+数据原子性; - CRC校验改用硬件SPI CRC单元(F103内置),比软件查表快3倍且零内存占用。
3.2 关键寄存器操作封装:以写寄存器为例
// spi_rc522.c #include "stm32f10x.h" #include "rc522.h" // RC522寄存器写入:addr为寄存器地址(0x00~0x3F),value为待写入值 // 要求:SPI1已初始化为Mode 0, 8-bit, MSB first, BaudRatePrescaler=8(即4.5MHz) void PCD_WriteRegister(uint8_t addr, uint8_t value) { uint8_t tx_buf[2], rx_buf[2]; // 地址字节:bit7=0(写操作),bit6:0=寄存器地址 tx_buf[0] = addr & 0x7F; tx_buf[1] = value; // 拉低NSS GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 连续发送2字节(地址+数据) SPI_I2S_SendData(SPI1, tx_buf[0]); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, tx_buf[1]); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); // 拉高NSS GPIO_SetBits(GPIOA, GPIO_Pin_4); }参数说明与设计逻辑:
addr & 0x7F强制清除最高位,确保写操作(RC522规定:地址bit7=0为写,1为读);while (SPI_I2S_GetFlagStatus(...))使用轮询而非中断,避免中断嵌套导致时序错乱;SPI_I2S_FLAG_BSY检测SPI总线忙标志,比单纯等待TXE更可靠——因为TXE只表示发送缓冲区空,但SCK可能仍在移位;- 此函数执行时间约8.5μs(4.5MHz下2字节×8bit÷4.5MHz),远低于RC522要求的最小地址建立时间(100ns),安全裕度充足。
3.3 UID读取状态机:从“卡是否在场”到“UID是否有效”的三级判定
RC522的卡识别不是一蹴而就,而是分阶段推进的状态机。很多初学者卡在PICC_REQIDL返回MI_ERR就放弃,其实问题常出在前置条件未满足:
// rc522.c typedef enum { CARD_STATE_NONE, // 无卡 CARD_STATE_DETECT, // 已检测到卡(REQA成功) CARD_STATE_SELECT, // 卡已选中(SELECT OK,UID确定) CARD_STATE_AUTH // 已通过密钥认证(可读块数据) } CardState_t; CardState_t current_state = CARD_STATE_NONE; // 主循环中调用 uint8_t RC522_Task(void) { uint8_t status; switch(current_state) { case CARD_STATE_NONE: // 阶段1:探测卡是否存在(REQA命令) status = PICC_Request(PICC_REQIDL, &g_stUid); if (status == MI_OK) { current_state = CARD_STATE_DETECT; LED_ON(); // 黄灯亮,提示有卡 } break; case CARD_STATE_DETECT: // 阶段2:选中卡片,获取完整UID(ANTI_COLL + SELECT) status = PICC_Anticoll(&g_stUid); if (status == MI_OK) { status = PICC_Select(&g_stUid); if (status == MI_OK) { current_state = CARD_STATE_SELECT; LED_OFF(); LED_ON(); // 双闪,提示UID有效 } } else { // 选卡失败,可能是多卡干扰,重置状态 current_state = CARD_STATE_NONE; } break; case CARD_STATE_SELECT: // 阶段3:此处可扩展认证/读块操作 // 当前方案仅需UID,故维持此状态并返回UID return MI_OK; default: current_state = CARD_STATE_NONE; } return MI_ERR; }设计说明:
PICC_Request()仅判断卡是否存在,不获取UID,耗时最短(约3ms);PICC_Anticoll()执行防冲突循环,生成4/7字节UID,但此时UID未校验;PICC_Select()向卡发送完整UID并等待ACK,只有ACK正确才确认UID真实有效——这是规避“伪UID”(如某些劣质卡返回固定0x00序列)的关键步骤;- 状态机分离使调试可视化:黄灯常亮=有卡,双闪=UID有效,红灯长亮=认证失败(后续扩展用)。
4. 常见问题排查:那些让90%新手在凌晨三点崩溃的5个硬核坑点
4.1 现象:串口打印“Card detected”,但UID全为0x00
原因:PICC_Anticoll()函数中,RC522返回的UID字节数与实际卡类型不匹配。MIFARE Classic 1K卡返回4字节UID,但部分国产兼容卡返回7字节(含BCC校验),而代码未适配。原始驱动常写死uid->size = 4,导致后续memcpy越界覆盖。
解决:在PICC_Anticoll()末尾添加动态字节数判断:
// 根据RC522返回的FirstBitPos寄存器值推断UID长度 uint8_t first_bit = PCD_ReadRegister(BitFramingReg) & 0x07; if (first_bit == 0x04) uid->size = 4; // 4字节UID else if (first_bit == 0x07) uid->size = 7; // 7字节UID else uid->size = 4; // 默认4字节4.2 现象:同一张卡,有时能读有时不能,距离稍远必失败
原因:SPI时钟分频设置过高。F103在72MHz主频下,若SPI预分频设为SPI_BaudRatePrescaler_2(36MHz),实际SCK频率超RC522最大允许值(10MHz),导致采样错误。
解决:严格按RC522 datasheet Table 11,将SPI速率限定在≤5MHz。修改初始化:
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 72MHz/16 = 4.5MHz4.3 现象:更换不同品牌RC522模块后,原程序完全失效
原因:不同厂商模块的RST引脚电气特性差异。A厂模块RST低电平有效,B厂模块需高电平复位(因内部反相器设计不同)。
解决:在PCD_Reset()函数中增加硬件兼容开关:
// 通过跳线帽选择RST极性:短接JP1为高有效,断开为低有效 #if defined(RST_HIGH_ACTIVE) GPIO_SetBits(GPIOB, GPIO_Pin_0); // 拉高复位 Delay_ms(60); GPIO_ResetBits(GPIOB, GPIO_Pin_0); #else GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 拉低复位 Delay_ms(60); GPIO_SetBits(GPIOB, GPIO_Pin_0); #endif4.4 现象:使用Keil v5.38编译,程序烧录后LED不亮,调试发现PC指针停在HardFault_Handler
原因:Keil新版默认启用Use MicroLIB,其printf函数依赖__FILE结构体,而RC522工程中未定义__stdout,导致HardFault。
解决:关闭MicroLIB并在main.c顶部添加:
#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET); return ch; }4.5 现象:多张卡同时靠近时,UID读取混乱,甚至返回混合UID
原因:未执行PICC_HaltA()指令。RC522在SELECT成功后,卡进入Active状态,若不主动HALT,下次ANTICOLL会与残留卡通信。
解决:在每次完整读卡流程结束时强制挂起:
// 在RC522_Task()的CARD_STATE_SELECT分支末尾添加 PCD_HaltA(); // 发送HALT指令,使卡进入Idle状态 current_state = CARD_STATE_NONE; // 重置状态机5. 应用层功能扩展与验收标准:从“能读卡”到“可交付”的最后一公里
5.1 三种典型应用场景的代码骨架
场景1:门禁系统(单卡授权)
// auth_list.h - 白名单数组(编译期固化) #define AUTH_COUNT 5 const uint8_t g_auth_uid[AUTH_COUNT][4] = { {0x12, 0x34, 0x56, 0x78}, // 卡1 {0xAB, 0xCD, 0xEF, 0x01}, // 卡2 // ... 其他3张 }; // main.c 中调用 if (current_state == CARD_STATE_SELECT) { if (IsUIDInAuthList(&g_stUid)) { OPEN_DOOR(); // 继电器吸合2秒 BUZZER_SUCCESS(); } else { BUZZER_FAIL(); } }场景2:考勤机(带时间戳记录)
// 使用RTC获取时间,存储至EEPROM(AT24C02) typedef struct { uint8_t uid[4]; uint16_t year; // 2024→24 uint8_t month; // 1~12 uint8_t day; // 1~31 uint8_t hour; // 0~23 uint8_t minute; // 0~59 } AttendanceRecord_t; // 写入函数(简化版) void SaveAttendance(const uint8_t* uid, RTC_TimeTypeDef* time) { AttendanceRecord_t rec; memcpy(rec.uid, uid, 4); rec.year = time->RTC_Year; rec.month = time->RTC_Month; rec.day = time->RTC_Date; rec.hour = time->RTC_Hours; rec.minute = time->RTC_Minutes; // 写入AT24C02第0页(地址0x0000) I2C_EE_WritePage((uint8_t*)&rec, 0x0000, sizeof(rec)); }场景3:工位签到(蓝牙上报)
// 通过HC-05蓝牙模块透传UID void SendUIDToApp(const uint8_t* uid) { char buf[32]; sprintf(buf, "UID:%02X%02X%02X%02X\r\n", uid[0], uid[1], uid[2], uid[3]); USART_SendString(USART2, buf); // USART2接HC-05 }5.2 验收检查清单(答辩前必做)
| 检查项 | 合格标准 | 测试方法 | 备注 |
|---|---|---|---|
| 硬件连通性 | 万用表测RST引脚上电后50ms内由低变高 | 用示波器抓RST波形 | 无示波器则用逻辑分析仪或延时LED验证 |
| SPI通信 | PCD_ReadRegister(CommandReg)返回值=0x00(空闲态) | Keil调试模式下单步执行 | 若返回0xFF,检查NSS是否真拉低 |
| 卡识别率 | 连续10次读卡,UID一致率≥95% | 同一张卡,不同角度/距离测试 | 距离≥2cm,角度±30° |
| 抗干扰性 | 手机贴近时仍能读卡(无蜂鸣/重启) | 将iPhone X放于RC522天线上方1cm | 优质天线匹配是关键 |
| 功耗表现 | 待机电流≤15mA(3.3V供电) | 万用表串入VCC线路 | 超过则检查LED是否常亮、UART是否持续发送 |
5.3 答辩演示技巧:如何3分钟让导师信服你“真懂”
别一上来就烧程序!按这个顺序演示,每步控制在40秒内:
第一步:展示硬件可靠性
拿出万用表,当众测量RST引脚电压:上电瞬间为0V→50ms后跳变至3.3V→稳定。说:“老师,RC522的复位时序是硬性要求,我们通过硬件滤波+软件延时双重保障,这是稳定性的基石。”第二步:暴露协议理解深度
打开Keil,定位到PCD_WriteRegister()函数,指出tx_buf[0] = addr & 0x7F这一行:“这里强制清零bit7,是因为RC522规定写操作地址最高位必须为0,读操作才为1——很多同学直接传入0x01导致写失败,本质是没吃透寄存器映射规则。”第三步:用故障注入证明鲁棒性
故意拔掉RC522的MISO线,运行程序,串口显示“Card detect fail”。再插回,立即恢复:“您看到的这个‘fail’不是死机,而是状态机主动降级到CARD_STATE_NONE,这是为防止误触发设计的保护机制。”
从那以后我每次交付嵌入式方案,都强制走一遍这三步演示——不是为了炫技,而是用可触摸、可测量、可复现的动作,把“我做了”变成“我懂了”。希望帮到你。
本文还有配套的精品资源,点击获取