基于AT89S52与MFRC522的RFID公交刷卡终端设计 简介面向电子信息工程、物联网及嵌入式课程设计的学习者这份文档围绕RFID公交车刷卡系统展开回应城市交通拥堵背景下公交一卡通在通行效率与环境质量上的需求。内容从系统总体方案切入硬件部分涉及Mifare One非接触式IC卡选型、AT89S52单片机、晶振电路、蜂鸣器提示音、LCD1602显示与DS1302时钟模块软件部分梳理初始化、射频卡与读写器通信及总流程图并讨论低成本、成熟器件的可靠性与经济性取舍。包内共1个doc文件约649KB以文字方案和电路说明为主适合按章节查阅硬件模块与软件流程。已有4297人学习可作为课程设计、电子系统设计说明书或公交刷卡终端方案的参考底稿也便于延伸到物流、身份识别等应用场景。1. 从一张公交卡说起RFID 刷卡终端的整体定位公交车上那声滴大概是城市里最没存在感的提示音但真把它拆开几十毫秒里跑的是一整套 RFID 刷卡终端动作寻卡、防冲突、挑一张卡、三次认证、读钱包余额、扣款、写回、LCD 刷新、蜂鸣器响。这份电子系统设计说明书讲的就是这么一台小终端的完整设计——AT89S52 做 MCUMFRC522 做 13.56MHz 射频前端Mifare OneS50卡当电子钱包外挂 LCD1602 显示和 DS1302 保证离线走时。它适合两类人看一类是要交课程设计、需要一份能落地焊接调试的完整方案另一类是手上已经有一堆积木模块想搞明白 RFID 扣费这条链路到底怎么串起来。下面按硬件、电路、软件、踩坑、改造的顺序拆。2. 硬件架构拆解AT89S52 主控与 MFRC522 射频前端怎么配2.1 选型对比Mifare One 与 AT89S52 为什么压过 DSP/ARM这套方案在选型上没有追求性能追求的是学生能焊、能调、成本能压到几十块。卡这边条码和磁卡便宜但怕磨、怕磁接触式 IC 卡芯片外露、插拔几百次就接触不良最后落到非接触式 IC 卡也就是射频卡芯片和天线线圈全封在 PVC 里不露金属、不怕水汽、不怕弯折读写靠 13.56MHz 载波供能卡本身不带电池。主控这边的取舍更值得说。DSP、ARM 当然算力强但在这套系统里MCU 干的活其实只有三件跟 RC522 打 SPI 时序、刷一块 16×2 的字符屏、读一颗三线时钟芯片。这些任务连 8 位机都嫌富余。真正吃资源的是稳定和便宜而不是快。维度AT89S52DSP/ARM 平台本系统是否敏感单价几元级数十元级敏感整车批量开发工具Keil C51 ISP 下载需仿真器/JTAG敏感学生可上手5V 电平原生 5V直连 LCD1602多为 3.3V需电平转换敏感少一层器件定时器/串口3 个定时器 1 个全双工串口远超需求不敏感算力12MHz 量级跑协议栈够用过剩不敏感AT89S52 的具体资源是40 引脚、32 个双向 I/O、8KB 可重复擦写 Flash、256B RAM、3 个 16 位定时计数器、1 个全双工串口兼容标准 MCS-51 指令集支持 ISP 在线编程。这里有个关键点常被忽略——Flash 可反复擦写意味着调试阶段不用每次都换芯片对反复改协议时序的项目来说这个便利比多 100MHz 主频实在得多。2.2 总体功能框图四个模块的边界怎么切说明书把系统切成四块这个切法是对的因为它对应了四条完全不同的调试路径控制模块AT89S52 晶振 复位电路负责时序调度和状态机。收发数据模块MFRC522 13.56MHz 晶振 天线匹配网络负责射频层的调制解调。串行通信模块MCU 与读写芯片之间的命令通道以及对外接 PC 的串口。显示模块LCD1602 承担余额、卡号、时间三类信息。这四块之所以要分开是因为它们出故障时的表现完全不同。天线没调好现象是卡贴上去没反应通信时序不对现象是能读到版本号但读不到卡显示模块出问题系统和读卡都正常只是屏上没字。按模块切分之后排查就从整个系统不工作变成哪一段不工作这是硬件调试里最省时间的一步。2.3 MFRC522 的内部结构模拟前端与数字接口的分工MFRC522 是这套方案里的核心器件它是 13.56MHz 非接触通信的读写芯片内部拆成模拟和数字两部分理解这个划分对调试帮助极大。模拟部分负责和空间里的电磁场打交道发送侧驱动天线、提供 13.56MHz 能量载波、按寄存器配置对数据做调制接收侧检测卡返回的负载调制信号并解调。13.56MHz 载波由外接晶振提供天线通常是一个 LC 低通滤波器加 LC 谐振电路——这部分是纯模拟示波器看得见但万用表量不出来。数字部分负责和 MCU 对话寄存器组、FIFO、中断线。说明书里写的是通过并口和中断与单片机通信实际工程里更常见的做法是用 SPI——四根线SCLK/MOSI/MISO/NSS加上一根复位线比起并口能省下大量 I/O。51 没有硬件 SPI那就软件模拟只要时序别太快分频到 1MHz 以下稳定性完全够。这里必须提醒一句SPI 的地址字节有固定格式写寄存器是(reg 1) 0x7E读寄存器要在此基础上把最高位置 1。地址算错是最常见的读出来全是 0x00 或 0xFF的来源。2.4 引脚分配与总线规划51 的 I/O 是稀缺资源32 个 I/O 听着不少但 LCD1602 的 8 位并口一占就是 8 根还得加 RS/RW/EN 三根RC522 SPI 五根、DS1302 三根、蜂鸣器一根剩下能留给按键、指示灯、串口的就不多了。我的习惯是一开始就把引脚定义全部收进一个头文件后期换板子、改走线只改这一处而不是满工程搜P1^3。/* board.h —— 全工程唯一的引脚定义入口换板子只改这里 */ #ifndef __BOARD_H__ #define __BOARD_H__ #include reg52.h /* ---- MFRC522软件模拟 SPI集中在 P1 口 ---- */ sbit RC522_SCLK P1^0; /* 时钟空闲低 */ sbit RC522_MOSI P1^1; /* 主机 - RC522 */ sbit RC522_MISO P1^2; /* RC522 - 主机 */ sbit RC522_NSS P1^3; /* 片选低电平有效 */ sbit RC522_RST P1^4; /* 硬复位低电平有效 */ /* ---- LCD16028 位并口数据走 P0 ---- */ #define LCD_DATA P0 sbit LCD_RS P2^0; sbit LCD_RW P2^1; sbit LCD_EN P2^2; /* ---- 蜂鸣器P2.3 输出低电平导通三极管蜂鸣器响 ---- */ sbit BEEP P2^3; /* ---- DS1302三线同步串行 ---- */ sbit DS1302_RST P2^4; /* 复位/片选 */ sbit DS1302_IO P2^5; /* 双向数据线 */ sbit DS1302_SCLK P2^6; /* 时钟 */ void DelayMs(unsigned int ms); /* 通用毫秒延时11.0592MHz 下标定 */ #endif这段代码的价值不在语法而在于把物理连线这件事收敛成一个可审查的接口。参数说明RC522 的 NSS 和 RST 都是低有效所以初始化时先拉高、再拉低再拉高做一次硬复位LCD 的 RS 决定写的是指令0还是数据1RW 决定读还是写本系统只写不读可以长期拉低省一根控制线的操作麻烦。注意P0口在 8051 里是开漏输出直接驱动 LCD1602 的数据线带不动必须在 P0 与 5V 之间挂一排 10kΩ 上拉电阻。这一条在原理图上很容易漏漏了的表现就是屏只亮背光、不显字符。3. 电路模块逐个过晶振、蜂鸣器、LCD1602 与 DS13023.1 晶振电路11.0592MHz 与 30pF 负载电容的配合系统时钟选 11.0592MHz而不是更整的 12MHz理由很实际11.0592 能被 9600、19200 这些常用波特率整除串口通信的误差接近 012MHz 下算 9600 波特率会偏 8% 以上短帧能凑合长帧就翻车。晶振接在 XTAL1、XTAL2 两个引脚之间两端各挂一只 30pF 无极性电容到地。这两只电容叫负载电容它不是滤波电容作用是配合晶振标称的负载电容值把谐振频率拉到标称点上同时影响起振的幅度和稳定性。常见做法是取值在 15pF 到 33pF 之间具体看晶振手册给的 CL 值CL ≈ C1*C2/(C1C2) CsCs 是 PCB 杂散电容一般按 35pF 估。取 30pF 对应的实际负载大约 18pF是绝大多数 11.0592MHz 晶振的标称值。调试时如果遇到上电不启动、示波器看不到时钟先别怀疑芯片把这两只电容换成 22pF 试一次很多时候就起来了。晶振旁边的走线要短、要远离天线区域天线那边是 13.56MHz 的大电流回路串扰进时钟线会让整个系统跑飞。3.2 蜂鸣器驱动为什么要一颗三极管提示音模块看起来最简单但它是新手最容易烧口的一个点。蜂鸣器分有源和无源有源的给电就响、无源的得给方波不管哪种工作电流都在 20mA 以上而 51 单个 I/O 口的灌电流能力有限长期直驱会拉低口线电平甚至损伤端口。电路里的做法是 P2.3 接三极管基极串一只 1kΩ 限流电阻发射极接地集电极接蜂鸣器负极蜂鸣器正极接 5V。逻辑是反的P2.3 输出高电平三极管截止回路断开不响输出低电平三极管导通回路闭合响。所以代码里响一下写的是拉低。/* 蜂鸣器提示低电平导通持续 ms 毫秒后关闭 */ void Beep(unsigned int ms) { BEEP 0; /* 三极管导通蜂鸣器得电 */ DelayMs(ms); /* 阻塞延时期间不处理其他任务 */ BEEP 1; /* 关断恢复高电平 */ }参数说明ms建议取 50100太短听不清太长会拖慢主循环。这套系统是裸机轮询结构DelayMs是阻塞的所以不要在刷卡主流程里连续调多次 Beep否则会错过下一张卡的寻卡窗口。如果后面要改成刷卡成功两声、失败一声长鸣把Beep拆成BeepOn()/BeepOff()两个非阻塞函数用定时器计数控制时长这是二次开发时第一个该动的地方。3.3 LCD1602 显示HD44780 兼容带来的便利LCD1602 是 16×2 的字符型点阵模块每行 16 个字符每个字符位由 5×7 或 5×11 点阵构成。它好用的地方在于控制器基本都兼容 HD44780 指令集也就是说一套初始化序列可以套用到市面上绝大多数同规格屏上换供应商不用重写驱动。初始化流程是固定的上电延时15ms→ 写 0x388 位数据、2 行显示、5×7 点阵→ 写 0x0C开显示、关光标→ 写 0x06写入后地址自增→ 写 0x01清屏需要额外延时。这几条顺序不能乱尤其是清屏指令执行时间长紧接着写数据会丢。/* LCD1602 底层写操作rs0 写指令rs1 写数据 */ void LCD_Write(unsigned char dat, bit rs) { LCD_RS rs; /* 选择指令寄存器或数据寄存器 */ LCD_RW 0; /* 只写模式 */ LCD_DATA dat; /* 数据送上 P0 口需外部上拉 */ LCD_EN 1; /* 使能拉高 */ DelayMs(1); /* 保持 450ns这里给足 1ms 更保险 */ LCD_EN 0; /* 下降沿锁存 */ DelayMs(2); /* 等待控制器内部执行清屏等指令需更长 */ } /* 在指定行列显示字符串row: 0/1col: 0~15 */ void LCD_ShowStr(unsigned char row, unsigned char col, char *str) { unsigned char addr (row 0) ? 0x80 : 0xC0; /* 两行起始 DDRAM 地址 */ LCD_Write(addr col, 0); /* 先设地址指针 */ while (*str ! \0) { LCD_Write((unsigned char)*str, 1); /* 逐字符写入 */ } }参数说明0x80和0xC0是两行 DDRAM 的首地址第二行实际起始地址是 0x40但指令里要或上 0x80 表示设置地址。LCD_Write里的两次延时是保守值实际 HD44780 大部分指令 40μs 就够只有清屏和归位需要 1.52ms 以上给 2ms 是为了兼容一些慢速兼容芯片代价是刷屏稍慢在刷卡场景里完全可接受。3.4 DS1302 时钟三线同步串行与突发读写公交车刷卡终端必须离线走时——不能假设车上一直有网络。DS1302 就是干这个的低功耗、带 31 字节 RAM、能对年月日周时分秒计时、带闰年补偿工作电压 2.5V5.5V掉电后靠纽扣电池维持。它用三线同步串行和 MCU 通信RST复位/片选、SCLK时钟、IO双向数据。通信规则是低位先行先拉高 RST然后每个 SCLK 上升沿写一位、下降沿读一位命令字节的最低位 D0 先发。命令字节的 bit7 固定为 1bit6 决定读1还是写0bit5~bit1 是寄存器地址bit0 固定 0。这套时序对边沿敏感写的时候要保证 SCLK 拉到高电平后 IO 上的数据已经稳定。/* 向 DS1302 写一个字节低位先行 */ void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_IO dat 0x01; /* 先送最低位 */ dat 1; DS1302_SCLK 1; /* 上升沿DS1302 采样 */ DS1302_SCLK 0; /* 拉低准备下一位 */ } } /* 写寄存器cmd 为命令字节bit71, bit60 表示写 */ void DS1302_WriteReg(unsigned char cmd, unsigned char val) { DS1302_RST 0; /* 先拉低复位重新开始一次传输 */ DS1302_SCLK 0; DS1302_RST 1; /* 拉高片选有效 */ DS1302_WriteByte(cmd); /* 发命令 */ DS1302_WriteByte(val); /* 发数据 */ DS1302_RST 0; /* 传输结束 */ } /* 关闭写保护0x8E 是写保护寄存器bit70 才允许写时间 */ void DS1302_AllowWrite(void) { DS1302_WriteReg(0x8E, 0x00); }参数说明0x8E是写保护寄存器地址上电默认 bit7 为 1写保护开不解除的话后面所有写时间操作都无效但也不报错——这是很隐蔽的一个坑。命令字节0x80是秒寄存器写地址0xBE是时钟突发写一次写 8 个字节秒分时日月周年突发模式的好处是避免跨秒写入导致时间跳变。读取时命令字节把 bit6 置 1比如读秒是0x81。4. 软件流程拆解初始化、寻卡防冲突与扇区扣费链路4.1 系统初始化时钟、串口、中断与 RC522 复位顺序初始化顺序有讲究原则是先让 MCU 自己站稳再去碰外设。正确顺序是关全局中断 → 配置定时器 → 配置串口波特率、8 位数据、1 位停止、开 FIFO→ 配置中断优先级 → 外设逐个复位 → 开全局中断。说明书里那段使能 PLL、设置 PLL 乘除因子、REMAP 重映射的描述其实是 ARM 平台带 PLL 和存储映射的初始化流程51 上根本没有这些寄存器。照着抄会在编译阶段就卡住。在 AT89S52 上主频完全由外部晶振决定没有软件可调的 PLL串口波特率靠定时器 1 工作在方式 28 位自动重装来产生/* 系统初始化11.0592MHz 晶振串口 9600-8-N-1 */ void SystemInit(void) { EA 0; /* 先关总中断配置期间不被打断 */ TMOD 0x0F; /* 清掉定时器1的高4位 */ TMOD | 0x20; /* 定时器1 方式28位自动重装 */ TH1 0xFD; /* 11.0592MHz 下 9600 波特率的重装值 */ TL1 0xFD; TR1 1; /* 启动定时器1产生波特率 */ SCON 0x50; /* 串口方式18位UART允许接收 */ PCON | 0x80; /* SMOD1波特率加倍 */ ES 1; /* 开串口中断 */ RC522_Reset(); /* 硬复位读卡芯片拉低 RST 再拉高 */ LCD_Init(); /* LCD1602 初始化序列 */ DS1302_AllowWrite(); /* 解除时钟芯片写保护 */ EA 1; /* 全部就绪开总中断 */ }参数说明TH1/TL1 0xFD是 11.0592MHz、SMOD1 时 9600 波特率的标准重装值误差为 0如果晶振换 12MHz这个值要重算误差也会变大这就是前面选 11.0592MHz 的实际原因。SCON 0x50拆开看是 SM0SM101方式 18 位异步、REN1允许接收。初始化的最后一步才是EA 1顺序颠倒会导致中断在配置到一半时进来读到半初始化的状态。4.2 寻卡与防冲突从 REQA 到拿到 4 字节 UID寻卡这一段是整个系统的核心链路也是协议味最重的部分。Mifare One 卡在进入读写器射频场后靠感应取电上电复位然后进入 IDLE 状态等命令。读写器先发 REQA短帧0x26或 WUPA0x52场内所有处于 IDLE 的卡同时用自己的 UID 做负载调制回应这就是冲突——多张卡同时说话读回来的是乱码。防冲突机制就是解决这个的读写器发 ANTICOLLISION0x93 0x20卡回自己的 UID多卡冲突时读写器用位冲突检测逐位筛选每轮锁定 1 位8 位一轮三轮之后剩一张卡被选中返回完整 UID4 字节S50 是单尺寸 UID。选中之后发 SELECT0x93 0x70 UID CRC卡回 SAK确认卡型然后未被选中的卡进入 HALT 状态不再响应后续命令。/* 寻卡pTagType 返回卡类型0x0400 Mifare One (S50) */ unsigned char PICC_Request(unsigned char reqMode, unsigned int *pTagType) { unsigned char status; unsigned int backBits 0; RC522_WriteReg(CommandReg, PCD_IDLE); /* 先让芯片空闲 */ RC522_WriteReg(BitFramingReg, 0x07); /* 短帧7 位有效 */ RC522_WriteReg(ComIEnReg, 0x00); RC522_WriteReg(ComIrqReg, 0x7F); /* 清所有中断标志 */ RC522_WriteReg(FIFOLevelReg, 0x80); /* 清 FIFO 读写指针 */ RC522_WriteReg(FIFODataReg, reqMode); /* 0x26 REQA0x52 WUPA */ RC522_WriteReg(CommandReg, PCD_TRANSCEIVE); /* 启动收发 */ RC522_WriteReg(BitFramingReg, 0x87); /* 启动发送 */ status RC522_WaitFor(0x30, 2000); /* 等 RxIRq 或 IdleIRq */ if (status ! MI_OK) { return status; } RC522_ReadReg(FIFODataReg, ...); /* 读回 2 字节 ATQA */ *pTagType (buf[0] 8) | buf[1]; return MI_OK; }参数说明reqMode传0x26REQA时只能唤醒 IDLE 态卡传0x52WUPA能连 HALT 态的卡一起唤醒实际产品里用 WUPA 更稳。BitFramingReg先写0x07是因为 REQA 是 7 位短帧写完命令字后要再写0x87最高位是 StartSend启动发送。RC522_WaitFor(0x30, 2000)里的 0x30 是掩码表示等 RxIRq 或 IdleIRq 任一置位2000 是超时计数取值跟主频有关太小会误判超时太大会卡住主循环。拿到 ATQA 之后就是防冲突和选卡这两步通常合在一个PICC_Anticoll()里做逐位比较、冲突位置位。实际调试时如果PICC_Request一直返回 MI_NOTAGERR无卡八成是天线匹配没调好或者 SPI 地址算错如果防冲突阶段返回的 UID 全是 0x00那基本是 CRC 校验没打开。4.3 三次认证与扇区读写钱包块怎么组织选中卡之后不能直接读数据得先过认证。S50 分 16 个扇区每扇区 4 块块 03每块 16 字节每扇区的块 3 是密钥区Key A 6 字节 存取控制 4 字节 Key B 6 字节。认证用的是三次互相确认卡发一个随机数给读写器 → 读写器用扇区密钥加密后连同自己的随机数回给卡 → 卡验证通过后回传加密结果。认证命令字很明确0x60用 Key A 认证0x61用 Key B 认证。认证通过后对扇区的读写就用加密流通信了换一个扇区必须重新认证不能复用。/* 校验扇区密码mode: 0x60KeyA, 0x61KeyBblockAddr 为绝对块号 */ unsigned char PICC_Auth(unsigned char mode, unsigned char blockAddr, unsigned char *pKey, unsigned char *pUid) { unsigned char status; unsigned char buf[12]; buf[0] mode; /* 0x60 或 0x61 */ buf[1] blockAddr; /* 要访问的块地址0~63 */ memcpy(buf[2], pKey, 6); /* 6 字节密钥 */ memcpy(buf[8], pUid, 4); /* 卡序列号最后 4 字节 */ RC522_WriteReg(CommandReg, PCD_IDLE); RC522_WriteReg(ComIrqReg, 0x7F); RC522_WriteReg(FIFOLevelReg, 0x80); RC522_WriteFIFO(buf, 12); RC522_WriteReg(CommandReg, PCD_MFAUTHENT); /* 0x0E认证命令 */ status RC522_WaitFor(0x10, 3000); /* 等 RxIRq */ return (status MI_OK) ? MI_OK : MI_ERR; }参数说明blockAddr是绝对块号扇区 0 的块 03 对应 03扇区 1 对应 47以此类推。传参时最容易犯的错是把扇区内块号当绝对块号传进去结果认证的是别的扇区返回 MI_ERR 但代码不报错现象就是能读到卡号但读不出余额。钱包块的格式值得单独说。S50 的电子钱包不是一个普通数据块它有固定结构字节 03 存值小端47 存值的反码811 再存一遍值12 存块地址13 存地址反码14 存地址15 存地址反码。只用普通 Read/Write 直接改数字是不行的必须用加值0xC1/减值0xC0 传输0xB0这组指令卡内部会自动维护反码一致性这也是 S50 钱包能做离线扣费的可靠性来源。扣费的标准动作序列是读钱包块确认余额够 → 减值指令0xC0 块号 4 字节金额→ 卡把结果暂存在内部数据寄存器 → 传输指令0xB0 块号把寄存器内容写回块。少了传输这一步减值只发生在缓冲区里卡一离开射频场就丢了余额不变——这是扣费不成功但系统显示扣了的经典原因。4.4 主循环总流程一次刷卡的完整时间线把上面的原子操作串起来主循环是一个状态机轮询寻卡 → 有卡则防冲突拿 UID → 选卡 → 认证钱包扇区通常用扇区 1 或更高扇区 0 的块 0 是厂商区只读→ 读钱包余额 → 判断余额是否够本次票价 → 够则减值 传输 蜂鸣器短响 LCD 刷新余额不够则蜂鸣器长响 LCD 提示 → 发 HALT 让卡休眠 → 回到轮询。这条链路上每一步都有超时任何一步失败都得回到最开始重新寻卡而不是在原地死等。轮询周期建议控制在 100200ms太快了卡还没上电复位完成太慢了用户体验差。HALT 一定要发否则同一张卡会一直被读到出现刷一次扣两次的问题。5. 避坑与排查这套板子最容易翻车的五处5.1 卡贴上去完全没反应程序停在寻卡现象烧录正常、LCD 能亮能显示、串口能打印但卡放上去PICC_Request永远返回无卡示波器测天线两端看不到 13.56MHz 载波。原因三种可能按概率排序——一是 RC522 的硬复位没做对芯片还在掉电复位状态二是 SPI 地址算法错了读版本号寄存器VersionReg0x37读回来不是 0x91/0x92三是天线匹配网络的电容参数不对谐振点偏了。解决先单独写一个 10 行的自检程序只做一件事——读 VersionReg 并打印。这一步能砍掉一半的排查路径。读不到正确版本号就是通信层问题别去动天线读得到版本号但寻不到卡才是射频层问题。射频层先量天线两端有没有 13.56MHz 正弦波没有就查 RC522 的TxControlReg0x14有没有把 TX1/TX2 使能位置 1——这个寄存器上电默认是关的忘了写就永远发不出载波。5.2 能寻到卡、能拿到 UID但认证一直返回失败现象PICC_Request和PICC_Anticoll都正常UID 打印出来也对一到PICC_Auth就返回 MI_ERR换几张卡都一样。原因密钥不对是最常见的——S50 出厂默认 Key A 是FF FF FF FF FF FF但很多公交卡已经改过密钥用出厂密钥当然过不去。其次是块地址传错把扇区内块号当绝对块号再其次是认证时传给芯片的 UID 只有 4 字节而某些卡需要完整序列号。解决先用一张确定没改过密钥的白卡验证代码逻辑过了再换真卡。如果白卡能过、真卡不过那就是密钥问题不是代码问题这时要么拿到正确密钥要么改读扇区 0 的块 0厂商区只读不需要密钥就能读到 UID。另外认证成功后如果不做任何操作直接切到另一个扇区前一次的认证状态会被清除必须重新认证这一点写代码时容易漏。5.3 扣费金额对不上或者扣了又变回去现象LCD 上显示扣款成功余额也变了但卡拿开再贴一次余额还是原来的数或者连续刷两次扣了两次。原因前者是减值后没发传输指令0xB0只改到了 RC522 与卡之间的缓冲区后者是刷卡后没发 HALT0x50卡一直停留在 ACTIVE 态主循环下一轮又读到同一张卡。解决把减值 传输当成一个不可拆分的原子操作封装成一个函数函数返回成功才算扣费完成中间任何一步失败都要回滚到寻卡。HALT 则放在流程末尾无条件执行并且加一个简单的去重逻辑记录上一张卡的 UID 和最后一次刷卡的时间戳同一 UID 在 1.5 秒内重复读到直接丢弃。这个时间窗不是拍脑袋是给乘客抬卡离开留的物理时间。5.4 LCD1602 只亮背光不显字符或者满屏方块现象上电后屏亮但一个字符都没有或者显示一排黑方块。原因黑方块通常是初始化序列没跑完或对比度电位器V0 引脚调得太偏完全不显示则多半是 P0 口没加上拉电阻——8051 的 P0 是开漏结构没有上拉就输出不了高电平数据线永远是低控制器收到的全是 0x00。解决先在 P0 与 5V 之间焊一排 10kΩ 排阻再调 V0 上的 10kΩ 电位器一般调到中间偏下位置字符最清晰。初始化失败还有一种隐蔽情况LCD_Write里使能脉冲宽度不够某些兼容芯片要求 EN 高电平保持 450ns 以上快一点的 MCU 上LCD_EN1; LCD_EN0;连写可能只有几十纳秒中间加一句短延时最保险。5.5 断电再上电时间和余额都归零现象DS1302 走时正常但整机断电再上电时间回到 2000 年 1 月 1 日卡里的余额也变成 0。原因时间归零是 DS1302 的备用电池没接或接反主电源掉电后芯片没有维持电源余额归零则更严重——说明钱包块被写坏反码不一致导致卡内部判定该块无效直接清零保护。解决DS1302 的 VCC1 必须接 3V 纽扣电池VCC2 接系统 5V两者不能接反接反会倒灌。钱包块的保护靠两条一是永远走减值/加值指令绝不直接 Write 钱包块二是每次扣费前先读一遍钱包块校验值和反码是否互为按位取反不匹配就拒绝操作并提示卡片异常。这个校验只多花几毫秒但能挡住绝大多数写坏卡的事故。6. 二次开发改造把说明书变成能跑的原型这份文档给的是设计思路和模块划分真正要落地第一件事是补一个能跑的工程骨架。我一般会按四层来搭board.h放引脚、rc522.c/h放射频驱动、app.c放业务状态机、main.c只做初始化和主循环。这样分的好处是换 MCU 平台时比如从 51 换到 STM32只需要重写board.h和 SPI 的四个底层函数rc522.c里的协议逻辑一行不用动。第一个值得做的改造是把阻塞延时换成定时器节拍。现在的DelayMs在蜂鸣器和 LCD 操作里到处用一旦要加待机 30 秒无操作进入低功耗这类需求就卡住了。做法是用定时器 0 产生 1ms 中断维护一个全局volatile unsigned long tick所有超时判断改成记录起始 tick比较差值蜂鸣器改成状态机控制开关。这个改动大概两百行但后面所有功能都好加。改造项现状建议做法收益时间基准DelayMs阻塞定时器 0 1ms 节拍支持非阻塞逻辑、低功耗存储余额只在卡里卡内钱包 本地 EEPROM 流水掉线可对账通信仅串口打印串口协议帧帧头长度CRC可对接上位机安全默认密钥每张卡独立密钥 扇区分散防复制显示余额时间加余额不足/请重刷状态页体验提升第二个改造点是流水记录。公交场景最怕的是乘客说扣了、系统说没扣所以本地要存最近 N 笔交易UID、时间、金额、余额存在 AT24C02 这类 I2C EEPROM 里按环形缓冲写。校验方法很简单连续刷同一张卡 100 次把 EEPROM 里的流水和卡内余额做差值对不上就说明某一环有事务没闭环。第三个是密钥体系。默认的FF FF FF FF FF FF只能用于打样量产必须一卡一密——用 UID 做分散因子通过一个分散算法算出该卡的扇区密钥读写器端实时算、不存明文。这一步不做卡被 clone 一次整个系统就废了。我自己的习惯是每次改完驱动先不接业务单独跑一个读卡 1000 次不失败的压测循环把失败率打到 0 再去接扣费逻辑。血泪经验是协议层的偶发失败一旦混进业务代码里会被误判成业务 bug查起来像在黑匣子里摸。从那以后我每次加新外设都强制先走一遍裸驱动压测再谈业务。希望帮到你。本文还有配套的精品资源点击获取