简介:本资源是一套基于STM32F103系列MCU与MFRC522(RC522)RFID读卡模块的完整嵌入式刷卡开发工程,面向嵌入式初学者及物联网项目开发者,解决非接触式MIFARE卡ID读取、SPI通信驱动与底层协议解析等核心实践问题。压缩包共148个文件,含38个.h头文件(定义寄存器、函数接口与协议结构)、35个.c源文件(涵盖STM32外设初始化、RC522驱动、CRC校验、防冲突处理及USART/OLED显示逻辑),以及.o、.d、.hex、.axf等编译中间与输出文件,整体大小2.78MB,工程基于Keil MDK构建,结构清晰、模块解耦,便于理解SPI时序配置、RFID通信流程与HAL库调用规范。目前已有5352人学习下载,资源提供可直接编译运行的完整工程,包含keilkilll.bat一键清理脚本、调试配置文件(dbgconf)、内存布局说明(sct)及详细预编译依赖关系,助读者快速上手RFID应用开发并掌握嵌入式软硬件协同调试关键能力。
1. 一个能“听懂”卡片的STM32最小系统:刷卡即触发、无PC依赖、上电就能跑通的嵌入式门禁原型
你有没有试过把一块RC522模块焊到开发板上,烧进程序,结果串口只打印乱码、LED不闪、卡片一靠近就死机?这不是玄学——是SPI时序没对齐、MFRC522初始化流程漏了关键寄存器配置、或者中断服务函数里偷偷调用了阻塞式延时。这个「STM32+RC522刷卡模块」项目,就是为这种真实翻车现场准备的:它不依赖上位机软件,不走USB虚拟串口协议栈,不靠HAL库自动适配,而是用标准外设库(SPL)手撸SPI驱动+状态机式卡片识别逻辑,最终实现——上电后绿灯常亮,卡片靠近0.3秒内蜂鸣器“滴”一声、LED切换状态、串口输出UID(如UID: 04 56 78 9A),整个过程不卡顿、不重启、不丢包。适合刚学完GPIO和SPI、正卡在“外设能点亮但读不到数据”阶段的嵌入式新手;也适合需要快速验证门禁逻辑、不想被HAL库抽象层绕晕的老手。它不是教学Demo,而是一个可直接裁剪进你真实项目的刷卡子系统——所有代码都在src/下,没有隐藏的CMSIS启动文件魔改,没有未声明的全局变量,连main.c里初始化顺序都按芯片手册推荐时序排布。
2. 硬件连接与工程结构:从原理图到Keil工程的逐层映射
2.1 RC522与STM32F103C8T6的物理接线:为什么必须用这6根线?
RC522模块虽小,但和STM32通信绝非插上线就完事。本项目采用硬件SPI主模式(非模拟IO),接线严格对应数据手册要求:
| RC522引脚 | STM32引脚 | 功能说明 | 关键约束 |
|---|---|---|---|
| SDA(MOSI) | PA7 | 主机输出数据 | 必须接SPI1_MOSI,不可用任意IO模拟 |
| SCL(SCK) | PA5 | 时钟信号 | 频率需≤10MHz,本项目设为8MHz(APB2=72MHz, 分频=9) |
| MOSI(MISO) | PA6 | 从机输出数据 | 若接错成PA7则收不到响应,调试时第一怀疑点 |
| NSS | PA4 | 片选信号 | 低电平有效,必须由MCU主动拉低才能开始通信,不能悬空 |
| RST | PB0 | 复位控制 | 项目中用软件复位(写寄存器),但硬件RST引脚仍需接上拉电阻(10kΩ) |
| IRQ | 未使用 | 中断请求 | 本项目采用轮询方式检测卡片,IRQ悬空或接地均可 |
提示:很多初学者把NSS接到PA0或其他IO,导致
PcdRequest()永远返回超时。因为MFRC522内部状态机只有在NSS拉低期间才响应SPI命令,一旦NSS意外释放,芯片立即进入休眠,后续所有读写全失效。
2.2 Keil工程目录结构:每个文件夹解决一个具体问题
项目采用分层设计,避免.c文件堆砌万行代码。所有源码位于Project/目录下,结构如下:
Project/ ├── CMSIS/ # 标准启动文件(startup_stm32f10x_md.s)和core_cm3.h ├── FWLIB/ # 标准外设库v3.5(stm32f10x_spi.c等),已预编译为静态库 ├── USER/ │ ├── main.c # 系统入口:时钟配置→GPIO初始化→SPI初始化→RC522初始化→主循环 │ ├── rfid.c # **核心驱动**:SPI读写封装、寄存器配置、防冲突算法实现 │ ├── rfid.h # 寄存器宏定义(如`MFRC522_REG_COMMAND = 0x01`)、函数声明 │ ├── delay.c # 微秒级延时(SysTick实现),用于RF场稳定等待(如`Delay_us(100)`) │ └── usart.c # 串口1初始化(115200bps),仅用于调试输出,可整块删除不影响刷卡功能 ├── OUTPUT/ # 编译输出(.axf/.hex/.lst),无需关注 └── Listings/ # 启动文件汇编列表,调试时查异常向量用注意:rfid.c是唯一与RC522强耦合的文件,其他模块(如LED控制、蜂鸣器驱动)全部通过rfid.h中定义的状态回调函数接入,例如:
// rfid.h 中声明 extern void Rfid_CardDetected(uint8_t *uid, uint8_t uid_len); extern void Rfid_NoCard(void); // main.c 中实现(你自己的业务逻辑) void Rfid_CardDetected(uint8_t *uid, uint8_t uid_len) { BEEP_ON(); // 蜂鸣器响 LED_GREEN_OFF(); // 绿灯灭 LED_RED_ON(); // 红灯亮 printf("UID: "); for(uint8_t i=0; i<uid_len; i++) printf("%02X ", uid[i]); printf("\r\n"); }这种解耦让你替换LED为继电器、把串口输出改为OLED显示,只需修改main.c中的回调函数,不用碰rfid.c一行代码。
2.3 SPI初始化关键参数:为什么分频系数必须是9?
RC522支持最高10MHz SPI时钟,但STM32F103C8T6的SPI1挂载在APB2总线上(默认72MHz)。若直接设为SPI_BaudRatePrescaler_2(36MHz),RC522会因时序违例拒绝响应。实测稳定工作的最大频率为8MHz,对应分频系数计算如下:
// spi.c 中 SPI1 初始化片段 SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 使能SPI1时钟 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_High; // 空闲时钟高电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 第二个边沿采样(RC522要求) SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_9; // 72MHz / 9 = 8MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);注意:
SPI_CPOL和SPI_CPHA必须设为High+2Edge,这是RC522数据手册第12页明确规定的SPI模式(Mode 3)。若设为默认的Low+1Edge,读出的UID全是0xFF,且PcdRequest()返回MI_ERR。
3. 核心驱动拆解:rfid.c中五个关键函数如何协作完成一次刷卡
3.1PcdReset():不是简单拉低RST引脚,而是按手册执行完整复位序列
RC522上电后需执行特定寄存器写入序列才能进入工作状态,仅靠硬件RST不够。本项目PcdReset()函数严格遵循数据手册第18页“Power-on reset sequence”:
// rfid.c void PcdReset(void) { // 步骤1:软复位(写CommandReg=0x0F) WriteRawRC(MFRC522_REG_COMMAND, PCDCMD_RESET); // 步骤2:等待振荡器稳定(至少5ms) Delay_ms(5); // 步骤3:关闭所有定时器(写TModeReg=0x00, TPrescalerReg=0x00, TReloadRegH/L=0x00) WriteRawRC(MFRC522_REG_TMODE, 0x00); WriteRawRC(MFRC522_REG_TPRESCALER, 0x00); WriteRawRC(MFRC522_REG_TRELOADHIR, 0x00); WriteRawRC(MFRC522_REG_TRELOADLOR, 0x00); // 步骤4:配置RF场(写TxControlReg=0x03,启用天线) WriteRawRC(MFRC522_REG_TXCONTROL, 0x03); // 步骤5:等待RF场建立(至少1ms) Delay_ms(1); }逻辑说明:
WriteRawRC()是底层SPI写函数,先拉低NSS,再发送地址+数据,最后拉高NSS;PCDCMD_RESET宏定义为0x0F,对应MFRC522的“Soft Reset”命令;- 关闭定时器是为了防止复位过程中意外触发中断;
TxControlReg=0x03表示启用TX1和TX2引脚(即开启RF发射场),若此处写错,卡片永远无法被激活。
3.2PcdRequest():状态机式轮询,比中断更可靠
很多教程用EXTI中断监听IRQ引脚,但实际部署中IRQ易受电源噪声干扰误触发。本项目采用纯轮询,PcdRequest()函数通过三次状态检查确保卡片真实存在:
// rfid.c uint8_t PcdRequest(uint8_t req_code, uint8_t *TagType) { uint8_t status; uint8_t unLen = sizeof(uint8_t) * 2; // 步骤1:清空FIFO并设置命令为REQA(0x26)或WUPA(0x52) WriteRawRC(MFRC522_REG_BITFRAMING, 0x07); // TxLastBits = 7, RxLastBits = 7 ClearBitMask(MFRC522_REG_STATUS2, 0x08); // 清除MFINDEX标志 WriteRawRC(MFRC522_REG_COMMAND, PCDCMD_IDLE); // 步骤2:发送请求命令(REQA用于寻卡,WUPA用于唤醒休眠卡) WriteRawRC(MFRC522_REG_COMMAND, req_code); // 步骤3:轮询等待卡片响应(最多100次,每次延时20us) for(uint8_t i=0; i<100; i++) { status = ReadRawRC(MFRC522_REG_STATUS2); if(status & 0x08) break; // 检查MFINDEX位是否置位 Delay_us(20); } if(!(status & 0x08)) return MI_ERR; // 超时未检测到卡片 // 步骤4:读取卡片返回的ATQA(2字节,标识卡片类型) status = PcdRead(PICC_REQA_OR_WUPA, TagType, &unLen); if(status != MI_OK) return status; return MI_OK; }参数说明:
req_code:PICC_REQA(0x26)用于普通寻卡,PICC_WUPA(0x52)用于唤醒低功耗卡;TagType:输出参数,存储ATQA值(如0x00 0x04表示Mifare Classic 1K);unLen:输入输出参数,传入缓冲区长度,返回实际读取字节数;- 关键点在于
MFRC522_REG_STATUS2的MFINDEX位——只有当卡片返回有效响应时该位才被硬件置1,比读FIFODataReg更可靠。
3.3PcdAnticoll():防冲突循环的边界处理,避免UID读取错位
多张卡同时靠近时,MFRC522需执行防冲突(Anticollision)流程获取唯一UID。本项目PcdAnticoll()严格实现ISO14443-3标准的bit-by-bit防冲突:
// rfid.c uint8_t PcdAnticoll(uint8_t *serNum) { uint8_t status; uint8_t i, j, snr_check = 0; uint8_t serNumBuf[5]; // 存储4字节UID + 1字节BCC // 步骤1:清空FIFO,设置命令为Anticollision ClearBitMask(MFRC522_REG_STATUS2, 0x08); WriteRawRC(MFRC522_REG_COMMAND, PCDCMD_IDLE); WriteRawRC(MFRC522_REG_BITFRAMING, 0x00); // TxLastBits = 0 // 步骤2:发送Anticollision命令(0x93 + 0x20) serNumBuf[0] = PICC_ANTICOLL1; serNumBuf[1] = 0x20; status = PcdComMF522(PCD_TRANSCEIVE, serNumBuf, 2, serNumBuf, &i); if(status != MI_OK) return status; // 步骤3:解析返回的UID(4字节)和BCC校验码 if(i != 5) return MI_ERR; // 必须返回5字节(4UID+BCC) for(j=0; j<4; j++) { serNum[j] = serNumBuf[j]; snr_check ^= serNumBuf[j]; } if(snr_check != serNumBuf[4]) return MI_ERR; // BCC校验失败 return MI_OK; }逻辑说明:
PICC_ANTICOLL1宏定义为0x93,是Mifare Classic的防冲突命令;PcdComMF522()是通用收发函数,负责将命令+数据写入FIFO,触发PCD_TRANSCEIVE命令;- BCC(Block Check Character)是前4字节UID的异或值,用于校验传输完整性;
- 若
i != 5,说明MFRC522未正确返回完整UID,常见于SPI时序错误或天线匹配不良。
4. 常见问题排查:五条血泪经验,专治“刷卡没反应”
4.1 现象:串口输出UID: FF FF FF FF,蜂鸣器不响
原因:SPI读取时序错误,MFRC522返回默认值0xFF。根本原因是SPI_CPHA配置错误——RC522要求在时钟第二个边沿采样,但代码中误设为SPI_CPHA_1Edge(第一个边沿)。
解决:检查spi.c中SPI_InitStructure.SPI_CPHA必须为SPI_CPHA_2Edge,重新编译下载。
4.2 现象:卡片靠近时LED闪烁但串口无输出,PcdRequest()始终返回MI_ERR
原因:RF场未建立。PcdReset()中TxControlReg写入值错误,或硬件天线未焊接。
解决:用万用表测量RC522的TX1/TX2引脚对地电压,正常应有2~3V交流信号;若为0V,检查WriteRawRC(MFRC522_REG_TXCONTROL, 0x03)是否被执行,以及PCB天线走线是否断开。
4.3 现象:单张卡可识别,两张卡同时靠近时程序卡死在for循环
原因:PcdAnticoll()中未处理防冲突失败场景。当多卡竞争时,MFRC522可能返回MI_ERR,但主循环未做超时退出,导致死循环。
解决:在PcdAnticoll()调用处增加重试计数器,例如:
for(uint8_t retry=0; retry<3; retry++) { if(PcdAnticoll(uid_buf) == MI_OK) break; Delay_ms(10); } if(retry == 3) return; // 重试3次失败,跳过本次识别4.4 现象:偶尔识别成功,但UID最后一位总是错(如应为9A却读成9B)
原因:BCC校验未启用或校验逻辑错误。MFRC522在防冲突阶段返回的第5字节是BCC,必须与前4字节异或校验。
解决:确认PcdAnticoll()函数中snr_check ^= serNumBuf[j]循环覆盖j=0~3,且比较snr_check != serNumBuf[4]。若跳过校验,错误UID会被当作有效数据处理。
4.5 现象:Keil编译报错undefined symbol RCC_APB2PeriphClockCmd
原因:FWLIB库未正确添加到工程。虽然目录中有FWLIB/,但Keil的“Options for Target → C/C++ → Include Paths”未包含FWLIB/inc路径,或FWLIB/src未加入Source Group。
解决:右键Project → “Manage → Components” → 确保STM32F10x_StdPeriph_Driver已勾选;或手动在Options中添加.\FWLIB\inc到Include Paths,并将.\FWLIB\src\stm32f10x_spi.c等文件拖入Source Group。
5. 二次开发实战:从刷卡触发LED到构建简易门禁系统
5.1 扩展UID白名单校验:三步替换Rfid_CardDetected()
原项目仅打印UID,实际门禁需判断是否授权。白名单可存于Flash或外部EEPROM,这里以Flash模拟(实际项目请用FLASH_Unlock()+FLASH_ProgramWord()):
// 定义白名单(最多10张卡) #define MAX_CARDS 10 typedef struct { uint8_t uid[4]; uint8_t valid; } CardRecord_t; CardRecord_t g_white_list[MAX_CARDS] = { {{0x04, 0x56, 0x78, 0x9A}, 1}, // 授权卡1 {{0xAB, 0xCD, 0xEF, 0x01}, 1}, // 授权卡2 {{0x00, 0x00, 0x00, 0x00}, 0}, // 未使用项 }; // 修改 Rfid_CardDetected 函数 void Rfid_CardDetected(uint8_t *uid, uint8_t uid_len) { if(uid_len != 4) return; // 线性查找白名单(O(n)复杂度,10张卡足够快) uint8_t is_authorized = 0; for(uint8_t i=0; i<MAX_CARDS; i++) { if(g_white_list[i].valid) { if(memcmp(uid, g_white_list[i].uid, 4) == 0) { is_authorized = 1; break; } } } if(is_authorized) { BEEP_ON(); LED_GREEN_ON(); LED_RED_OFF(); printf("Access granted\r\n"); // 此处可加继电器驱动:GPIO_ResetBits(GPIOB, GPIO_Pin_1); } else { BEEP_LONG(); // 长鸣提示拒绝 LED_GREEN_OFF(); LED_RED_ON(); printf("Access denied\r\n"); } }提示:Flash写入有擦除寿命限制(约10万次),若需频繁更新白名单,务必用外部I2C EEPROM(如AT24C02),并在
rfid.c中封装Eeprom_Write()函数。
5.2 添加刷卡记录日志:用环形缓冲区避免阻塞主循环
串口打印UID会占用大量CPU时间,影响实时性。改用环形缓冲区异步记录,主循环只负责存入,另起一个低优先级任务(或SysTick中断)负责发送:
// log.h #define LOG_BUF_SIZE 64 typedef struct { uint8_t buffer[LOG_BUF_SIZE][8]; // 每条记录:4字节UID + 2字节时间戳 + 1字节状态 + 1字节结束符 uint16_t head; uint16_t tail; } LogBuffer_t; extern LogBuffer_t g_log_buffer; // log.c void LogCardEvent(uint8_t *uid, uint8_t status) { if((g_log_buffer.head + 1) % LOG_BUF_SIZE == g_log_buffer.tail) return; // 缓冲区满,丢弃 uint8_t *p = g_log_buffer.buffer[g_log_buffer.head]; memcpy(p, uid, 4); *(uint16_t*)(p+4) = GetSysTick(); // 获取毫秒级时间戳 p[6] = status; // 0=granted, 1=denied p[7] = 0xFF; // 结束标记 g_log_buffer.head = (g_log_buffer.head + 1) % LOG_BUF_SIZE; } // 在 SysTick_Handler() 中添加(每10ms执行一次) void SysTick_Handler(void) { static uint16_t tick_count = 0; tick_count++; if(tick_count >= 10) { // 每100ms检查一次日志 tick_count = 0; if(g_log_buffer.tail != g_log_buffer.head) { uint8_t *p = g_log_buffer.buffer[g_log_buffer.tail]; printf("LOG:%02X%02X%02X%02X,%04d,%d\r\n", p[0],p[1],p[2],p[3], *(uint16_t*)(p+4), p[6]); g_log_buffer.tail = (g_log_buffer.tail + 1) % LOG_BUF_SIZE; } } }5.3 移植到STM32F4系列:仅需修改三处硬件抽象层
F4系列用HAL库,但rfid.c逻辑完全复用。关键修改点:
| 文件 | 原F1代码 | F4适配代码 | 说明 |
|---|---|---|---|
rfid.h | #include "stm32f10x.h" | #include "stm32f4xx.h" | 头文件切换 |
rfid.c | SPI_I2S_SendData(SPI1, data) | HAL_SPI_Transmit(&hspi1, &data, 1, 100) | HAL SPI发送,超时设100ms |
delay.c | SysTick_Config(SystemCoreClock / 1000000) | HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000000) | SysTick初始化适配 |
注意:F4的SPI时钟源为APB2(通常100MHz),需重新计算分频系数。若保持8MHz,则
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128(100MHz/128≈781kHz,需实测调整)。
从那以后我每次接到新硬件模块,都强制走一遍“数据手册寄存器表→原理图引脚对照→示波器抓SPI波形→逐字节比对响应数据”的四步验证法。RC522看似简单,但它的寄存器多达64个,任何一个配置错误都会让整个系统变成黑匣子。这份代码的价值不在功能多炫酷,而在于每一步操作都有据可查——WriteRawRC()调用哪一页手册、Delay_us(100)对应哪个时序参数、PcdAnticoll()的5字节结构怎么来的,全都钉死在注释里。希望帮到你。
本文还有配套的精品资源,点击获取