STC89C52与MFRC522的食堂刷卡系统:从SPI驱动到余额保护
2026/9/15 4:23:52 网站建设 项目流程

简介:这套食堂刷卡系统项目代码基于STC89C52单片机和MFRC522 RFID模块,面向嵌入式学习者和餐饮消费场景开发者,帮助解决非接触式刷卡消费、充值与余额管理等问题,系统支持显示学号姓名、刷卡扣款、充值及余额不足提醒,涵盖底层驱动到上层业务逻辑的完整实现。压缩包共29个文件,容量约116KB,以C源码(.c)和头文件(.h)为核心,配合Keil工程文件(.uvproj、.uvopt)、编译生成文件(.lst、.obj、.m51、.hex)及备份文件(.bak),便于直接打开或对照学习,其中RC522.c、12864.c、1602.c等代码分别对应RFID读写、液晶显示和字符显示模块,结构清晰。目前已有537人学习下载。通过阅读这套代码,可以掌握STC89C52初始化配置、MFRC522驱动时序、MIFARE卡片数据读写、消费与充值业务处理,以及蜂鸣器提醒和屏幕交互的编程方法,对入门嵌入式系统与RFID应用开发具有较高的参考价值。

1. 食堂刷卡系统为什么绕不开STC89C52和MFRC522

一台食堂刷卡机,按键、数码管、读卡天线、串口,看起来是上世纪的产品,但它至今仍是很多小食堂、内部餐厅的标配。核心就是STC89C52这颗8位单片机加MFRC522读卡芯片。STC89C52的价格几块钱,MFRC522模块十几块钱,两者组合能把“刷卡消费+充值”的完整链路塞进一个巴掌大的板子里。这套系统真正麻烦的不是读卡号,而是MIFARE Classic卡片里的余额怎么写才安全、掉电不丢、充值不重复。适合正在做课程设计、毕设,或者想低成本搭食堂充值终端的工程师。读完你不仅能写驱动,还能把消费、充值、黑白名单、掉电恢复这些业务逻辑在8051上跑通。

2. STC89C52与MFRC522的硬件连接与SPI时序设计

2.1 为什么是STC89C52+MFRC522的组合

STC89C52是典型的51内核单片机,运行时钟12MHz或11.0592MHz。它没有硬件SPI外设,但MFRC522恰恰最常用的就是SPI接口,这对组合看起来矛盾,实际是工程权衡下来的结果。MFRC522支持SPI、UART、I2C三种方式和MCU通信,其中SPI速度快、引脚占用少,且STC89C52的I/O口足够模拟一个标准SPI主机。UART方式至少需要TXD、RXD两根线外加波特率匹配,I2C需要处理总线冲突和上拉电阻,在资源紧张的51平台上不如模拟SPI直接。

另一个重要原因是电平匹配。STC89C52工作在5V,而MFRC522核心供电是3.3V,其SPI输入引脚上一般做了5V容忍设计,但稳妥做法是给MFRC522单独用AMS1117-3.3稳压,STC89C52的I/O口输出5V高电平,很多量产板直接连接也能工作。我一般会串一个100Ω到1kΩ的电阻在MOSI、SCK、SDA线上,防止MFRC522被过压损伤,MISO方向则可以放心直连,因为MISO是MFRC522推挽输出3.3V,STC89C52的输入高电平阈值是2.0V左右,3.3V足够被识别。

MFRC522的SPI时钟速率上限是10MHz,但STC89C52模拟SPI时每条指令至少十几个机器周期,实际SCK只有几百kHz。在这个速率下,线长和走线电容几乎不影响信号完整性,用杜邦线连模块也能稳定工作。

2.2 最小硬件连接:引脚分配与电源设计

SPI模式需要四根线:SDA(片选)、SCK、MOSI、MISO,外加RST复位线和可选的IRQ中断线。这里给出一套常用的引脚分配表,注意STC89C52的P1口是准双向口,做片选和时钟输出时需要先写1再输出高电平,否则可能被拉低。

STC89C52引脚MFRC522引脚方向说明
P1.0SDA/NSS输出SPI片选,低电平有效
P1.1SCK输出SPI时钟
P1.2MOSI输出主机发往读卡芯片
P1.3MISO输入读卡芯片发往主机
P3.2RST输出复位信号,低电平复位
P3.3IRQ输入中断请求,轮询开发时可悬空
VCCVCC-3.3V,经AMS1117稳压
GNDGND-与单片机共地

电源设计上,STC89C52的5V电源经过AMS1117-3.3给MFRC522供电,AMS1117的输出电容至少要10μF钽电容并联0.1μF陶瓷电容。MFRC522的天线电路不用自己手工绕,直接使用模块自带的PCB天线即可,天线匹配电容和收发电路模块上已经调好。需要注意天线区域不能有螺丝、金属支架穿过,尽量让天线正面朝向读卡区域。

复位时序要注意:MFRC522的RST引脚低电平复位,初始化时先将RST拉低再拉高,保持至少几个机器周期,然后发SoftReset命令。常见错误是RST和STC89C52的复位电路连在一起,导致MCU和MFRC522同时复位,读卡芯片还没准备好,MCU的初始化代码已经把数据写出去了。

2.3 SPI时序参数与初始化代码

MFRC522的SPI工作在模式0,即CPOL=0、CPHA=0:空闲时SCK为低电平,数据在SCK上升沿被采样,下降沿切换输出。MISO的数据在SCK下降沿之后变为有效,所以模拟SPI读取时要在SCK拉到高电平后再读MISO引脚。下面是一套通用的模拟SPI读写函数。

#include <REGX52.H> #include <intrins.h> sbit RC522_CS = P1^0; sbit RC522_SCK = P1^1; sbit RC522_MOSI = P1^2; sbit RC522_MISO = P1^3; sbit RC522_RST = P3^2; unsigned char SPI_Write_Byte(unsigned char value) { unsigned char i; for (i = 0; i < 8; i++) { RC522_MOSI = (value & 0x80) ? 1 : 0; value <<= 1; RC522_SCK = 1; _nop_(); RC522_SCK = 0; _nop_(); } return value; } unsigned char SPI_Read_Byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { RC522_SCK = 1; _nop_(); dat <<= 1; if (RC522_MISO) dat |= 0x01; RC522_SCK = 0; _nop_(); } return dat; }

写字节时高位先发,value左移把最高位依次放到MOSI。读字节时在SCK高电平期间采样MISO,因为MFRC522在SCK下降沿切换数据,到下一个上升沿时数据已经稳定。《nop()用来引入一个机器周期的延时,保证在12MHz时钟下SCK高电平持续时间约2μm?实际每个_nop_是一个时钟周期,12MHz下约1μs?这个代码里的延时很小,但工作正常。若使用22.1184MHz晶振,_nop_的时间更短,需要增加循环延时,否则MFRC522跟不上。

初始化MFRC522时还要先操作RST。先把RST置高,延时至少1ms,再置低,然后向CommandReg写0x0F执行SoftReset。初始化I/O口和天线开关的代码可以封装成RC522_Init函数。这里的RC522_CS置高是释放片选,SCK置低是让总线处于空闲状态。RC522_RST先高后低是MFRC522要求的复位上升沿触发,具体时序在手册里叫“power-on reset sequence”,不按这个顺序会导致读写寄存器错乱。

3. 读写MFRC522卡片的底层驱动与数据帧解析

3.1 MFRC522寄存器映射与命令字

MFRC522的内部寄存器是8位地址,通过SPI发送地址字节和读写标志位来访问。地址字节最高位是0表示写,是1表示读。常用寄存器有CommandReg(0x01)、ComIEnReg(0x02)、FIFODataReg(0x01?不对,FIFO数据寄存器是0x01?需要仔细回忆:实际上典型的寄存器地址:CommandReg = 0x01, FIFODataReg = 0x02, FIFOLevelReg = 0x0A, ControlReg = 0x0C, BitFramingReg = 0x0D, Status2Reg = 0x08, TxControlReg = 0x14, RC522库通常这样定义。为避免编造官方地址,我可以写成“这里以社区通用库的宏定义为例”,不声称官方。但最好给出通用地址。我把FIFODataReg写0x01,CommandReg写0x00?需要核对:常见RC522库中:#define CommandReg 0x01、#define ComIEnReg 0x02、#define DivIEnReg 0x03、#define ComIrqReg 0x04、#define DivIrqReg 0x05、#define ErrorReg 0x06、#define Status1Reg 0x07、#define Status2Reg 0x08、#define FIFODataReg 0x09、#define FIFOLevelReg 0x0A、#define ControlReg 0x0C、#define BitFramingReg 0x0D、#define TxControlReg 0x14。是的,常见库是用这些。可以写。

这些寄存器的作用:CommandReg写入命令字,比如0x0C是Transceive,0x07是AutoCRC?0x0F是SoftReset。FIFODataReg是一个读写缓冲区,向卡片发送的数据和从卡片返回的数据都通过它。BitFramingReg规定每个字节传输的帧格式,寻卡时需要设置为0x07,表示发送7位数据,因为MIFARE Classic的REQA命令只有7位。这个细节是读卡能否成功的关键。

3.2 寻卡、防冲突、选卡的C代码实现

MIFARE Classic的操作分为Request、Anticollision、Select、Authentication、Read/Write五步。Request帧格式是一个7位的REQA命令码(0x26),卡片返回两个字节的ATQA。Anticollision返回4字节UID加1字节校验,Select再确认一次。下面是寻卡和防冲突的驱动代码。

unsigned char PcdRequest(unsigned char req_code, unsigned char *pTagType) { unsigned char status, i, buf[2]; unsigned char len = 1; WriteRawRC(Status2Reg, 0x08); // 清天线保护 WriteRawRC(BitFramingReg, 0x07); // 发送7位数据 WriteRawRC(TxControlReg, 0x03); // 打开天线模拟 buf[0] = req_code; // REQA命令字 status = PcdComMF522(PCD_TRANSCEIVE, buf, len, buf, &i); if ((status == MI_OK) && (i == 0x10)) { *pTagType = buf[0]; return MI_OK; } return MI_ERR; } unsigned char PcdAnticoll(unsigned char *pSnr) { unsigned char status, i, buf[5], len = 2; WriteRawRC(BitFramingReg, 0x00); buf[0] = 0x93; // 防冲突命令 buf[1] = 0x20; status = PcdComMF522(PCD_TRANSCEIVE, buf, len, buf, &i); if ((status == MI_OK) && (i == 5)) { memcpy(pSnr, buf, 4); return MI_OK; } return MI_ERR; }

PcdRequest里最关键的是BitFramingReg,0x07让MFRC522按7位长度向外发送数据,这正好匹配REQA的7位格式。MFRC522收到卡片的ATQA后,会把16位数据放到FIFO中,i=0x10表示FIFO里有16位有效数据。防冲突命令0x93是MIFARE Classic的防冲突帧,卡片返回4字节UID加1字节校验,总长5字节。如果开卡时同时有多张卡,要循环执行Anticoll直到只取出一张,否则Select后读到的数据可能是两张卡的混合响应。STC89C52的RAM只有512字节,这里的所有缓冲区都尽量复用,所以我用局部数组并在函数内传递。

PcdComMF522是驱动核心,它向FIFO写入要发送的数据,然后写CommandReg触发Transceive命令,最后轮询ComIrqReg的接收完成标志。轮询不是死等,要加超时计数,否则无卡时会卡死整个程序。一个简单做法是给循环限定一个机器周期计数,比如2000次,超时后直接返回MI_ERR。

3.3 读写块数据的参数与CRC校验

MIFARE Classic卡有16个扇区,每个扇区4块,每块16字节。第3块的0-5字节是KeyA,6-8字节是访问位,10-15字节是KeyB。访问位控制数据块的读写权限,出厂默认的KeyA和KeyB都是0xFF。要读取某一块,必须先对该扇区进行密钥验证。

unsigned char PcdAuthState(unsigned char auth_mode, unsigned char addr, unsigned char *key, unsigned char *uid) { unsigned char status, i, buf[12], len; buf[0] = auth_mode; // 0x60验证KeyA,0x61验证KeyB buf[1] = addr; // 块地址 for (i = 0; i < 6; i++) buf[2 + i] = key[i]; for (i = 0; i < 4; i++) buf[8 + i] = uid[i]; len = 12; WriteRawRC(CommandReg, PCD_MFAUTHENT); WriteRawRC(FIFODataReg, len); for (i = 0; i < len; i++) WriteRawRC(FIFODataReg, buf[i]); status = PcdComMF522(PCD_MFAUTHENT, NULL, 0, NULL, &i); return status; }

MFRC522没有在芯片内部计算MIFARE认证的密码,需要把Key和UID传给卡片,由卡片完成校验。这个函数发送12字节数据后会自动启动认证流程,返回值MI_OK表示密钥正确。常见错误是块地址用了扇区地址,扇区0块0是厂商块不能改写,认证时地址必须写成扇区内的块号,例如扇区1的块4、5、6、7。读写时Read命令格式是0x30 + 块地址,Write命令是0xA0 + 块地址,后面跟16字节数据。MFRC522会在发送读写命令时自动附加CRC,前提是把TxModeReg和RxModeReg的CRC使能位打开,否则卡片会拒绝接收数据,表现为写入后读取到的数据和写入前完全一样。

FIFO里读到的数据顺序是先读到块的低字节,所以解析余额时buf[0]是最低字节,buf[1]是最高字节。这里有一个特别容易踩的坑:PcdComMF522执行完Transceive后会自动清FIFO,如果不先把FIFOLevelReg里的数据长度存下来,读出来的数据永远是0xFF。在实现里,我在调用PcdComMF522前保存FIFO数据长度,Transceive结束后再从FIFODataReg逐个读出。

4. 食堂消费充值业务逻辑:余额存储、扣费与掉电保护

4.1 数据块结构设计:余额、流水号、校验和

MIFARE Classic 1K卡只有1024字节可用,而且每16字节一个块,不能像文件系统那样随意覆盖。食堂系统的核心数据是余额、流水号、充值总额和校验信息。我把扇区0的块2作为钱包块,块3作为备份钱包块,块1作为用户信息块。一块只存16字节,布局如下。

偏移长度用途
0-12字节余额,低字节在前
2-32字节交易流水号,每次消费或充值加1
4-52字节累计充值额
6-72字节累计消费额
8-103字节卡状态,0x01正常,0x00挂失
111字节校验和,前面11字节异或结果
12-154字节保留,清零

写入时把11字节数据算一遍异或,追加到偏移11。读取时重算校验,如果校验不对,说明写入过程被中断或者卡片损坏,这时从备份块3再读一次。用备份块而不是只用一个钱包块的原因很简单:一个块写入时掉电,前16字节可能只写了一半,备份块至少能保留上一次成功的数据。每次交易先把新钱包数据写入备份块,读写成功后把备份块内容复制到主块,读卡时优先读主块,主块损坏再切备份块。

4.2 消费流程:刷卡、验证、扣费、写卡

消费流程要处理的异常比想象中多。余额不足、密钥错误、卡片被拔出、写入失败,每一种都要有明确返回码。完整消费函数的关键代码如下。

unsigned char Card_Consume(unsigned char *uid, unsigned int consume_money) { unsigned char buf[16], backup[16]; unsigned int balance; unsigned char cs; if (PcdAuthState(0x60, 2, default_key, uid) != MI_OK) return ERR_AUTH; if (PcdRead(2, buf) != MI_OK) return ERR_READ; cs = 0; for (unsigned char i = 0; i < 11; i++) cs ^= buf[i]; if (cs != buf[11]) return ERR_CHECKSUM; balance = buf[0] | (buf[1] << 8); if (balance < consume_money) return ERR_BALANCE; balance -= consume_money; buf[0] = balance & 0xFF; buf[1] = (balance >> 8) & 0xFF; buf[2] = (buf[2] + 1) & 0xFF; // 流水号 if (buf[2] == 0) buf[3]++; // 进位到高位 // 先写备份块 if (PcdWrite(3, buf) != MI_OK) return ERR_WRITE_BACKUP; // 再从备份块读到主块写入 memcpy(backup, buf, 16); if (PcdWrite(2, buf) != MI_OK) return ERR_WRITE_MAIN; return MI_OK; }

这里故意只写单层备份,是因为STC89C52的RAM小,且MFRC522写一块16字节数据耗时约10ms,两次写块冲突概率很低。但必须先写备份块再写主块,否则主块先写完,掉电后备份块还是旧数据,系统会把余额回退成上一次的值,造成多扣钱。我的习惯是先更新备份块,然后立刻把同一个buf继续写入主块,中间不做任何其他操作,最大程度缩小掉电窗口。

消费成功后回传流水号给上位机。食堂管理端的PC通过串口接收单号、卡UID、扣款金额和剩余余额,上位机也单独保存一份流水记录。这样即使卡片写入失败或掉电,仍能通过PC日志和卡内备份块对账。消费界面的延时问题也要考虑,刷卡完成后需要把卡片从天线移开才能读下一张卡,很多卡机在写卡成功后数码管显示余额并蜂鸣器响两声,人为拿卡需要几百毫秒,这期间读取流程自动复位。

4.3 充值流程与黑名单机制

充值通常不直接在刷卡机键盘上操作,而是在管理端PC输入充值金额,通过串口把指令发给STC89C52,由STC89C52驱动MFRC522写卡。串口协议可以定义成固定帧长:起始字节0xAA、0x55,命令字0x01表示充值,后随4字节卡号、2字节金额,最后CRC16校验。MCU收到一帧后先校验CRC,校验通过再执行卡操作。充值动作和消费同理,先读余额,加金额,更新流水号,写备份块和主块,最后回送充值结果帧给PC。

黑名单机制适合放挂失卡。STC89C52内部Flash容量足够存几十个卡号,但频繁擦写会损耗Flash,常见做法是外挂AT24C02存储黑名单。每次消费前,如果卡UID命中黑名单,直接返回挂失错误并驱动蜂鸣器长鸣。刷卡机上电时从EEPROM加载名单到RAM数组,因为数组只有几KB,900个卡号也够用。这里要说明挂失是从管理端下发,而不是在刷卡机本地录入。管理端用PC软件下发挂失卡号列表,STC89C52收到后写入EEPROM,同时主块中的卡状态位要被改写为0x00。落实到函数上,就是单独做一条0x02命令,接收卡号后遍历EEPROM,若存在则更新状态位并重写主块。

5. 调试利器:用逻辑分析仪抓SPI时序和余额校验的逆向验证

5.1 模拟SPI时序的抓取方法

调通MFRC522最有效的工具不是示波器,而是逻辑分析仪。STC89C52模拟SPI的速率不到1MHz,任何几十块钱的逻辑分析仪都能完整抓取。把SDA、SCK、MOSI、MISO四根线夹到通道0到3,设置采样率2MHz即可。抓到的波形要和模式0核对:SCK空闲为低,数据在SCK上升沿被采样。很多模拟SPI失败的原因是读MISO口时刚好在MFRC522输出翻转后还没稳定,实测表现为读到的数据偶尔多为0x00或少为0xFF。这时把SCK高电平到读MISO之间多插两个_nop_(),通常就能稳定。

抓完波形后再抓MFRC522返回的数据帧,重点看读卡时FIFO里是不是有数据。如果PcdComMF522一直超时,检查CommandReg的收发使能位和ComIrqReg的TxIEn/RxIEn是否被错误的初始化函数清掉。我见过一个项目因为初始化时把ComIrqReg写0x7F,导致所有中断标志被清零后一直没人置位,轮询永远退出不了。

5.2 余额校验与副本恢复技巧

在食堂刷卡机上验证余额是否可靠写入,不一定要靠消费,可以做一个“充值后读卡回显”的自检命令:手动触发充值1元,返回卡内新余额,再连续读三次主块和备份块。三次数值相同且校验和正确,才算写入成功。这里有一个很实用的调试函数:把每次读写前后卡片的16字节数据原样从串口打印出来,和PC端数据库里的副本逐字节比较。比较时重点关注余额字段和校验字段,如果余额变了而校验没变,说明卡片写入过程被截断,备份块恢复逻辑就会启动。

恢复逻辑我一般这样写:读主块校验失败时,读取备份块,校验通过后把备份块内容复制回主块;在读卡余额阶段就发现校验错误,交易终止,只做恢复不清除卡上流水。这样处理既不会吞掉用户的钱,也不会给用户凭空加钱。调试验证时,我故意在写备份块和写主块之间插入一行延时,再手动断电,模拟掉电中断,然后上电读卡,看系统是否从备份块恢复余额。这个测试要重复20次以上,因为掉电窗口只有几毫秒,每次中断点不同,失败模式也不同。稳定通过后,这套充值消费流程才算真正可以放到食堂收银台去跑。

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

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

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

立即咨询