ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STC89C52与MFRC522的食堂刷卡系统:从SPI驱动到余额保护

STC89C52与MFRC522的食堂刷卡系统:从SPI驱动到余额保护 简介这套食堂刷卡系统项目代码基于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 为什么是STC89C52MFRC522的组合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.3VSTC89C52的输入高电平阈值是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即CPOL0、CPHA0空闲时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表示读。常用寄存器有CommandReg0x01、ComIEnReg0x02、FIFODataReg0x01不对FIFO数据寄存器是0x01需要仔细回忆实际上典型的寄存器地址CommandReg 0x01, FIFODataReg 0x02, FIFOLevelReg 0x0A, ControlReg 0x0C, BitFramingReg 0x0D, Status2Reg 0x08, TxControlReg 0x14, RC522库通常这样定义。为避免编造官方地址我可以写成“这里以社区通用库的宏定义为例”不声称官方。但最好给出通用地址。我把FIFODataReg写0x01CommandReg写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是Transceive0x07是AutoCRC0x0F是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里最关键的是BitFramingReg0x07让MFRC522按7位长度向外发送数据这正好匹配REQA的7位格式。MFRC522收到卡片的ATQA后会把16位数据放到FIFO中i0x10表示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字节是KeyA6-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验证KeyA0x61验证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字节交易流水号每次消费或充值加14-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数组因为数组只有几KB900个卡号也够用。这里要说明挂失是从管理端下发而不是在刷卡机本地录入。管理端用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次以上因为掉电窗口只有几毫秒每次中断点不同失败模式也不同。稳定通过后这套充值消费流程才算真正可以放到食堂收银台去跑。本文还有配套的精品资源点击获取
返回列表