news 2026/10/19 3:02:36

STM32+RC522门禁原型:手撸SPI驱动实现稳定刷卡识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+RC522门禁原型:手撸SPI驱动实现稳定刷卡识别

简介:本资源是一套基于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则收不到响应,调试时第一怀疑点
NSSPA4片选信号低电平有效,必须由MCU主动拉低才能开始通信,不能悬空
RSTPB0复位控制项目中用软件复位(写寄存器),但硬件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.cSPI_I2S_SendData(SPI1, data)HAL_SPI_Transmit(&hspi1, &data, 1, 100)HAL SPI发送,超时设100ms
delay.cSysTick_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字节结构怎么来的,全都钉死在注释里。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 3:01:54

C# OpenCVSharp圆形检测实战:霍夫圆算法、边缘检测与亚像素精修

有段时间我的工作台上摆着一批金属垫片&#xff0c;项目要求很简单&#xff1a;在来料图像里快速找到每个垫片的圆心&#xff0c;把圆心坐标和半径发给后续机械臂。听着不难&#xff0c;真正开始做才发现&#xff0c;圆这种东西在照片里非常容易变形——光照一变&#xff0c;边…

作者头像 李华
网站建设 2026/10/19 3:01:31

iOS应用脱壳实战:从Mach-O加密原理到Frida工具链完整解析

做iOS逆向的朋友应该都听过一句话&#xff1a;拿到App之后&#xff0c;第一步是砸壳。往实际里讲&#xff0c;App Store下载的应用&#xff0c;可执行文件默认带着FairPlay DRM加密&#xff0c;用otool随手看一眼&#xff0c;cryptid那一栏几乎都是1。壳不脱&#xff0c;后面所…

作者头像 李华
网站建设 2026/10/19 3:01:30

STM32 MODBUS从站工程:8个功能码全覆盖的RTU协议栈实现

简介&#xff1a;面向需要掌握工业串行通信的嵌入式开发者&#xff0c;这是一套基于STM32的MODBUS RTU通信实例&#xff0c;覆盖0x03、0x04、0x06、0x10等全部常用功能码&#xff0c;适合从协议理解到工程落地的进阶学习。压缩包共787个文件、约11.14MB&#xff0c;包含HAL库相…

作者头像 李华
网站建设 2026/10/19 3:01:29

日志传输加密实战:Logstash-forwarder TLS证书配置与排障指南

维护过日志采集链路的人&#xff0c;对Logstash-forwarder应该不陌生。这个轻量级的日志转发器在当年几乎是日志管道的标配&#xff0c;盯着服务器上的日志文件&#xff0c;有新内容就打包发给中央Logstash。如今它已经被Filebeat取代&#xff0c;但这不妨碍它的安全设计思路至…

作者头像 李华
网站建设 2026/10/19 3:00:30

打造Claude技能包:从提示词工程到Skill Creator 2.0实战

很多人在第一次接触 Claude Skills 这个概念的时候&#xff0c;都会有和我一样的困惑&#xff1a;提示词明明已经写得很详细了&#xff0c;为什么每次遇到同类任务&#xff0c;还是要从头解释一遍&#xff1f;内容越长&#xff0c;模型越容易被无关信息干扰&#xff0c;行为越不…

作者头像 李华