STM32F103ZET6驱动RC522实现RFID门禁系统:从SPI时序到白名单管理
2026/9/17 2:18:15 网站建设 项目流程

简介:STM32F103ZET6单片机RFID-RC522门禁控制系统资源,特别适合嵌入式初学者、电子设计爱好者以及准备物联网课程设计或毕业设计的开发者,可据此从零搭建一套基于13.56MHz射频识别的非接触式门禁控制方案,也能用于实验室门禁、智能柜管理等真实场景。资源压缩包约34.4MB,以函数库源码与开发板原理图文件为主要内容,体积小巧、便于下载研读,目前已有410人学习关注。源码部分详尽覆盖GPIO、时钟与SPI接口的初始化,MFRC522驱动读写、防冲突处理、卡号数据解析,以及根据识别结果执行门禁开闭的逻辑,同时包含读卡失败、通信异常时的错误处理,能帮助开发者理解数据从射频卡到单片机再到执行设备的完整流转过程,有效训练底层驱动与上层业务整合能力。原理图则清楚画出STM32F103ZET6与RC522模块、LED指示灯、按键、蜂鸣器等外围设备的连接关系,为硬件搭建、信号排查与后期功能扩展提供直接依据;软件与硬件结合的学习路径,既能提升嵌入式C语言与调试技能,也能积累物联网安防产品的实际工程经验,为后续更复杂的射频识别项目打下扎实基础。

1. 为什么这门禁要先读卡再门锁:RC522 在 STM32F103ZET6 上的核心问题

门禁系统是嵌入式开发里最容易做成“能跑就行”的项目:刷卡,显示卡号,继电器咔哒一声,完事。但等你真去调电路、看波形、核对时序时才会发现,RC522 读不到卡不是模块坏了,而是 SPI 速率、天线匹配或防碰撞流程没对上;等你把卡读出来了,又会发现直接比卡号根本不够用,MIFARE 卡的密钥机制、白名单存储、掉电重试这些问题才是把项目从演示变为“能用”的关键。这个标题里的关键词拆开看其实是三件事:STM32F103ZET6 这颗 64 引脚、512KB Flash 的大容量 MCU 提供了什么资源;RFID-RC522 这套 13.56MHz 读卡方案走的是哪种协议流程;“函数库版”又意味着代码基于 ST 标准外设库而不是 HAL,适合老工程迁移、考级毕设和裸机教学。适合谁?想做门禁、考勤、储物柜、实验室设备管理的人,尤其适合手上有 F103ZET6 开发板、想把 RC522 真正驱动起来而不是停留在点灯层面的人。

2. RC522 读取门禁卡:SPI 时序与 MIFARE 寻卡流程拆解

2.1 RC522 与 STM32F103ZET6 之间的总线选型

RC522 支持 SPI、I2C、UART 三种接口,但在 STM32F103ZET6 上做门禁,我通常直接选 SPI,原因有三个:RC522 的 SPI 从模式支持 10Mbit/s 时钟,读卡时整体占用很短;F103ZET6 的 SPI1 挂在 APB2 总线上,时钟可达 36MHz,分频后轻松覆盖 RC522 需求;SPI 线少(SCK、MOSI、MISO、NSS),在开发板原理图上排线方便。I2C 模式地址冲突多,UART 模式要额外配波特率,都没有 SPI 直白。

接线时注意 RC522 模块和开发板原理图上的引脚标号并不总是同名,常见的映射是:

RC522 引脚STM32F103ZET6说明
SDA(NSS)PA4SPI1_NSS,软件控制片选
SCKPA5SPI1_SCK
MOSIPA7SPI1_MOSI,连 RC522 的 SI
MISOPA6SPI1_MISO,连 RC522 的 SO
IRQ可不接轮询时悬空,中断时接 PA3
RSTPA8(或 PB0)复位引脚,用 GPIO 控制
3.3V / GND3.3V / GND必须 3.3V,不能接 5V 引脚

提示:RC522 模块上自带的电平转换电路输出电压是 3.3V,但部分模块的 MISO 引脚是 5V 容忍的。稳妥做法是全部按 3.3V 处理,避免长期运行损伤 STM32F103ZET6 的 PA6。

2.2 MIFARE 卡交互的五个标准步骤

RC522 读卡不是直接“读一下 UID”就结束,它内部要跑一组命令序列。这张卡是 NXP 的 MIFARE Classic 1K 或 S50 兼容卡,命令流程稳定如下:

  1. 寻卡 Request:发送PCD_REQA(0x26)或PCD_WUPA(0x52),得到 2 字节卡类型(如 0x4400 表示 Mifare 1K)。
  2. 防碰撞 Anticollision:发送PCD_ANTICOLL(0x93),返回 4 字节 UID 和 1 字节校验。
  3. 选卡 Select:发送PCD_SELECT(0x93 后接 UID),拿到卡容量和确认字节。
  4. 三次认证:发送PCD_AUTHENT,需要扇区号和密钥(默认是 6 字节 0xFF)。
  5. 读块或写块:PCD_READ(0x30)或PCD_WRITE(0xA0),此时才能访问数据块。

RC522 的命令寄存器是 0x01(CommandReg),写命令后再查询 0x02(ComIEnReg 的中断标志)或 0x05(Status2Reg 的 MFCrypto1On)来判断执行结果。初学者最容易跳过第 3 步直接读块,结果返回 MI_NOTAGERR。这是 MIFARE 的固有设计——选卡之后通信链路才锁定到这张卡上。

2.3 RC522 内部寄存器的读写时序

函数库版的实现核心是两个底层函数:写寄存器RC522_WriteReg和读寄存器RC522_ReadReg。SPI 通信时每条数据都由一个地址字节和一个数据字节组成,地址字节的最高位表示方向,0 为写、1 为读。写寄存器时把片选拉低,发送地址字节和数据字节,释放片选;读寄存器时先发送地址字节,再发送一个虚拟字节作为时钟驱动,同时从 MISO 采样返回数据。

// RC522 底层寄存器读写,SPI 模式,适用于标准外设库 uint8_t RC522_ReadReg(uint8_t addr) { uint8_t value; GPIO_ResetBits(RC522_NSS_PORT, RC522_NSS_PIN); // 片选拉低,进入通信 SPI_SendByte(RC522_SPI, ((addr << 1) & 0x7E) | 0x80); // 读标志位:最高位置 1 value = SPI_SendByte(RC522_SPI, 0x00); // 发送空字节,同时接收数据 GPIO_SetBits(RC522_NSS_PORT, RC522_NSS_PIN); // 片选拉高,通信结束 return value; } void RC522_WriteReg(uint8_t addr, uint8_t data) { GPIO_ResetBits(RC522_NSS_PORT, RC522_NSS_PIN); SPI_SendByte(RC522_SPI, (addr << 1) & 0x7E); // 写方向,最高位为 0 SPI_SendByte(RC522_SPI, data); GPIO_SetBits(RC522_NSS_PORT, RC522_NSS_PIN); }

地址为什么左移一位?因为 RC522 的 SPI 帧格式里地址占 6 位,方向位在第 7 位,后面还保留了一位,所以传入的寄存器地址(如 CommandReg 是 0x01)要先左移一位再拼方向位。这里的SPI_SendByte是一次完整的 SPI 收发函数,需要保证 F103 的 SPI1 已配置为主模式,否则 MISO 上采不到数。验证这两段代码是否正确,可以读 RC522 的 VersionReg(0x37),正常返回 0x92 或 0x12,返回其它值说明接线或 SPI 配置有错。

3. 函数库版工程骨架:GPIO 初始化与 SPI 参数怎么定

3.1 STM32F103ZET6 的 RCC、GPIO、SPI 初始化

函数库版指的是 ST 官方标准外设库(SPL),结构上是RCC_ConfigurationGPIO_ConfigurationSPI_Configuration三段式,没有 HAL 的MX_前缀,也没有 CubeMX 自动生成。对系统时钟初始化,外部晶振用 8MHz,PLL 倍频到 72MHz,APB1 总线 36MHz,APB2 总线 72MHz。SPI1 挂 APB2,所以 SPI 的时钟源就是 72MHz,分频系数最低是 2,对应 36MHz——这个值远高于 RC522 的 10MHz 上限,需要继续分频。

void SPI1_Configuration(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // RC522 是从机 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_8; // 72MHz / 8 = 9MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

3.2 三个容易让 RC522 沉默的参数细节

波特率预分频为什么选 8 而不是 2 或 4?RC522 规格书标称 SPI 时钟最高 10MHz,但天线电路和布线质量会压缩实际裕量,9MHz 比 5MHz 更接近临界值,时序余量小,PCB 布局差一点就可能出现“第一张卡能读、换张卡就卡死”的怪问题。我在正式项目里会用 8 分频(9MHz),在面包板飞线上用 16 分频(4.5MHz),稳定性优先。

CPOL 和 CPHA 的组合也很讲究。RC522 要求 SPI 空闲时钟为低、第一个时钟沿采样数据,对应 CPOL=Low、CPHA=1Edge,也就是 SPI_MODE0。如果你是从别的工程复制代码,先检查这两行,模式不配对时 RC522 的 VersionReg 一样能读出来,但收发数据会错位,命令响应永远是 0xFF 或超时。

GPIO 初始化里 SCK、MOSI、NSS 要配置为复用推挽输出(GPIO_Mode_AF_PP),MISO 配为上拉输入(GPIO_Mode_IPU)。NSS 特别提一句:虽然SPI_NSS_Soft使能后 F103 不再自动控制硬件 NSS,但 PA4 如果被其它外设复用,片选信号会串扰,所以要用 GPIO 单独控制 PA4 的输出电平,初始化时先置高。

3.3 最小工程验证:读版本号

搭好工程后,第一件事不是去读卡,而是读 RC522 的版本寄存器。把前面两个函数组合起来,在主函数里连续读三次 0x37 地址,串口打印输出。这一步能同时验证 SPI 时序、GPIO 映射、电源三样东西,比直接跑到寻卡那一步省事得多。

// 从 RC522 读取版本号,用于验证 SPI 通信是否打通 uint8_t version = 0; RC522_WriteReg(CommandReg, PCD_RESETPHASE); // 先复位 RC522 内部状态机 version = RC522_ReadReg(VersionReg); // 0x37 地址 printf("RC522 version: 0x%02x\r\n", version); // 正常为 0x92 或 0x12

RESETPHASE 命令的作用是把 RC522 内部寄存器恢复到上电默认值,这不是可省的步骤——模块上电后内部状态不确定,直接发寻卡命令可能卡在某个错误状态。版本号能读出来后再初始化天线(在 TxControlReg 里使能模拟前端),然后进入寻卡循环。如果版本号读出来是 0x00,优先检查 MOSI/MISO 是否接反,这是所有 SPI 设备调试里最频繁的接线事故。

4. 从读到放行:白名单校验与继电器动作的完整链路

4.1 设计一个能判断“放行还是拒绝”的状态机

门禁逻辑的层次要分清:RC522 驱动层负责拿到 UID,应用层负责判断这个 UID 是否有权限。如果在驱动层直接做判断,后面想扩展密码模式、刷卡加密码双重验证就会很别扭。我一般把整个刷卡到开门拆成五个状态:空闲(等待刷卡)、寻卡成功、防碰撞取 UID、认证扇区读取数据、比对后输出动作。

// 门禁主状态机:刷卡 → 取 UID → 查白名单 → 动作输出 uint8_t card_uid[4]; uint8_t status; while (1) { status = RC522_CheckCard(card_uid); // 寻卡 + 防碰撞 + 选卡,一步完成 if (status == MI_OK) { // 在 M1 卡的 0 扇区 0 块,前 4 字节就是 UID // 第 5 字节是卡号校验位,第 6~16 字节是厂商数据 // 比对时只取前 4 字节,不同品牌的卡 UID 长度有 4 字节和 7 字节两种 if (WhiteList_Check(card_uid) == 1) { Relay_Open(); // 打开电磁锁或继电器,持续 2 秒 OLED_ShowUID(card_uid); // 屏显放行状态 } else { Buzzer_Warn(); // 蜂鸣器响 3 声,提示无权限 } delay_ms(500); // 防止同一张卡重复触发 } }

RC522_CheckCard内部把协议那一节讲的五步都走完了。注意一个细节:delay_ms(500)这个延时防的是“卡贴在读卡器上不拿走,状态机反复读到同一张卡导致继电器频繁抖动”,不是防碰撞去重的替代品。如果你还想做得更严谨,可以记录上一次放行的 UID,在它持续紧贴期间不重复执行继电器动作,等卡离开后再复位比较位。没有电磁锁的工程通常会省略这个逻辑,继电器会非常吵。

4.2 白名单存哪:直接比对 UID 还是读 M1 卡扇区数据

门禁系统的常见做法有两种。第一种直接把卡号写死在代码数组里,适合几张卡、结构简单的场景;第二种把持卡人信息写入 M1 卡扇区,系统只验证扇区密钥,再用读到的数据做二次确认。我会优先推第二种——M1 卡的 UID 虽然出厂唯一,但用 PM3 等设备可以复制扇区数据,破解门槛不高。增加一层扇区认证,哪怕密钥仍然使用默认的 6 字节 0xFF,至少把“只认卡号”这种安全性极低的方案挡在门外。

M1 卡的扇区结构:一共 16 个扇区,每个扇区 4 个块,每块 16 字节。每个扇区的第 3 块是密钥块,存储 6 字节密钥 A 和 6 字节密钥 B,以及访问位。门禁里常用的位置是第 1 扇区、第 0 块,前 4 字节存用户编号,中间存权限等级,剩下作为备用。

块内偏移用途示例
0~3门禁号或用户 ID{0x01, 0x00, 0x00, 0x00}
4~5权限等级1 表示可通行
6~9有效期(时间戳)0,表示永不过期
10~15预留0xFF

认证扇区时用PCD_AUTHENT,需要知道密钥类型(KeyA 或 KeyB)和扇区号对应的块地址。扇区 1 的块地址是 4,这个换算常有人写错:扇区序号乘 4 就是该扇区第一个块的地址。读块要用PCD_READ命令,前面提到的第 4 步认证没有通过时,读出来的数据全部是 0。

4.3 继电器与电磁锁的驱动边界

RC522 是 3.3V 逻辑,继电器线圈是 12V 或 5V,中间必须有三极管或 ULN2003 做电平转换。这一步不接线问题不大,但如果直接在 PA8 上挂继电器,STM32F103ZET6 的 GPIO 驱动能力只有 8mA 左右,根本吸合不了继电器,还可能把引脚烧掉。标准做法是 GPIO→三极管基极限流电阻→继电器线圈→续流二极管→电源地。

函数库版里控制继电器的代码就是两个 GPIO 写电平的调用:

// 继电器开/关,参数 t 为吸合时间,单位毫秒 void Relay_Open(uint16_t t) { GPIO_SetBits(GPIOB, GPIO_Pin_5); // 三极管基极高电平,继电器吸合 delay_ms(t); // 电磁锁吸合时间 2000ms 左右 GPIO_ResetBits(GPIOB, GPIO_Pin_5); // 释放 }

继电器开多长时间要看门装的是什么。电插锁通常需要 1~2 秒的断电时间让人拉门;电磁锁则要保持通电,断电才开门,逻辑恰好相反——这时候 GPIO 的控制电平要反向,还要加一个互锁避免误判。开发板原理图上一般会画一个 LED 指示继电器状态,调试时看这个 LED 比听声音直观。

5. 设计门禁系统的 3 个进阶细节:掉电保存、看门狗与仿真排错

5.1 用 STM32F103ZET6 内部 Flash 保存卡号白名单

把白名单数组直接写在程序里,每次改卡号都要重新下载固件,这在交付场景里不可接受。用 F103ZET6 的内部 Flash 存放白名单更实际——它有 512KB 主 Flash,页大小 2KB,写一个扇区足够几百个卡号。函数库版里 Flash 操作的入口是FLASH_ProgramWordFLASH_ErasePage,写入前要解锁并清标志位:

// 从内部 Flash 读一张卡的白名单,addr 为 Flash 中的地址偏移 uint8_t WhiteList_Load(uint32_t addr, uint8_t *uid_buf) { uint32_t *p = (uint32_t *)addr; uid_buf[0] = (uint8_t)(p[0] & 0xFF); uid_buf[1] = (uint8_t)((p[0] >> 8) & 0xFF); uid_buf[2] = (uint8_t)((p[0] >> 16) & 0xFF); uid_buf[3] = (uint8_t)((p[0] >> 24) & 0xFF); return 1; }

注意几个点。Flash 写操作会阻塞 CPU,写之前必须关中断,否则程序会跑飞;写入一个 32 位数据的时间大约是几十微秒,完全可以接受。另外 Flash 擦写次数有限,虽然 F103 的 Flash 寿命通常在 1 万次以上,但频繁用按键“添加卡号”的场合,最好先判断卡号是否已存在,避免每次都触发擦写。

5.2 死循环与看门狗:RC522 通信卡死后的自救

RC522 的 SPI 通信偶尔会进入死状态——射频干扰或天线距离变化导致命令无响应,程序如果卡在等待中断响应的死等循环里,整个门禁就瘫了。在函数库版工程里加一个独立看门狗(IWDG)是成本最低的解决方案:每 500 毫秒喂狗一次,如果 RC522 卡死导致主循环停止,IWDG 自动复位 MCU,模块恢复初始状态。注意喂狗要在主循环末尾,而且 RC522 的重试时间要小于喂狗周期。

// 初始化独立看门狗,超时时间约 2 秒 IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_128); // 40kHz / 128 = 312.5Hz IWDG_SetReload(625); // 2 秒超时 IWDG_Enable(); // 主循环中: IWDG_ReloadCounter();

5.3 Proteus 仿真排错:函数库版工程能不能先不烧板子

如果你手里的 F103ZET6 开发板还没到,或者引脚接线图找不到了,可以先在 Proteus 里搭一个仿真工程。Proteus 8 及更新版本可以直接搜到 STM32F103ZET6 芯片,不需要额外装第三方库——选择“Pick Device”后输入“STM32F103ZET6”就能看到,前提是当前用的 Proteus 版本组件库较新。RC522 模块在 Proteus 里有现成模型,但需要注意:很多仿真模型不支持真实的射频时序,S50 卡的仿真数据和实体卡读出来的 UID 不完全一致,验证代码跑通后,还是要回到开发板原理图核对实物接线。

Keypad 输入法、串口助手轮询读卡、OLED 屏显,这三样是门禁调试时最常用的“眼睛”。仿真能帮你确立状态机逻辑,实物才能验证 RC522 的射频时序和继电器负载能力。无论是毕设答辩还是实际交付,板子上的天线走线、模块位置和铁质门框的距离,这些原理图上看不出来的物理因素,才是门禁可靠性真正的分水岭。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询