51单片机RC522 RFID完整程序:从驱动到门禁考勤实战
简介:这份资源是面向51单片机学习者与嵌入式入门者的RFID程序完整工程包,围绕阅读器端开发,解决从模块驱动到应用逻辑的落地问题。内容涉及RFID模块选型(如MFRC522、EM4069)、SPI/I2C接口连接、ISO/IEC 14443等协议理解,以及初始化、命令收发、数据解析、中断处理与错误检测等关键环节,并延伸至读卡显示、数据记录、门禁控制等应用层示例,适合具备C语言与基础电路知识的读者对照实践。压缩包为rar格式,约15.04MB,上游未提供文件总数与类型明细,从描述看应包含驱动代码、主程序、示例应用、配置参数与说明文档等模块。目前已有1658人学习下载,可作为搭建小型RFID系统、理解51单片机与射频模块协同工作的参考素材,便于读者梳理工程结构并迁移到自己的物联网或智能设备项目中。
1. 从一块读卡器板子说起:这套51单片机RFID程序到底能跑出什么
手里有一块RC522读卡模块、一片STC89C52或者AT89C52最小系统板,再加上一张M1卡,很多人第一次接线通电之后看到串口助手刷出一串UID就以为完事了。真正拿这套东西去做课程设计、门禁Demo、考勤机原型的时候才会发现,难点根本不在“读出一串卡号”,而在于读卡之后的整套流程:寻卡、防冲突、选卡、认证、读写扇区、串口回传、LCD显示、继电器开锁,每一步的时序和状态机都得对得上,错一个字节就卡死在那里。这份51单片机RFID程序完整版,解决的就是从裸片驱动到业务闭环这一整段,把RC522的SPI时序、M1卡认证流程、串口协议和外围控制都串起来了,适合正在做单片机课程设计、门禁考勤类小系统、或者想把RFID读卡模块吃透的开发者直接拿来改。
它不是一个只跑通“读卡号”的Demo,而是一套带完整工程结构的C51程序:底层是RC522寄存器操作和SPI位操作,中间层封装了寻卡、防冲突、选卡、认证、读写块这些ISO14443A协议动作,上层是主循环里的业务逻辑,负责把卡号比对、串口输出、显示刷新、继电器动作串起来。技术栈很朴素——Keil C51编译环境、标准8051指令集、软件模拟SPI、UART串口,没有RTOS,没有复杂框架,全部靠主循环加状态标志推进。这种结构对新手友好,对熟手也够用,因为每一层都能单独替换:想换成硬件SPI,改底层;想换成别的卡型,改协议层;想接上位机,改串口协议层。下面按“这项目怎么组织、怎么跑起来、怎么改”一路拆下去。
2. 工程结构与通信链路:从RC522寄存器到主循环的完整分层
2.1 目录与文件划分
这套程序常见做法是按功能拆成几个.c/.h文件,而不是全塞进一个main.c。典型结构如下:
| 文件 | 职责 |
|---|---|
| main.c | 主循环、初始化、业务状态机 |
| rc522.c / rc522.h | RC522寄存器读写、复位、天线配置 |
| spi.c / spi.c.h | 软件模拟SPI的位操作时序 |
| mfrc522_app.c | 寻卡、防冲突、选卡、认证、读写块 |
| uart.c | 串口初始化与收发 |
| lcd1602.c 或 oled.c | 显示驱动(按实际外设选) |
| delay.c | 毫秒/微秒级延时 |
| config.h | 引脚定义、卡号白名单、功能开关 |
这种拆法的好处是引脚和业务分离。config.h里集中定义SCK、MOSI、MISO、NSS、RST这几个引脚,换板子只改这一处;rc522.c只关心寄存器地址和位操作,不掺业务;main.c只调用“读一张卡”这个高层函数,不关心SPI怎么翻电平。二次开发时最怕的就是引脚散落在各个文件里,改一个脚要全局搜索,这套结构避开了这个坑。
2.2 软件SPI的时序实现
51单片机多数型号没有硬件SPI,所以这套程序用普通IO口模拟SPI。核心就是四条线:时钟SCK、主出从入MOSI、主入从出MISO、片选NSS。RC522支持SPI模式0,也就是空闲时时钟低电平,数据在上升沿采样。下面这段是典型的位操作写法:
// 软件SPI写一个字节,模式0:空闲低电平,上升沿采样 void SPI_WriteByte(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { SCK = 0; // 时钟拉低,准备数据 if(dat & 0x80) // 取最高位,MSB先行 MOSI = 1; else MOSI = 0; dat <<= 1; // 左移,准备下一位 SCK = 1; // 上升沿,从机采样 } SCK = 0; // 恢复空闲低电平 } // 软件SPI读一个字节,同样MSB先行 unsigned char SPI_ReadByte(void) { unsigned char i, dat = 0; for(i = 0; i < 8; i++) { dat <<= 1; SCK = 1; // 上升沿后从机输出数据 if(MISO) dat |= 0x01; // 采样MISO SCK = 0; // 拉低,准备下一位 } return dat; }这里有两个参数必须对上:一是MSB先行,RC522的SPI要求高位先出,写反了读出来的寄存器值全是乱的;二是模式0的相位关系,SCK先低后高再低,数据在上升沿被采样。很多新手翻车就翻在“读出来全是0xFF或者0x00”,八成是SCK空闲电平或者采样沿搞反了。延时方面,51的IO翻转本身有机器周期,一般不需要额外加nop,但如果主频很高(比如11.0592MHz以上跑得比较快),可以在SCK拉高后加一两个空操作保证建立时间。这套程序里delay.c提供了微秒级延时,SPI里按需插入,不盲目加长延时,否则读一张卡会明显变慢。
2.3 RC522寄存器读写与复位流程
RC522的所有操作都落到寄存器上,读寄存器是“地址左移一位再或0x80”,写寄存器是“地址左移一位再与0x7E”。这个地址变换是协议规定的,写错一位就访问到别的寄存器。典型实现:
// 写RC522寄存器 void RC522_WriteReg(unsigned char addr, unsigned char val) { NSS = 0; // 片选拉低 SPI_WriteByte((addr << 1) & 0x7E); // 写地址:左移一位,最低位清零 SPI_WriteByte(val); // 写入数据 NSS = 1; // 片选拉高 } // 读RC522寄存器 unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; NSS = 0; SPI_WriteByte(((addr << 1) & 0x7E) | 0x80); // 读地址:最高位置1 val = SPI_ReadByte(); NSS = 1; return val; }复位流程一般是:拉低RST一段时间,拉高,然后写CommandReg软复位,等PowerDown位清零,再配置TimerReg、TxModeReg、RxModeReg、ModWidthReg这些天线相关寄存器,最后打开天线。这套顺序不能乱,尤其是软复位后必须等芯片内部就绪再往下走,否则后面写进去的配置全丢。程序里用轮询CommandReg的PowerDown位来判断复位完成,比死延时可靠。
2.4 主循环里的业务状态机
主循环不阻塞,靠状态标志推进。常见写法是每轮先调一次寻卡,有卡才进后续流程,没卡就刷新显示或者等串口命令。伪代码如下:
void main(void) { System_Init(); // 引脚、串口、定时器、LCD初始化 RC522_Init(); // 复位并配置RC522 LCD_ShowString(0, 0, "RFID Ready"); while(1) { if(PcdRequest(PICC_REQIDL, cardType) == MI_OK) // 寻卡 { if(PcdAnticoll(uid) == MI_OK) // 防冲突,拿UID { if(Check_WhiteList(uid)) // 比对白名单 { Relay_Open(); // 开锁 UART_SendCard(uid); // 串口回传 LCD_ShowString(0, 1, "Access OK"); } else { LCD_ShowString(0, 1, "Denied"); } Delay_ms(500); // 防重复读 } } } }这段逻辑的关键在最后那个延时。不加延时,同一张卡会在几十毫秒内被反复读到,继电器哒哒响,串口刷屏。常见做法是读到卡后延时几百毫秒再继续寻卡,或者用“卡离开检测”标志位,只有检测到卡离开才允许下一次触发。这套程序用的是延时加状态标志的组合,简单但够用。
3. 跑起来:接线、编译、烧录与串口验证
3.1 硬件接线对照
RC522是3.3V器件,51单片机一般是5V系统,这里有个必须注意的点:RC522的电源和信号电平都是3.3V,直接拿5V单片机的IO去驱动,短时间可能能跑,长时间有损坏风险。稳妥做法是信号线串电阻分压或者用电平转换,电源单独给3.3V。接线对照如下:
| RC522引脚 | 51单片机引脚 | 说明 |
|---|---|---|
| SDA(NSS) | P2.0 | 片选,低有效 |
| SCK | P2.1 | 时钟 |
| MOSI | P2.2 | 主出从入 |
| MISO | P2.3 | 主入从出 |
| RST | P2.4 | 复位,低有效 |
| 3.3V | 3.3V电源 | 不可接5V |
| GND | GND | 共地 |
串口部分用P3.0/P3.1接USB转TTL,波特率常见9600,晶振11.0592MHz时误差最小。如果板上还有LCD1602,数据口接P0,控制脚接P2.5到P2.7,具体以config.h为准。
3.2 Keil工程配置与编译
打开工程后先确认三件事:器件型号选对(STC89C52或AT89C52按实际芯片选)、晶振频率填对(11.0592MHz或12MHz,影响延时和串口波特率)、输出勾选生成HEX文件。编译前检查config.h里的引脚定义和实际接线是否一致,这是最常见的“编译通过但跑不起来”的原因。编译无报错后,用STC-ISP或者对应烧录工具下载HEX,冷启动上电。
3.3 串口验证与读卡测试
上电后串口助手设9600、8位、无校验、1停止位,应该能看到初始化信息。拿一张M1卡靠近天线,正常会输出类似“UID: xx xx xx xx”的内容。如果没反应,先看天线有没有接好(RC522模块天线是PCB印刷的,一般不用外接),再看RC522的3.3V是否正常,最后用万用表量RST脚上电后是否被拉高。读卡距离一般2到5厘米,太远读不到不是程序问题,是天线功率和卡类型的问题。
提示:第一次调试建议先把LCD和继电器相关代码注释掉,只保留串口输出,确认读卡链路通了再逐步加外设,否则出问题时分不清是读卡失败还是显示阻塞。
4. 核心代码拆解:寻卡、防冲突、选卡与扇区认证
4.1 寻卡与防冲突的协议动作
寻卡用PcdRequest,发的是REQA或者WUPA命令,卡在射频场内会回ATQA。防冲突用PcdAnticoll,发0x93 0x20,卡回4字节UID加校验。这两步的返回值判断很关键,MI_OK才继续,MI_NOTAGERR说明没卡,其他值说明通信出错。防冲突里有个细节:多张卡同时在场时,防冲突算法会逐位仲裁,最终选出一张。这套程序按单卡场景实现,多卡场景下需要循环调用直到没有冲突,做考勤机多卡同时刷的场景要注意这一点。
4.2 扇区认证与块读写
M1卡分16个扇区,每个扇区4个块,每块16字节,第3块是密钥块。读写数据前必须先认证,认证命令是0x60(KeyA)或0x61(KeyB),后面跟块地址、6字节密钥、4字节卡号。认证通过才能读块。典型读块流程:
// 认证扇区0的块1,使用KeyA,默认密钥全FF unsigned char keyA[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; if(PcdAuthState(0x60, 1, keyA, uid) == MI_OK) { unsigned char data[16]; if(PcdRead(1, data) == MI_OK) // 读块1 { UART_SendBuffer(data, 16); // 回传16字节 } }参数说明:第一个参数0x60表示KeyA认证,0x61是KeyB;第二个参数是绝对块号,扇区0块1就是1,扇区1块0是4,以此类推;keyA是6字节密钥,出厂默认全FF,实际项目里一定要改,否则谁都能读;uid是防冲突拿到的4字节卡号,认证时必须带上。写块用PcdWrite,写之前同样要认证,写完建议回读校验,M1卡写操作有失败概率,不回读容易埋雷。
4.3 卡号白名单与权限判断
白名单常见做法是存成二维数组或者结构体数组,每个元素4字节UID加一个权限标志。比对时逐字节比较,全等才放行。更工程化的做法是把白名单放EEPROM,支持串口下发增删,但这套程序为了简洁用的是编译期常量数组。二次开发时如果要支持动态授权,把Check_WhiteList改成从EEPROM读,再加一条串口命令“ADD uid”写入,就能做成简易发卡器。
5. 避坑与排查:读卡失败、认证不过、串口乱码的现场记录
5.1 现象:串口一直输出0xFF或0x00,读不到卡
原因多半在SPI时序或者接线。先确认NSS、SCK、MOSI、MISO四根线没有接反,尤其是MOSI和MISO对调是高频错误;再确认RC522供电是3.3V不是5V;最后用示波器或者逻辑分析仪看SCK有没有波形。没有仪器的话,写一个只读VersionReg的测试函数,正常应该读到0x91或0x92,读不到就说明SPI链路没通,跟卡没关系。
5.2 现象:能寻到卡但认证总是失败
认证失败先看密钥。出厂卡默认KeyA是六个0xFF,但有些卡被改过密钥,拿默认密钥当然过不了。其次看块地址,认证的块号必须是绝对块号,不是扇区内偏移,扇区1块0要写4不是0。最后看UID,认证时带的UID必须是防冲突拿到的4字节,顺序不能反。这三条对不上,认证必挂。
5.3 现象:继电器反复吸合,串口刷屏
原因是主循环里没有做读卡去抖。同一张卡在射频场内会持续被寻到,每轮循环都触发一次业务。解决办法是读到卡后延时300到500毫秒,或者记录上一次UID,相同UID在设定时间内不重复处理。更严谨的做法是检测卡离开(连续若干次寻不到卡)再重置标志,这样连刷同一张卡也不会重复触发。
5.4 现象:串口输出乱码
先查波特率和晶振是否匹配。11.0592MHz配9600是标准组合,12MHz配9600会有误差,容易乱码。再查串口初始化里TMOD、TH1、TL1、SCON的配置,TH1和TL1要按晶振和波特率算,不能照抄。最后确认USB转TTL的TX/RX和单片机的RX/TX是交叉接的,TX接TX是接不通的。
5.5 现象:程序跑一段时间后死机
51单片机RAM很小,常见原因是数组越界或者递归调用。检查串口接收缓冲区有没有做长度限制,检查LCD显示字符串有没有超出行宽,检查中断里有没有做耗时操作。另外RC522长时间工作发热时通信可能不稳,加一个超时重试机制,读失败重试两三次再报错,比直接死等可靠。
6. 二次开发改造:从读卡Demo到可维护的小系统
这套程序最值得改的地方不是读卡本身,而是把“读卡”抽象成一个事件,让业务逻辑跟驱动彻底分开。我一般会做三件事:第一,把Check_WhiteList和Relay_Open这些业务函数从main.c里抽出来,放到app.c,main.c只负责调度;第二,给读卡加一个环形缓冲或者标志队列,中断里只置标志,主循环处理,避免在SPI操作中间被串口中断打断导致时序错乱;第三,把卡号白名单和配置参数挪到EEPROM,加两条串口命令做增删查,这样不用每次改代码重新烧录。
验证改造是否成功,不用整机反复试,写一个分层测试的顺序:先单独测SPI读VersionReg,再测寻卡返回ATQA,再测防冲突拿UID,最后测认证读块。每一层通了再往上走,出问题能立刻定位到层。这个习惯是从一次血泪经验来的:当时把LCD刷新和读卡放在同一个循环里,读卡偶尔失败,查了两天才发现是LCD写操作占用了太多时间,导致SPI时序被拉长。从那以后我每次改这类程序,都强制先跑一遍分层自检,确认底层稳了再动业务。希望帮到你。
本文还有配套的精品资源,点击获取