51单片机RFID门禁系统制作:MFRC522读卡器全套教程

发布时间:2026/9/8 10:53:48
51单片机RFID门禁系统制作:MFRC522读卡器全套教程 简介基于51单片机的RFID完整工程资源主要面向嵌入式初学者和物联网应用开发者帮助解决如何在经典8051平台上搭建一套可用的RFID读写系统。内容围绕RFID模块的选型与硬件接口展开涉及MFRC522等常见读写模块的SPI通信接线、命令收发、中断响应和标签数据解析并兼顾ISO/IEC 14443等协议的底层理解包括错误检测与处理机制的实现思路。程序结构包含驱动层、主控制逻辑、示例应用以及必要的配置与说明文档压缩包大小15.04MB整体设计较规范便于直接移植或二次开发。目前这套程序在CSDN已有1657人学习学习者可借此快速掌握51单片机与RFID模块的联调思路减少从零调试的弯路。对于门禁考勤、智能仓储、资产管理等需要非接触式识别的场景这份完整程序具有较高的参考价值。 51单片机RFID项目做完整了前后调了差不多两天把MFRC522模块、LCD1602显示、蜂鸣器提示、读卡写卡全部跑通。这篇文章把整个项目的硬件选型、接线方式、代码分层、核心协议流程和踩坑记录完整整理出来适合正在做51单片机课设、电赛训练或者想入门RFID开发的读者参考。1. 项目概述51单片机RFID能做什么RFID射频识别在生活中太常见了食堂饭卡、门禁卡、图书馆借书卡都属于13.56MHz频段的非接触式IC卡。这个项目做的就是让51单片机自己当读卡器把卡号读出来、显示在LCD屏幕上同时根据卡号做出不同响应比如合法卡亮绿灯、非法卡亮红灯并蜂鸣器报警。整体功能就是一个简化版门禁系统的读卡端。用51单片机做RFID读取很多人第一反应是“51性能够吗”。实际上MFRC522模块支持SPI接口通信51用IO口模拟SPI时序完全能跑而且RFID协议栈里最复杂的寻卡、防碰撞、认证都是在RC522芯片内部完成的51单片机只需要通过SPI读写寄存器、按照命令序列发送指令就行对主控性能的要求并不高。项目最终的硬件成本很低STC89C52RC单片机最小系统板一块MFRC522读卡模块一个LCD1602显示屏一个蜂鸣器和LED若干再加几张IC卡S50卡最好也就是M1卡。整个搭建下来五十块钱以内可以搞定非常适合学生党练手。这套方案适合三类人正在学51单片机但不知道做什么项目的人用它把SPI通信、中断、状态机这些知识点串起来做毕业设计或者课程设计需要RFID相关题目的学生代码和原理图都能直接改想给实验室或者宿舍做个简易门禁的爱好者扩展一下继电器就能控制电锁。2. 硬件连接与引脚分配2.1 MFRC522模块引脚功能说明MFRC522模组常见的引脚有8个SDA片选、SCK时钟、MOSI主出从入、MISO主入从出、IRQ中断请求、GND、RST复位、VCC电源。这里最重要的一点是电压匹配。MFRC522芯片的工作电压是3.3V而51单片机是5V逻辑电平。市面上绝大多数RC522模块板载了3.3V稳压芯片和电平转换电路可以直接接5V供电和5V逻辑引脚但某些精简版模块没有做电平转换接线前最好查一下模块原理图。判断模块是否支持5V直连有个简单方法看板上有没有AMS1117-3.3这颗稳压芯片有的话VCC可以接5V再看MISO引脚有没有串接电阻或者三极管转换电路没有的话建议在51的TX和RC522的MISO之间串一个1k电阻分压保护。2.2 51单片机与RC522接线表以STC89C52RC为例我实测通过的接线方案如下RC522引脚51单片机引脚说明SDAP2.0片选信号低电平有效SCKP2.1SPI时钟由51产生MOSIP2.2主机发送数据给RC522MISOP2.3RC522返回数据给主机RSTP2.4复位信号低电平复位IRQ不接本设计使用轮询方式不使用中断VCC5V模块稳压后供电3.3VGNDGND共地必须接LCD1602的接线是标准的4位模式RS接P0.0RW接P0.1EN接P0.2DB4-DB7接P0.4-P0.7P0口外接10k上拉排阻。蜂鸣器用NPN三极管S8050驱动基极接P1.0发射极接地集电极接蜂鸣器负极蜂鸣器正极接5V。硬件连接有个容易忽略的点RC522模块的电源引脚旁边有滤波电容但面包板供电时容易引入高频噪声实测表现为读卡偶尔失败、卡号跳变。解决方法是VCC和GND之间并一个100uF电解电容和一个104瓷片电容尽量靠近模块引脚放置。这个细节让我少走了很多弯路。3. 软件架构与核心协议流程3.1 代码分层设计很多人写单片机程序习惯把所有代码堆在一个main.c里项目小的时候无所谓但RFID程序涉及SPI驱动、RC522命令封装、LCD显示、主逻辑四部分全堆在一起后期调试会很痛苦。我采用的是模块化设计整个工程分成四个文件main.c主函数和状态机负责整体调度rc522.c / rc522.hRC522底层的寄存器读写和命令封装spi_soft.c / spi_soft.h软件模拟SPI时序lcd1602.c / lcd1602.hLCD显示驱动分层的核心逻辑是main不直接操作SPI引脚只调用RC522提供的高层API寻卡、读卡号、读块数据RC522层不关心数据最终显示在哪只负责和模块交互LCD层完全独立谁需要显示就调用谁。这个设计的优势在调试时特别明显。读不到卡的时候先怀疑是硬件问题还是驱动问题只需要在spi_soft.c里加几个测试函数确认时钟和MOSI波形对不对不用在主逻辑里来回翻代码。3.2 ISO14443A协议的核心流程MFRC522支持的通信协议是ISO14443A也就是M1卡Mifare S50使用的标准。一次完整的读卡操作顺序是固定的寻卡PcdRequest向天线区域发送请求命令检测是否有卡片进入防碰撞PcdAnticoll当多张卡同时进入时通过算法选出一张卡的4字节序列号选卡PcdSelect选中这张卡建立通信密码认证PcdAuthState验证扇区密码M1卡默认密码是FFFFFFFFFFFF读写块PcdRead / PcdWrite对指定扇区的数据块进行操作这个流程必须在程序里严格按照顺序执行跳过任何一步都会失败。比如有些新手直接调用Read函数读卡号发现返回错误码就是因为没有先做寻卡和认证。如果只是读取卡号实现门禁功能只需要执行到第3步拿到4字节的卡片序列号就行。这里有个细节M1卡的4字节序列号不是完整UID完整UID是4字节序列号加1字节校验位通常情况下读前4字节即可显示出来转成十六进制就是大家常见的8位卡号。4. 核心代码实现详解4.1 软件SPI驱动51单片机没有硬件SPI外设用IO口模拟是最常见的做法。SPI通信模式有四种RC522工作在Mode 0时钟空闲为低电平数据在上升沿采样下降沿输出。// spi_soft.c void SPI_Init(void) { SPI_SCK 0; // CPOL0时钟空闲低电平 SPI_CS 1; // 片选默认高电平 } u8 SPI_ReadWriteByte(u8 byte) { u8 i, temp 0; for(i 0; i 8; i) { // 先发数据在上升沿之前拉高/拉低MOSI if(byte 0x80) SPI_MOSI 1; else SPI_MOSI 0; byte 1; // 时钟上升沿主机发送数据的同时从机输出数据 SPI_SCK 1; // 读取MISO引脚获取从机返回的1位数据 temp 1; if(SPI_MISO) temp | 0x01; // 时钟下降沿为下一个周期做准备 SPI_SCK 0; } return temp; }软件SPI相比硬件SPI的优点是引脚任意、实现透明缺点是速度受限于IO翻转速率。51单片机工作在12MHz晶振时模拟出的SPI时钟大约在几百kHz级别对RC522来说完全够用实测一次完整的寻卡流程不超过几十毫秒。有个容易出错的地方SPI_CS片选信号必须在每次通信之前拉低通信结束拉高。很多人只在初始化时设置一次片选后面读写寄存器全都不管片选导致数据错乱。RC522的片选是低有效每次操作前都要重新拉低。4.2 RC522寄存器读写封装RC522的寄存器是8位地址加8位数据通过SPI的MOSI发送地址字节MISO读取数据字节。地址字节的最高位决定操作类型写寄存器时最高位为0读寄存器时最高位为1。// rc522.c void RC522_WriteRegister(u8 addr, u8 value) { SPI_CS 0; SPI_ReadWriteByte((addr 1) 0x7E); // 写命令地址低7位左移一位 SPI_ReadWriteByte(value); SPI_CS 1; } u8 RC522_ReadRegister(u8 addr) { u8 value; SPI_CS 0; SPI_ReadWriteByte(((addr 1) 0x7E) | 0x80); // 读命令最高位置1 value SPI_ReadWriteByte(0x00); SPI_CS 1; return value; }这里需要注意地址换算的细节。RC522的寄存器地址在数据手册里通常写成8位格式比如Command寄存器地址是0x01但在SPI协议里这个地址实际上是被拆成两部分高6位是寄存器地址低1位是地址增量标志再加上最高1位的读写标志。所以实际发送的字节是地址左移1位再按读写需求拼接最高位。第一次写驱动时容易忽略这个移位导致读写全部无效。4.3 寻卡、防碰撞、选卡函数实现这三个函数是RC522命令层的核心直接用命令字操作RC522的FIFO缓冲区FIFODataReg地址0x01和CommandReg地址0x01。// 寻卡检测天线范围内是否有卡片 u8 RC522_PcdRequest(u8 req_mode, u8 *tag_type) { u8 status; RC522_WriteRegister(ComIrqReg, 0x7F); // 清除所有中断标志 RC522_WriteRegister(CommandReg, PCD_IDLE); // 先进入空闲状态 RC522_WriteRegister(FIFOLevelReg, 0x80); // 清空FIFO缓冲区 RC522_WriteRegister(FIFODataReg, req_mode); // 发送请求命令0x52或0x26 RC522_WriteRegister(CommandReg, PCD_TRANSCEIVE); // 启动发送接收 RC522_WriteRegister(BitFramingReg, 0x07); // 发送7位数据 // 等待通信完成 status RC522_WaitForCompletion(); if(status ! MI_OK) return status; tag_type[0] RC522_ReadRegister(FIFODataReg); // 读取卡片应答的ATQA return MI_OK; } // 防碰撞获取4字节卡号序列号 u8 RC522_PcdAnticoll(u8 *serial) { u8 i, status; RC522_WriteRegister(BitFramingReg, 0x00); RC522_WriteRegister(CommandReg, PCD_IDLE); RC522_WriteRegister(FIFOLevelReg, 0x80); RC522_WriteRegister(FIFODataReg, PICC_ANTICOLL); // 命令字0x93 RC522_WriteRegister(FIFODataReg, 0x20); // 发送完整的2字节命令 u8 cmd PCD_TRANSCEIVE; RC522_WriteRegister(CommandReg, cmd); RC522_WriteRegister(BitFramingReg, 0x00); status RC522_WaitForCompletion(); if(status ! MI_OK) return status; for(i 0; i 4; i) serial[i] RC522_ReadRegister(FIFODataReg); RC522_WriteRegister(CommandReg, PCD_IDLE); return MI_OK; }寻卡命令有两个参数0x52表示寻所有卡0x26表示寻未休眠的卡。常规的单卡读取用0x52就行实测发现用0x26在某些场合响应更快但可能漏掉刚被休眠的卡片。等待通信完成的RC522_WaitForCompletion函数是一个超时循环不断读取ComIrqReg寄存器判断是否收到中断标志同时检查错误标志寄存器ErrReg。这里必须加超时退出否则如果天线区域没有卡程序会一直死等整个系统就像卡住了一样。大约100ms的超时比较合理。我设置的是连续读取500次寄存器约等于几毫秒的等待窗口没有卡的时候快速返回超时错误不会影响主循环里其他任务的执行。4.4 主程序查询卡号并显示主程序的工作流程很简单初始化LCD和RC522然后循环执行寻卡、防碰撞、显示卡号。// main.c 核心逻辑 void main() { u8 status; u8 card_serial[4]; LCD_Init(); SPI_Init(); RC522_Init(); LCD_ShowString(0, 0, RFID Reader); LCD_ShowString(0, 1, Place Card...); while(1) { // 寻卡 status RC522_PcdRequest(0x52, tag_type); if(status ! MI_OK) { DelayMs(50); continue; } // 防碰撞获取卡号 status RC522_PcdAnticoll(card_serial); if(status ! MI_OK) { DelayMs(50); continue; } // 显示卡号 LCD_ShowString(0, 1, UID:); for(i 0; i 4; i) { LCD_ShowHex(4 i * 2, 1, card_serial[i]); } // 蜂鸣器提示 Beep_On(); DelayMs(100); Beep_Off(); // 防止重复读同一张卡加延时 DelayMs(500); } }这里必须加一个去重复处理。如果循环太快同一张卡放在天线区域会被反复读到LCD上的卡号会不断刷新并伴随蜂鸣器响个不停。最简单的办法是读取成功后延时500ms再继续寻卡。更完善的做法是缓存上一次的卡号如果相同就只更新成功率不响蜂鸣器不同才提示。4.5 密码认证与块数据读写如果需要进一步验证卡片的某个扇区密码或者读写扇区里的数据就需要执行认证和读写块操作。M1卡有16个扇区每个扇区有4个块每个块16字节。扇区0的块0存放制造商代码和UID通常是只读的。真正存数据要从其他扇区开始。// 密码认证对指定扇区进行A/B密钥认证 u8 RC522_PcdAuthState(u8 auth_mode, u8 addr, u8 *key, u8 *serial) { u8 i, status; RC522_WriteRegister(CommandReg, PCD_IDLE); // 写入认证命令到FIFO RC522_WriteRegister(FIFODataReg, auth_mode); // 0x60KeyA0x61KeyB RC522_WriteRegister(FIFODataReg, addr); // 块地址 for(i 0; i 6; i) RC522_WriteRegister(FIFODataReg, key[i]); // 6字节密钥 for(i 0; i 4; i) RC522_WriteRegister(FIFODataReg, serial[i]); // 4字节卡号 RC522_WriteRegister(CommandReg, PCD_AUTHENT); // 执行认证命令 status RC522_WaitForCompletion(); return status; }认证通过后就可以直接对块地址进行读写了u8 RC522_PcdRead(u8 addr, u8 *data) { u8 i, status; RC522_WriteRegister(CommandReg, PCD_IDLE); RC522_WriteRegister(FIFOLevelReg, 0x80); RC522_WriteRegister(FIFODataReg, PICC_READ); // 0x30 RC522_WriteRegister(FIFODataReg, addr); RC522_WriteRegister(CommandReg, PCD_TRANSCEIVE); RC522_WriteRegister(BitFramingReg, 0x00); status RC522_WaitForCompletion(); if(status ! MI_OK) return status; for(i 0; i 16; i) data[i] RC522_ReadRegister(FIFODataReg); return MI_OK; }M1卡的默认密钥是6个0xFF绝大多数新卡出厂就是这几个字节。有些门禁卡会改密用默认密钥认证会返回MI_AUTHENTERR错误这种卡除非知道密码否则读不了数据。不过门禁类应用只读卡号的话不需要认证因为卡号在防碰撞阶段就直接拿到了。5. 常见问题与排查记录5.1 读不到卡或者数据连接错误实际调试中最常见的现象是程序烧进去后LCD一直显示Place Card把卡放上去没反应。排查步骤按顺序做先看模块供电。用万用表量RC522模块的VCC和GND之间电压正常应该是稳定的3.3V。如果电压低于3.0V或者波动明显换一个独立5V电源试试不要用USB转TTL那根线的5V带载能力太弱。再看接线是否正确。重点检查SDA片选和RST复位两根线的连接RC522的RST引脚必须由单片机控制拉高悬空或直接接VCC会导致模块不工作。这是新手最容易犯的错。最后确认软件初始化序列。RC522_Init函数里除了写寄存器配置还包括一个软复位操作先写CommandReg为0x0F软复位命令再延时等待模块复位完成。如果软复位时序不对模块内部状态是混乱的。5.2 卡号不变或者所有卡都返回同一个号这个问题一般出在防碰撞后对返回值处理不正确。M1卡的防碰撞返回值在FIFO里是5个字节前4个是卡号第5个是校验码。如果程序里循环读取了包含校验码在内的5字节然后只显示前4字节看起来可能正常但如果读取顺序错了比如多读了一次就会把上一张卡的残留数据当成当前卡号。排查方法是串口打印原始数据把FIFO里的内容全部发到电脑上看。正常情况下应该是4字节卡号加1字节校验值校验值等于前4字节的异或结果。不符合这个规律说明SPI读取有时序问题试着把SPI_ReadWriteByte函数里两个延时加上降低通信速度看是否稳定。5.3 Keil编译报错code空间不足51单片机内部程序空间只有8KBSTC89C52是8KBSTC89C516是64KBRC522驱动加LCD1602驱动如果全开优化有时候会提示code空间溢出。解决办法有两个。第一是在Keil的Options for Target里选择优化等级把默认的Level 0改成Level 8Optimize for Speed或者Level 9Optimize for Size。LCD1602显示字符串函数如果用了const数组并且是全局的占空间比较大尽量用指针传递而非定义多个临时数组。第二个办法是换芯片用STC89C516或者STC12C5A60S2程序空间64KB随便折腾。实测STC12系列比STC89系列快很多因为它是1T内核一个时钟周期执行一条指令RFID读卡响应更快。5.4 蜂鸣器乱响或者LED闪烁异常蜂鸣器乱响大概率不是程序问题而是IO口初始化时的电平不确定。STC89系列单片机P1口默认是准双向IO上电时输出高电平。如果蜂鸣器设计成低电平触发上电瞬间蜂鸣器会长鸣一声。解决办法有两个把蜂鸣器改成高电平触发NPN三极管基极接IO口高电平导通或者在main函数第一步就把蜂鸣器IO拉低。另外要注意蜂鸣器驱动电流的问题直接用IO口推蜂鸣器可能电流不够响度很小用三极管放大是标准做法。继电器扩展的话线圈必须并联续流二极管1N4007否则断开瞬间的感应电动势可能击穿三极管。5.5 LCD1602显示乱码LCD1602花屏乱码百分之九十是初始化时序不对。4位模式下初始化函数必须严格按照延时顺序执行void LCD_Init(void) { DelayMs(15); // 上电等待15ms LCD_WriteCmd(0x30); // 发送8位模式命令 DelayMs(5); LCD_WriteCmd(0x30); DelayMs(5); LCD_WriteCmd(0x30); // 连续三次 LCD_WriteCmd(0x20); // 切换为4位模式 // 以下是4位模式的显示设置 LCD_WriteCmd(0x28); // 4位模式2行5x7点阵 LCD_WriteCmd(0x08); // 关闭显示 LCD_WriteCmd(0x01); // 清屏 LCD_WriteCmd(0x06); // 光标右移 LCD_WriteCmd(0x0C); // 开显示不闪烁 }很多人跳过了前面三次发送0x30的步骤直接用4位模式命令初始化结果上电后屏幕偶尔正常偶尔花屏。还有一个细节每条命令之间的延时不能太短清屏命令0x01需要等待1.5ms以上否则后续命令可能会被丢弃。6. 后续扩展建议这个项目做完之后可以往两个方向扩展。第一个是做完整门禁在现有基础上增加继电器模块当读到合法卡号时继电器吸合控制电磁锁开门非法卡号报警。这个扩展只需要在main函数里增加一个合法卡号数组比对成功后拉高一个IO口驱动继电器即可代码改动量很小。第二个是增加存储功能用AT24C02存储白名单卡号断电不丢失。这样添加新卡不需要重新烧录程序通过上位机串口发送指令就能往AT24C02里写入新卡号。这套方案在实际工程中使用得很普遍有机会可以单独写一篇AT24C02存储卡号的完整实现。对于有精力的朋友可以把LCD显示换成OLEDSSD1306I2C接口这样整机体积更小显示更灵活。RC522的SPI接口换成I2C模式模块上有跳线可以选择接线还能少两根线。最后提醒一句门禁系统涉及安全管理建议只用于学习实验和自用场景。如果做产品化要考虑到卡片复制风险和加密方案不要在产品里直接用默认密钥。我在实际操作中发现RFID调试最折磨人的往往不是代码本身而是芯片手册里那些看似不起眼的细节比如地址移位、FIFO清空时机、超时处理。把这些细节整理成文档以后再回头做类似项目能省很多时间。如果你也正在调RC522建议手边常备逻辑分析仪或者示波器哪怕是几十块钱的简易逻辑分析仪抓一次SPI波形就能定位大部分通信问题比自己对着手册猜效率高太多了。本文还有配套的精品资源点击获取