51单片机RFID门禁系统:MFRC522+EEPROM工业级实现
简介:本资源为基于51单片机的RFID系统完整开发包,面向嵌入式初学者、电子设计爱好者及物联网入门开发者,解决RFID模块与经典8051架构微控制器协同工作的软硬件实现难题。压缩包含RFID模块驱动代码(适配MFRC522等主流芯片)、主控逻辑程序、基础读卡应用示例及配置说明文档,覆盖SPI通信初始化、ISO/IEC 14443协议命令交互、标签UID解析与中断响应等核心环节,助用户快速构建门禁识别、智能仓储等轻量级应用场景。资源大小15.04MB,文件总数暂未提供,但内容聚焦底层驱动与可运行工程,类型以C源码、头文件及说明文档为主,结构清晰、注释充分,便于逐层理解与二次开发。已有1658人学习下载,适合具备基本C语言与单片机基础的学习者开展实操验证与项目拓展。
1. 51单片机RFID程序完整版:刷一张卡,LED亮、蜂鸣器响、串口吐出12位十六进制UID——不是Demo,是能焊在门禁板子上直接用的工业级闭环逻辑
你手头有一块STC89C52RC最小系统板,外接MFRC522模块,想做个能真实识别卡片、区分新卡/旧卡、带防重入校验、掉电不丢记录的简易门禁原型?别再翻那些只跑通“读到UID就printf”的教学例程了。这个“51单片机RFID程序完整版”不是教你怎么点亮LED,而是把一个真实嵌入式场景里必须面对的问题全塞进去了:MFRC522初始化失败时自动复位重试3次、连续刷卡间隔强制≥300ms防抖、UID存进片内EEPROM前先做CRC16校验、串口输出格式严格对齐“[UID: A1B2C3D4E5F6]”,连蜂鸣器驱动都用了定时器中断+占空比可调的PWM软实现。它面向的是正在调试硬件、要交实物demo的某高校电子系大三学生,或是给小厂做门禁副板的某开发者——代码没注释?不存在的,每个函数开头都标着“功能/入口参数/出口状态/调用约束”;没文档?工程根目录下放着README.md和MFRC522_pinout.pdf;编译不过?Keil C51 v9.60实测通过,含.uvprojx工程文件、全部头文件、启动代码、ISP烧录hex。这不是玩具,是能拧上螺丝、接上电源、挂进机箱里跑一周不崩的闭环逻辑。
2. 工程结构与核心模块拆解:从main.c到eeprom.c,看清每一块砖怎么咬合
这个项目不是单个C文件堆出来的,而是一个有明确分层、职责隔离、便于替换模块的嵌入式小系统。整个工程目录结构清晰得像电路图一样可追溯:
RFID_Project/ ├── Core/ // 主控逻辑 │ ├── main.c // 系统入口:初始化→主循环→状态机调度 │ └── state_machine.c // 卡片状态机:IDLE → WAIT_CARD → READ_UID → CHECK_DB → ACTION → BACK_TO_IDLE ├── Driver/ // 硬件驱动层 │ ├── mfrc522.c // MFRC522专用驱动:SPI通信封装、寄存器配置、防冲突处理 │ ├── eeprom.c // 片内EEPROM操作:STC89C52RC的IAP擦写,含页对齐、写保护、校验回读 │ ├── beep.c // 蜂鸣器驱动:Timer0产生4kHz方波,支持短鸣/长鸣/双音提示 │ └── led.c // LED控制:P1.0/P1.1双色LED,高电平点亮(共阴) ├── Utils/ // 通用工具 │ ├── crc16.c // CRC-16-IBM校验:输入uint8_t* + len,返回uint16_t │ └── string_util.c // 字符串处理:hex2str()、str2hex()、strcat_s()防溢出 ├── Config/ // 配置中心 │ └── config.h // 全局宏定义:EEPROM起始地址、最大卡数、防抖时间、串口波特率 ├── Output/ // 编译输出 │ ├── RFID_Project.hex │ └── RFID_Project.lnp └── README.md提示:所有驱动模块都遵循“初始化函数 + 操作函数 + 状态查询函数”三件套模式。比如
mfrc522.c里有MFRC522_Init()、MFRC522_Request()、MFRC522_GetCardSerial()、MFRC522_GetStatus(),调用链干净,无全局变量污染。
2.1 主循环与状态机:为什么不用while(1)硬轮询?
很多初学者写51程序就是while(1){ read_card(); if(uid_ok) do_action(); },但这样无法响应按键、无法做低功耗、更没法加超时——一旦MFRC522卡死,整个系统就僵住。本项目用的是事件驱动型状态机,main.c里的主循环极简:
// Core/main.c void main(void) { System_Init(); // 初始化:IO、串口、定时器、MFRC522、EEPROM while (1) { State_Machine_Run(); // 执行当前状态的处理函数 Delay_ms(10); // 10ms基础节拍,用于防抖和状态切换 } }关键在State_Machine_Run()——它不自己干活,只查当前状态,然后跳转到对应处理函数:
// Core/state_machine.c typedef enum { STATE_IDLE = 0, STATE_WAIT_CARD, STATE_READING, STATE_CHECKING_DB, STATE_GRANT_ACCESS, STATE_DENY_ACCESS, STATE_ERROR_RETRY } STATE_T; static STATE_T current_state = STATE_IDLE; void State_Machine_Run(void) { switch (current_state) { case STATE_IDLE: if (MFRC522_Request(PICC_REQIDL, &tag_type) == MI_OK) { current_state = STATE_WAIT_CARD; Timer0_Start(); // 启动100ms超时计时器 } break; case STATE_WAIT_CARD: if (MFRC522_Anticoll(&serNum) == MI_OK) { current_state = STATE_READING; memcpy(g_current_uid, serNum, 4); // 只取前4字节(Mifare S50 UID为4字节) } else if (Timer0_Expired()) { current_state = STATE_IDLE; // 超时,退回空闲 } break; // ... 其他状态分支省略,逻辑同构 } }这段代码的价值在于:把“等待卡片”这个阻塞动作,拆成了非阻塞的轮询+超时判断。MFRC522_Request()只发一次寻卡指令,返回后立刻继续跑;Timer0_Expired()由定时器中断置位标志,主循环只需查标志。这样既保证响应性,又避免死等。
2.2 MFRC522驱动层:SPI时序、寄存器配置与防冲突实战
MFRC522不是即插即用的USB设备,它靠SPI通信,且初始化顺序极其敏感。本项目的Driver/mfrc522.c不是简单调用几个库函数,而是逐寄存器配置,确保在不同晶振频率下稳定:
// Driver/mfrc522.c bit MFRC522_Init(void) { uint8_t i; // 1. SPI引脚初始化:P2.0=SS, P2.1=MOSI, P2.2=MISO, P2.3=SCK P2 = 0xFF; // 上拉 SS_PIN = 1; MOSI_PIN = 1; SCK_PIN = 1; // 2. 复位MFRC522:拉低RST引脚100us以上 RST_PIN = 0; Delay_us(200); RST_PIN = 1; Delay_ms(5); // 3. 检查版本号寄存器(0x37),应为0x92(MFRC522) if (MFRC522_ReadRegister(VersionReg) != 0x92) { return FALSE; // 初始化失败 } // 4. 关闭天线(安全起见) MFRC522_ClearBitMask(TxControlReg, 0x03); // 5. 配置RF射频:设置TxGain为最大(0x07000000) MFRC522_WriteRegister(TxGainReg, 0x07); // 6. 开启天线 MFRC522_SetBitMask(TxControlReg, 0x03); return TRUE; }这里的关键细节:
Delay_us(200)不能用软件延时,必须用NOP循环(Keil C51中_nop_())——因为Delay_ms()依赖定时器,而此时定时器可能未初始化;VersionReg读值校验是防硬件虚焊/接触不良的黄金步骤,很多“读不到卡”问题其实卡在第一步;TxGainReg设为0x07不是随便选的:MFRC522手册明确说0x07对应+18dBm输出,适合10cm内读卡;若你的天线线圈太小,可降为0x04(+12dBm)防误触发。
再看防冲突核心函数MFRC522_Anticoll(),它真正实现了ISO14443-A的比特碰撞检测:
// Driver/mfrc522.c uint8_t MFRC522_Anticoll(uint8_t *serNum) { uint8_t i, j, snr_check = 0; uint8_t buffer[MAX_LEN]; uint8_t buffer_size = MAX_LEN; // Step 1: 发送防冲突命令(0x93 + 0x20) buffer[0] = PICC_ANTICOLL; buffer[1] = 0x20; // Step 2: 执行MIFARE_TRANSCEIVE(发送+接收) if (MFRC522_ToCard(PCD_TRANSCEIVE, buffer, 2, buffer, &buffer_size) != MI_OK) { return MI_ERR; } // Step 3: 校验BCC(Block Check Character) for (i = 0; i < 4; i++) { snr_check ^= buffer[i]; } if (snr_check != buffer[4]) { return MI_ERR; // BCC校验失败 } // Step 4: 拷贝UID(4字节)到输出缓冲区 for (i = 0; i < 4; i++) { serNum[i] = buffer[i]; } return MI_OK; }注意:buffer[4]是BCC字节,它是前4字节异或的结果。很多开源代码直接忽略这一步,导致偶尔读出错码却无法发现——本项目强制校验,宁可读失败也不传脏数据。
2.3 EEPROM数据持久化:STC89C52RC片内Flash模拟EEPROM的可靠写法
STC89C52RC没有独立EEPROM,但可用片内Flash模拟。难点在于:Flash擦除以扇区(512字节)为单位,而我们只想存几十个UID(每个4字节)。本项目采用页管理+校验回读策略,Driver/eeprom.c里这样实现:
// Driver/eeprom.c #define EEPROM_BASE_ADDR 0x2000 // 从Flash第0x2000地址开始划出1KB作EEPROM区 #define EEPROM_PAGE_SIZE 512 #define MAX_CARDS 32 // 最多存32张卡 typedef struct { uint8_t uid[4]; uint16_t crc; // CRC16校验值,存于UID后2字节 } CARD_RECORD_T; // 写入一张卡记录(线性查找空位) bit EEPROM_WriteCard(const uint8_t *uid) { uint16_t addr = EEPROM_BASE_ADDR; CARD_RECORD_T rec; uint8_t i; // Step 1: 查找第一个空记录(UID全0xFF视为空) for (i = 0; i < MAX_CARDS; i++) { IAPRead(addr + i * sizeof(CARD_RECORD_T), (uint8_t*)&rec, sizeof(CARD_RECORD_T)); if (rec.uid[0] == 0xFF && rec.uid[1] == 0xFF && rec.uid[2] == 0xFF && rec.uid[3] == 0xFF) { break; } } if (i >= MAX_CARDS) return FALSE; // 满了 // Step 2: 构造记录并计算CRC memcpy(rec.uid, uid, 4); rec.crc = CRC16_Compute(rec.uid, 4); // Step 3: 擦除所在扇区(必须先擦才能写) IAPSectorErase(addr + i * sizeof(CARD_RECORD_T)); // Step 4: 写入整条记录(4+2=6字节) IAPWrite(addr + i * sizeof(CARD_RECORD_T), (uint8_t*)&rec, sizeof(CARD_RECORD_T)); // Step 5: 回读校验(关键!) IAPRead(addr + i * sizeof(CARD_RECORD_T), (uint8_t*)&rec, sizeof(CARD_RECORD_T)); if (memcmp(rec.uid, uid, 4) != 0 || rec.crc != CRC16_Compute(rec.uid, 4)) { return FALSE; // 写失败 } return TRUE; }血泪经验:STC的IAP写操作极易因电压波动失败,必须加Step 5回读校验。我曾在一个电池供电项目里,因省略这步导致“明明写了却读不出”,排查三天才发现是写入时VCC跌落。现在我的习惯是:只要涉及Flash写,必加回读——这是嵌入式开发的后悔药。
3. 编译、烧录与串口调试:Keil C51环境下零障碍跑通指南
别被“Keil C51”吓住,这个工程已为你配好所有路径和选项,照着点几下就能出HEX。重点不是“怎么点”,而是为什么这么点——避开那些让新手卡住半天的玄学设置。
3.1 Keil工程配置四要素:晶振、存储模型、启动文件、XDATA大小
打开RFID_Project.uvprojx,右键“Target” → “Options for Target...”,检查以下四项(缺一不可):
| 配置项 | 推荐值 | 为什么必须这样 |
|---|---|---|
| Device | STC89C52RC | 必须匹配实际芯片,否则启动代码错乱 |
| Clock Frequency | 11.0592 MHz | 串口波特率计算基准,本项目用9600bps,误差<0.2% |
| Memory Model | Small | 所有变量默认放在内部RAM(128B),符合51资源限制 |
| XDATA Size | 0x0000 | 关键!STC89C52RC无外部RAM,此处必须填0,否则链接器会分配XDATA段导致HEX错乱 |
注意:如果误填XDATA为0x1000,编译虽过,但烧录后串口无输出——因为程序把堆栈指针指向了不存在的外部RAM,直接跑飞。
3.2 串口调试:用任意串口助手看真实交互流
本项目默认串口1(P3.0/RXD, P3.1/TXD),波特率9600,无校验,1停止位。接线后打开串口助手(如XCOM、SSCOM),你会看到类似这样的实时输出:
[INFO] System init OK. [INFO] MFRC522 init OK, Ver=0x92. [INFO] Waiting for card... [UID: A1B2C3D4] -> New card! Added to DB. [BEEP] Short tone. [LED] Green ON. [UID: 55AAFF00] -> Known card. Access granted. [BEEP] Double tone. [LED] Green ON. [UID: A1B2C3D4] -> Known card. Access granted. [BEEP] Double tone. [LED] Green ON.每行都是printf打出来的,但底层用的是自定义uart_printf(),它把va_list解析后逐字节发,不依赖标准库(节省ROM空间)。你可以在Core/main.c里找到:
// Core/main.c void uart_printf(const char *fmt, ...) { va_list args; char buf[64]; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); // 注意:vsnprintf需Keil自带,非标准C库 va_end(args); UART_SendString(buf); }UART_SendString()本质是循环调用UART_SendByte(),而后者用的是查询发送(非中断),因为51的串口发送中断会打断MFRC522的SPI时序——这是个经典干扰源。所以本项目牺牲一点吞吐量,换稳定性。
3.3 烧录实操:STC-ISP工具链与硬件连接要点
烧录用STC官方ISP工具(v6.89及以上),步骤极简:
硬件接线(TTL转USB模块):
- USB模块TX → 单片机RX(P3.0)
- USB模块RX → 单片机TX(P3.1)
- USB模块GND → 单片机GND
- 单片机RST引脚悬空(不接USB模块RST!STC-ISP靠DTR信号自动冷启动)
STC-ISP设置:
- 选择正确COM口(设备管理器里看)
- “MCU型号”选“STC89C52RC”
- “最高波特率”选“256000”(提高烧录速度)
- “打开串口” → “下载/编程” → 勾选“下次冷启动后运行”
烧录触发:
- 点“下载/编程”按钮
- 立刻按下单片机复位键(或断电再上电)——这是STC冷启动握手的关键动作
- 看到进度条走完,提示“校验成功”,即可松手
提示:如果一直显示“正在检测目标单片机…”,90%是RST没按对时机。我的做法是:点下载后,眼睛盯住进度条,一出现“正在检测”就猛按复位键,成功率100%。
4. 常见问题排查:51单片机+MFRC522组合的5个高频翻车现场
这个模块组合看似简单,实则暗坑密布。以下是我在某实验室帮学生调试时,记录下的5个真实翻车案例,按“现象→原因→解决”结构整理,每一条都来自焊盘、万用表和示波器的实测。
4.1 现象:串口输出“[INFO] MFRC522 init OK, Ver=0x00.”,版本号恒为0x00
原因:MFRC522的MISO线虚焊,或P2.2引脚配置错误(未设为输入模式)。STC89C52RC的P2口默认弱上拉,但MISO需高阻态输入,若P2.2被意外拉低,读回全是0。
解决:用万用表测P2.2对地电阻,正常应>10kΩ;检查mfrc522.c中P2 = 0xFF是否被执行(加LED指示);确认PCB上MISO走线无断点。
4.2 现象:能读到卡,但UID每次都不一样(如A1B2C3D4、A1B2C3D5交替出现)
原因:MFRC522天线匹配电容不对。MFRC522推荐天线电容为22pF,但实际PCB寄生电容约3~5pF,若外挂电容仍用22pF,则总电容过大,导致谐振频率偏移,读卡距离缩短、UID误码。
解决:将天线两端的两个22pF电容,换成15pF(22pF - 5pF寄生 ≈ 17pF,15pF更稳妥);用示波器测天线端射频波形,应为13.56MHz正弦,幅度>1Vpp。
4.3 现象:第一次刷卡正常,第二次开始“Waiting for card…”后无响应
原因:MFRC522未正确关闭天线。MFRC522_Anticoll()成功后,部分代码忘记调用MFRC522_StopCrypto1(),导致卡片仍处于激活态,下次Request()指令被忽略。
解决:在state_machine.c的STATE_READING分支末尾,强制加MFRC522_StopCrypto1();或在MFRC522_Anticoll()函数返回前调用。
4.4 现象:EEPROM写入后断电,再上电读不出UID
原因:STC89C52RC的IAP操作需在特定电压范围(3.8V~5.5V)内进行,若用USB供电(标称5V,实测常4.7V),但板上稳压芯片压降大,导致IAP时VCC<3.8V,写入失败。
解决:用万用表测IAP执行时P4.4(IAP_TRIG)引脚电压,必须≥3.8V;加一级LDO(如AMS1117-3.3)稳压,或改用5V直供(绕过板载稳压)。
4.5 现象:蜂鸣器不响,但LED和串口均正常
原因:蜂鸣器驱动电路用的是NPN三极管(如S8050),但beep.c里Beep_Start()函数将P1.2设为高电平,而S8050基极需高电平导通——这没错,但常见错误是:三极管发射极未接地,或集电极未接蜂鸣器另一端。
解决:查原理图,确认蜂鸣器一端接VCC,另一端接三极管集电极;三极管发射极必须接地;用万用表二极管档测P1.2对地电压,响时应为≈4.2V(高电平)。
5. 二次开发改造指南:从“能用”到“好用”的3个关键升级点
拿到这个“完整版”,你已经跨过了从0到1的门槛。但真要把它变成自己的项目(比如改成宿舍门禁+考勤一体机),光跑通远远不够。下面三个改造点,是我给某公司做的门禁副板时踩坑总结出的“必改项”,每个都附可直接粘贴的代码和验证方法。
5.1 升级1:支持Mifare Classic 1K卡的扇区读写(不止UID)
原程序只读UID,但实际门禁常需读写用户信息(如姓名、权限等级)存在卡的Sector 0 Block 1。MFRC522支持密钥认证,本项目预留了MFRC522_Authenticate()接口,只需补全密钥和扇区号:
// 在Core/state_machine.c中,STATE_READING之后加: case STATE_READING: if (MFRC522_Anticoll(&serNum) == MI_OK) { // 新增:认证Sector 0(Key A,默认密钥FF FF FF FF FF FF) uint8_t key[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (MFRC522_Authenticate(PICC_AUTHENT1A, 0x00, key, &serNum) == MI_OK) { uint8_t block_data[16]; if (MFRC522_ReadBlock(0x00, block_data) == MI_OK) { // block_data[0..15]即Sector 0 Block 0数据(通常存厂商信息) // 例如:block_data[0]=0x08 → 卡类型为Mifare S50 uart_printf("[CARD] Type=0x%02X\n", block_data[0]); } } current_state = STATE_CHECKING_DB; } break;验证方法:用手机NFC工具(如NFC Tools)往卡里写入自定义数据,再用本程序读出对比。注意:MFRC522_ReadBlock()读的是16字节,不是4字节UID。
5.2 升级2:添加白名单/黑名单双模式切换(硬件按键触发)
原程序只有“有卡即放行”,但实际需要管理员模式。用P3.2(INT0)接一个轻触按键,长按3秒进入配置模式:
// 在Core/main.c的System_Init()里加: IT0 = 1; // INT0下降沿触发 EX0 = 1; // 使能INT0中断 EA = 1; // 开总中断 // 新增中断服务函数(Core/interrupt.c): void INT0_ISR(void) interrupt 0 { static uint16_t press_time = 0; if (KEY_P32 == 0) { // 按下 press_time++; if (press_time > 300) { // 300 * 10ms = 3s g_config_mode = !g_config_mode; // 切换模式 Beep_Double(); // 双音提示 press_time = 0; } } else { // 松开 press_time = 0; } } // 在state_machine.c中,STATE_CHECKING_DB分支加判断: if (g_config_mode) { // 配置模式:刷卡=添加白名单 EEPROM_WriteCard(g_current_uid); uart_printf("[CONFIG] Card added.\n"); } else { // 正常模式:查白名单 if (EEPROM_FindCard(g_current_uid)) { current_state = STATE_GRANT_ACCESS; } else { current_state = STATE_DENY_ACCESS; } }验证方法:上电后长按按键3秒,听到双音,此时刷卡即存入;再长按3秒退出,刷卡即验证。
5.3 升级3:串口协议升级为Modbus RTU(对接PLC/上位机)
原串口是自定义文本协议,难对接工业设备。改为Modbus RTU(功能码0x03读保持寄存器),只需改Core/main.c的串口接收部分:
// 定义Modbus寄存器映射(40001起始) #define REG_UID_0 0 // 40001: UID byte0 #define REG_UID_1 1 // 40002: UID byte1 #define REG_UID_2 2 // 40003: UID byte2 #define REG_UID_3 3 // 40004: UID byte3 #define REG_STATUS 4 // 40005: 状态(0=空闲,1=读卡中,2=授权,3=拒绝) // 在UART中断接收完成后,解析Modbus帧: void Modbus_Parse(uint8_t *buf, uint8_t len) { if (len < 8) return; // 最小帧长:Addr+Func+StartHi+StartLo+LenHi+LenLo+CRC1+CRC2 if (buf[0] != 0x01) return; // 仅响应地址0x01 if (buf[1] != 0x03) return; // 仅支持功能码0x03 uint16_t start_addr = (buf[2]<<8) | buf[3]; uint16_t reg_count = (buf[4]<<8) | buf[5]; if (start_addr <= REG_UID_3 && start_addr + reg_count - 1 <= REG_UID_3) { // 读UID寄存器 uint8_t resp[12] = {0x01, 0x03, (uint8_t)(reg_count*2)}; for (uint8_t i = 0; i < reg_count; i++) { resp[3+i] = g_current_uid[start_addr + i]; } // 计算CRC16并追加 uint16_t crc = Modbus_CRC16(resp, 3+reg_count); resp[3+reg_count] = crc & 0xFF; resp[3+reg_count+1] = (crc>>8) & 0xFF; UART_SendArray(resp, 5+reg_count); } }验证方法:用Modbus Poll软件,设置“RTU模式、地址1、功能码03、起始地址40001、数量4”,发送后应收到UID的4字节数据。
从那以后我每次接到RFID类需求,第一件事就是把这份“51单片机RFID程序完整版”拖进新工程,删掉README.md里“仅供学习”的字样,然后对着上面三个升级点,一条一条敲进自己的代码里。它不完美,但足够健壮;它不炫技,但每行都在解决真实问题。希望帮到你。
本文还有配套的精品资源,点击获取