简介:围绕RFID公交车刷卡系统展开的电子系统设计说明书,面向电子信息工程、嵌入式与交通信息化方向的课程设计学习者,重点回应公交一卡通场景下的刷卡识别、显示与时钟管理需求。资源包共1个doc文件,约649KB,正文以设计说明、系统框图和电路流程描述为主,便于按章节查阅方案选型与实现思路。文档从研究背景入手,依次讨论Mifare One射频卡与AT89S52单片机的选型依据,并覆盖晶振电路、提示音模块、LCD1602显示、DS1302时钟模块及系统总体方案;软件部分给出初始化、射频卡与读写器通信和总流程图,结构接近完整课程设计报告。已有4297人学习,适合需要参考硬件组成、模块划分与软件流程的读者快速建立设计框架,也可用于比对同类一卡通方案的取舍与文档组织。
1. 早高峰那三秒钟:这套 RFID 公交车刷卡系统要解决什么
早高峰车门一开,队伍从投币箱前排到后门,如果每人刷卡多花两秒,一趟车就少拉十几个人。这套基于 RFID 的公交车刷卡系统设计要解决的就是这三秒钟:卡片贴近读头十厘米以内,读卡、验卡、扣费、响一声、屏上跳出余额,整个流程压在 300 ms 以内完成。方案用 AT89S52 单片机做主控,配 MFRC522 射频读写芯片和 Mifare One(S50)非接触卡,带 LCD1602 显示、DS1302 走时和蜂鸣器提示,是典型的电子系统设计题目——器件便宜、资料齐全、焊出来能真刷。它适合要交一份完整硬件加软件方案的电子信息类学生,也适合想拿一套低成本读卡模块改造成门禁、考勤、食堂消费终端的从业者。
2. 总体方案与器件选型:四个模块如何分工完成一次刷卡
一次刷卡看着简单,拆开是四条并行又互相牵制的链路:射频链路负责把 13.56 MHz 的能量送出去并收回卡片调制回来的数据,控制链路负责跑协议状态机,人机链路负责发声和显示,时间链路负责给每笔交易打上时间戳。模块划分如果一开始没定清楚,后面调试就会陷入「到底是卡的问题、天线的问题还是程序的问题」的黑匣子状态。我一般先把职责边界画成一张表固定下来,再逐个模块选器件,谁负责什么、接口是什么电平、时序由谁主导,全部写死。
2.1 系统功能结构框图与四个模块的职责边界
按功能划分,整套系统分为控制模块、发送接收数据模块、串行通信模块、显示模块。控制模块由单片机和 11.0592 MHz 晶振构成,是整个系统的主时钟源和决策中心;发送接收数据模块由 MFRC522、13.56 MHz 晶振、天线及匹配网络组成,模拟部分驱动天线产生载波并调制发送、检测解调卡片回传信号,数字部分通过串行接口与单片机交换命令和数据;串行通信模块负责读卡器与上位机之间的数据回传,把卡号、扣费金额、时间打包上传;显示模块包括 LCD1602、DS1302 和蜂鸣器,负责余额、票价、时间的呈现和刷卡成功提示。
| 模块 | 核心器件 | 职责 | 与主控接口 |
|---|---|---|---|
| 控制模块 | AT89S52、11.0592 MHz 晶振、复位电路 | 协议状态机、扣费逻辑、任务调度 | 系统主体 |
| 收发数据模块 | MFRC522、13.56 MHz 晶振、PCB 环形天线 | 载波发射、卡片寻呼、防冲突、加解密通信 | 软件模拟 SPI,4 根 I/O |
| 串行通信模块 | AT89S52 内置 UART | 交易记录上传、参数下发 | RXD/TXD,9600 bps |
| 显示模块 | LCD1602、DS1302、有源蜂鸣器 | 余额与时间显示、刷卡提示音 | 8 位并口 + 3 线时钟 + 1 根 I/O |
这张表的价值在于后续排故时能快速二分。卡刷不上,先看是收发模块没返回卡号,还是控制模块收到卡号却没走完认证,还是显示模块刷新卡死把主循环堵住了。职责分清楚,问题就落在一个格子里,不用满板子乱猜。
2.2 主控选型:为什么是 AT89S52 而不是 ARM
这个题目最容易出现的争论是「都什么年代了还用 51」。从工程角度讲,选型不是比谁先进,是比谁在这个需求下性价比最高。刷卡终端的核心负载是:驱动一个 SPI 外设、跑一个几百字节的状态机、刷一块 16×2 字符屏、走一个 UART。这些活儿 AT89S52 完全扛得住,而且它的开发链条成熟到什么程度——编译器、下载器、例程、调试方法全都是现成的,出问题有大量前人踩过的坑可查。
AT89S52 的关键参数:40 引脚封装,32 个双向 I/O 口,片内 8 KB 可重复编程 Flash(不同批次手册标注略有出入,接线前按手头器件的数据手册核对一遍),256 字节 RAM,3 个 16 位可编程定时计数器,1 个全双工串行口,兼容标准 MCS-51 指令系统,支持在线编程(ISP),可反复擦写,开发成本低。相比之下 DSP 和 ARM 方案在主频、运算能力、片上外设上确实强,但对这个项目来说属于过量配置:价格高出一截,开发环境上手周期长,而且大部分算力闲置。
真正需要算清楚的是 RAM。256 字节要装下:LCD 显示缓冲区(可选,也可以直接写指令不缓存)、串口收发缓冲、卡片 UID(4 字节或 7 字节)、密钥(6 字节)、MFRC522 的 FIFO 数据缓冲(读块操作一次 16 字节)。这些加起来不到 80 字节,剩下还有余量。如果后面想加「最近 10 笔交易流水」这种功能,就得压缩缓冲或者外挂 24C02,这是选型时应该提前想到的扩展点。
提示:选 51 不代表可以省掉看门狗。刷卡机在车上是长期上电设备,程序跑飞后必须能自己复位,AT89S52 内置看门狗定时器,初始化和喂狗要写进主循环。
2.3 射频卡与读写芯片的搭配:Mifare One 加 MFRC522
卡片这一侧的选择理由很直接。市面上常见的有条码、磁卡、接触式 IC 卡、非接触式 IC 卡。条码和磁卡在超市、银行用得多,成本低但易复制、易磨损;接触式 IC 卡芯片外露,插拔次数一多就接触不良,早年公共电话亭那种故障率就是教训;非接触式 IC 卡把芯片和线圈封装在卡片内部,不怕弯折、不怕灰尘油污,刷卡不用对准,可靠性明显更高。
最终选用 Mifare One 卡(业内常称 S50、M1 卡)。它的存储结构是 1 KB EEPROM,分成 16 个扇区,每个扇区 4 个块,每块 16 字节。第 0 扇区第 0 块是厂商固化区,存 uid 和厂商信息,只读;每个扇区的第 3 块是尾块,存密钥 A(6 字节)、存取控制位(4 字节)、密钥 B(6 字节);其余块是数据块,可以配置成普通读写块,也可以配置成值块(钱包)。值块这个特性是公交刷卡场景的关键,加减值由卡片硬件保证原子性,比在单片机里做加减运算再写块安全得多。
读写端选 MFRC522。它工作在 13.56 MHz,支持 ISO14443A 协议,正好对得上 Mifare One;内部集成发送调制、接收解调、CRC 校验、加解密,单片机只需要发命令和读结果,不用自己啃射频时序;支持 SPI、UART、I2C 三种接口,供电 3.3 V。要注意 MFRC522 是 3.3 V 器件,而 AT89S52 的 I/O 是 5 V,直连会烧芯片,这不是可选项而是必须处理的硬约束,后面会专门讲接法。
3. 硬件电路设计:从最小系统到天线匹配
硬件这一层最容易出现「原理图全对、焊出来不工作」的情况。原因往往不在逻辑,而在模拟部分:晶振负载电容选偏了频率就飘,天线匹配没调好读距就从十厘米掉到贴着才灵,电源纹波一大读卡瞬间就复位。所以这一章不只给连接方式,还把每个电路的参数为什么这么取值讲清楚,读者照着改的时候知道动哪个旋钮。
3.1 最小系统与 11.0592 MHz 晶振的负载电容
晶振接在单片机的 XTAL1、XTAL2 两个引脚之间,两端各接一只无极性电容到地,容量取 30 pF。这两只电容叫负载电容,作用是配合晶体本身的标称负载电容,让振荡电路工作在标称频率上,同时影响起振速度和输出幅度。取值不是随便填的:晶振规格书会标一个 CL 值(常见 12 pF、20 pF),实际用电容约等于 2×(CL − 杂散电容),杂散电容一般估 3~5 pF。取 30 pF 是这套电路里稳妥的常见值,如果发现起振慢或者频率偏移大,可以把这两只电容换成 22 pF 或 33 pF 对比。
为什么偏偏选 11.0592 MHz 而不是更整的 12 MHz?关键在串口。51 的串口波特率由定时器溢出率分频得到,11.0592 MHz 这个数能被 9600、19200、57600 这些标准波特率整除,误差为 0;换成 12 MHz,9600 的误差就到了 8% 以上,通信会出现偶发乱码。做长期运行的设备,这种玄学故障最难查,源头就在晶振频率上。
复位电路用 10 μF 电解电容配 10 kΩ 电阻的经典上电复位,EA 引脚接高电平表示从片内 Flash 取指令。下面这段代码把 11.0592 MHz 换算成 1 ms 定时基准,是后面所有超时判断和去抖延时的基础。
#include <reg52.h> #define FOSC 11059200UL /* 系统晶振 11.0592 MHz */ #define T1MS (65536 - FOSC/12/1000) /* 方式1下 1ms 的初值:65536-921=64615 */ /* 定时器0:方式1,16位定时,1ms 中断一次,作为系统软时钟 */ void Timer0_Init(void) { TMOD &= 0xF0; /* 保留定时器1的配置,清空定时器0的4位 */ TMOD |= 0x01; /* 定时器0 工作在方式1(16位定时器) */ TH0 = T1MS >> 8; /* 装入初值高字节 0xFC */ TL0 = T1MS & 0xFF;/* 装入初值低字节 0x67 */ ET0 = 1; /* 允许定时器0中断 */ EA = 1; /* 开总中断 */ TR0 = 1; /* 启动定时器0 */ }这里的 921 是 11.0592 MHz 除以 12 再除以 1000 得到的每毫秒机器周期数,取整后每毫秒有约 0.1% 的误差,对刷卡超时判断、LCD 延时、蜂鸣器发声时长完全够用。如果要做精确到秒的计时,不能靠这个软时钟累积,得交给 DS1302。这也是为什么系统里既要定时器又要时钟芯片——一个负责毫秒级的流程节奏,一个负责年月日时分秒。
3.2 蜂鸣器驱动电路为什么要加三极管
提示音用有源蜂鸣器,读写操作完成时响一声。蜂鸣器不能直接挂在 I/O 口上,原因很实在:单片机每个 I/O 的灌电流能力有限,而蜂鸣器工作电流通常在 20~30 mA 以上,直驱轻则声音发闷,重则长期过流把口线拉坏。所以在 P2.3 和蜂鸣器之间加一只 NPN 三极管做开关,基极串 1 kΩ 限流电阻,蜂鸣器回路里再串 100 Ω 限流。
工作逻辑是反相的:P2.3 输出高电平时三极管截止,蜂鸣器回路断开,不响;P2.3 输出低电平时三极管导通,回路闭合,蜂鸣器发声。写代码的时候要记住这个极性,别写成「置高发声」,否则一上电就一直叫。
sbit BEEP = P2^3; /* 蜂鸣器控制脚,低电平有效 */ /* 响 ms 毫秒,调用时主循环会被阻塞,所以时长控制在 50ms 内 */ void Beep(unsigned int ms) { BEEP = 0; /* 三极管导通,蜂鸣器发声 */ while (ms--) { Delay1ms(); /* 复用 1ms 定时基准的软件延时 */ } BEEP = 1; /* 关断,避免长期通电发热 */ }参数上唯一的讲究是发声时长。刷卡成功的「嘀」一般给 50 ms,失败给两声短促的,各 30 ms 中间隔 60 ms。时长太长会让乘客以为机器卡住了,太短在嘈杂车厢里听不见。
3.3 LCD1602 与 DS1302 的接口和走线
LCD1602 是 16×2 的字符型液晶,每行 16 个字符,每个字符位由 5×7 点阵组成,控制器基本都基于 HD44780 兼容芯片,所以一套驱动代码可以通吃市面上大多数字符屏。接口上 RS、RW、E 三根控制线加 8 根数据线,共 11 根 I/O;如果 I/O 紧张可以改成 4 位模式,数据线只用高 4 位,代价是每次要分两次送。这套系统 I/O 够用,走 8 位模式,刷新更快。对比度调节端 VL 接 10 kΩ 电位器到地,焊完先调这个,否则最常见的问题是「屏亮了但一个字看不见」。
DS1302 是低功耗带 RAM 的实时时钟芯片,能对年、月、日、周、时、分、秒计时,带闰年补偿,工作电压 2.5~5.5 V,通过三线(CE、SCLK、I/O)与单片机同步通信,支持突发模式一次传多个字节。它外接 32.768 kHz 晶振和一颗纽扣电池,主电源掉电后继续走时。走线上有一条硬要求:32.768 kHz 晶振尽量靠近芯片引脚,两根引脚走线短而等长,下面不要走其他信号线,否则走时会出现「一天慢几秒」的偏差。通常的做法是在晶振两端各接一只 6 pF 左右的电容到地,具体看晶振规格书。
3.4 天线与匹配网络:13.56 MHz 载波这一段
MFRC522 的能量载波由 13.56 MHz 晶振提供,通过内部电路驱动 TX1、TX2 输出,经 LC 低通滤波和 LC 谐振网络送到天线。天线本体是 PCB 上的一圈或几圈铜箔环形走线,属于整个设计里唯一需要「调」而不是「算」的部分。
匹配网络的典型结构是:TX1/TX2 各串一只电阻(常见 1~10 Ω,用来抑制谐波和限流),再串一只电容,天线两端并一只可调电容,构成谐振回路,让整个回路谐振在 13.56 MHz。调的方法不复杂:给系统上电,用示波器探头(低电容探头,或者自制一个小环探头)靠近天线,看天线两端的电压幅度,旋动可调电容,找到幅度最大的那个点,就是谐振点。这时候读卡距离最远。
读距还跟天线面积、圈数、Q 值有关。板载天线常见做到 5~10 cm,想更远就得加大天线面积,但 Q 值太高会导致带宽变窄、对卡片频率偏差更敏感,反而在批量卡上表现不稳。课程设计这个量级,5 cm 左右稳定读卡就够验收了。
| 环节 | 器件/参数 | 作用 | 调试要点 |
|---|---|---|---|
| 载波源 | 13.56 MHz 晶振 | 提供射频基准 | 负载电容按晶振规格取,别照抄 30 pF |
| 滤波 | LC 低通 | 抑制高次谐波 | 电感和电容按参考设计取值 |
| 匹配 | 串联电阻 + 可调电容 | 阻抗匹配、谐振点调节 | 示波器看天线幅度找最大值 |
| 天线 | PCB 环形铜箔 | 发射能量、接收负载调制 | 天线正下方禁止铺铜和走线 |
注意:天线区域下方的地平面必须挖空。很多人图省事让整板铺地,结果天线能量全被地平面吸走,读距只剩一两厘米,还以为是芯片坏了。
4. 软件设计:从初始化到扣费,一条完整的刷卡时序
软件部分的价值不在于写了多少行,而在于把卡片操作拆成确定性状态机。Mifare One 的访问流程是强顺序的:寻卡、防冲突、选卡、认证、读写、中止,任何一步跳过或者顺序颠倒,后面都拿不到正确结果。这一章按调用顺序给代码骨架,同时说明每一步失败时返回什么、上层该怎么处理,读者照着改就能跑起来。
4.1 系统初始化与串口、中断配置
初始化要按依赖顺序来:先配时钟和定时器,再配串口,再配外部芯片,最后开中断。串口是读卡器与上位机的通信端口,使用前要设波特率、收发字符格式、清 FIFO;串口和 MFRC522 都基于中断工作,还要对中断优先级做分配,避免射频通信被打断。
/* 串口初始化:9600 bps,8 位数据,1 位停止,无校验(11.0592MHz 下误差为 0) */ void Uart_Init(void) { SCON = 0x50; /* 方式1,8位UART,允许接收 */ TMOD &= 0x0F; TMOD |= 0x20; /* 定时器1 方式2,8位自动重装,做波特率发生器 */ TH1 = 0xFD; /* 9600bps 初值:256 - 11059200/(12*32*9600) = 253 */ TL1 = 0xFD; TR1 = 1; ES = 1; /* 开串口中断 */ } /* 中断优先级:射频通信 > 串口 > 定时器,保证寻卡时序不被串口打断 */ void Int_Priority_Init(void) { IP = 0x01; /* 定时器0中断设为高优先级,用于超时判断 */ /* 串口与射频操作放在主循环中顺序执行,避免中断嵌套导致 SPI 时序错乱 */ }这里有个设计取舍值得说明:软件模拟 SPI 的时序对时间敏感,如果把寻卡流程放在中断里,被串口中断打断几十微秒,就可能错过卡片的响应窗口。所以通常的做法是中断只做标志位和缓冲区搬运,真正的卡片操作全部放在主循环里顺序执行。中断优先级配置的目的不是让流程更快,而是让超时基准不被拖慢。
4.2 寻卡、防冲突、选卡与三次认证的状态机
这一段是整套软件的核心。Mifare One 的通讯协议和波特率是固定的,卡片进入读写器操作范围后,读写器按协议发起呼叫,判断是不是 M1 卡;多张卡同时在范围内时,防冲突机制从中选一张操作,未被选中的回到空闲状态等待下一次选卡,这个过程会返回被选卡的序列号;随后按序列号选中该卡并返回容量代码;最后选定要访问的扇区,用密钥对该扇区做三次相互认证,认证通过后才能建立加密通道通信。换扇区就要重新认证,这是很多人漏掉的一点。
/* MFRC522 软件模拟 SPI 底层:主机先发地址字节,再连发数据 */ unsigned char RC522_ReadReg(unsigned char addr) { unsigned char dat; RC522_NSS = 0; /* 片选拉低 */ RC522_WriteByte((addr << 1 & 0x7E) | 0x80); /* 读命令:最高位置1 */ dat = RC522_WriteByte(0x00); /* 读回一个字节 */ RC522_NSS = 1; return dat; } /* 完整寻卡流程:返回 0 表示成功拿到 4 字节 UID */ unsigned char Pcd_ReadCard(unsigned char *uid) { unsigned char status; status = Pcd_Request(PICC_REQIDL, CardType); /* 寻天线区内未进入休眠的卡 */ if (status != MI_OK) return 1; /* 无卡或卡类型不符 */ status = Pcd_Anticoll(uid); /* 防冲突,返回序列号 */ if (status != MI_OK) return 2; /* 多卡冲突未解决 */ status = Pcd_Select(uid); /* 选卡,返回容量代码 */ if (status != MI_OK) return 3; /* 选卡失败,卡可能已离开 */ return 0; } /* 三次互相认证:keyType 0x60 用密钥A,0x61 用密钥B */ unsigned char Pcd_Auth(unsigned char keyType, unsigned char block, unsigned char *key, unsigned char *uid) { return Pcd_AuthState(keyType, block, key, uid); /* 返回 MI_OK 才可读写 */ }三处失败返回值不同,好处是排故时串口一打印就知道卡在哪一步:返回 1 是根本没寻到卡,先怀疑天线和卡片;返回 2 是场上不止一张卡;返回 3 是选卡过程中卡片被拿走了。这个错误码设计比统一返回 0/1 有用得多,答辩演示时也能主动解释异常处理。
4.3 值块加值与减值:扣费事务怎么保证不掉钱
公交场景的钱包必须用 Mifare 的值块,不能当普通数据块读写。值块的 16 个字节有固定格式:前 4 字节是数值(小端),接着 4 字节是数值按位取反,再 4 字节重复数值,最后 4 字节重复取反并附块地址校验。这个冗余结构的目的是让卡片自己校验数据的完整性,被篡改或写坏时能识别出来。
钱包操作有一套专用命令:初始化钱包把指定块定义成钱包格式;加值对钱包块加钱;减值减钱;存储把块内容存到数据寄存器;传输把数据寄存器内容写回块;中止让卡进入暂停状态。扣费的正确序列是:认证 → 读余额 → 判断余额是否够票价 → 减值 → 传输。
/* 一次完整扣费:成功返回 0,余额不足返回 1,通信失败返回 2 */ unsigned char Wallet_Pay(unsigned char block, unsigned char *key, unsigned char *uid, unsigned short fare, unsigned short *balance) { unsigned short old; if (Pcd_Auth(0x60, block, key, uid) != MI_OK) return 2; /* 认证失败 */ if (Pcd_ReadValue(block, &old) != MI_OK) return 2; /* 读当前余额 */ if (old < fare) { /* 余额不足 */ *balance = old; return 1; /* 不打孔、不扣费 */ } /* 关键:减值只写入芯片内部数据寄存器,掉电也不会改变卡片余额 */ if (Pcd_Decrement(block, fare) != MI_OK) return 2; /* 传输才是真正落盘,这一步之前的任何掉电都等于没扣钱 */ if (Pcd_Transfer(block) != MI_OK) return 2; *balance = old - fare; return 0; }这个「减值 + 传输」两步是整套扣费逻辑里最该讲清楚的地方。减值命令只是把计算结果放进读写器与卡片之间的缓冲,真正写进 EEPROM 的是传输命令。如果中间掉电、卡片被拿走、通信中断,余额保持原样,不会出现「扣了钱没记账」或者「没扣钱却掉了余额」。反过来说,如果图省事直接读块、在单片机里做减法、再写块,一旦在写块的过程中断,整块数据可能变成非法值,卡片就废了。
提示:写块之前务必确认该块已被配置成值块。有些卡出厂时全扇区都是普通数据块,直接对它发减值命令会返回错误,这类卡要先按值块格式初始化。
4.4 主流程与显示刷新节奏
主循环按固定节奏轮转:寻卡 → 认证 → 扣费 → 显示 → 鸣响 → 防重复刷卡 → 读时间。防重复刷卡这一步不做会很尴尬:乘客把卡放在读头上没拿走,一秒钟扣十次。通常的做法是缓存上一次成功的 UID,同一张卡在 2 秒内不再触发扣费,2 秒后再放上去才允许第二次交易。
显示刷新要单独说明。LCD1602 每次写指令后需要延时等待,如果把这个延时放在中断里,会打乱 DS1302 的三线时序;如果放在主循环里裸等,又会拖慢寻卡。稳妥做法是把「余额变化」和「时间秒变化」作为刷新触发条件,而不是每圈都刷,时间显示只刷新秒位,余额只在扣费后整行刷新。
void main(void) { unsigned char uid[4], status; unsigned short balance; Mcu_Init(); /* 单片机、定时器、串口、中断初始化 */ RC522_Init(); /* 复位 MFRC522,开启天线,设置接收增益 */ LCD1602_Init(); /* 液晶初始化,清屏,设置显示模式 */ DS1302_Init(); /* 时钟芯片初始化,首次上电写默认时间 */ LCD_ShowString(0, 0, "Bus Card Ready "); while (1) { status = Pcd_ReadCard(uid); /* 寻卡 + 防冲突 + 选卡 */ if (status == 0 && !Is_Same_Card_Within(uid, 2000)) { status = Wallet_Pay(0x04, KEY_A, uid, FARE, &balance); if (status == 0) { /* 扣费成功 */ Beep(50); LCD_ShowBalance(balance); Uart_Report(uid, FARE, balance, DS1302_GetTime()); } else if (status == 1) { /* 余额不足 */ Beep(30); Delay1ms(60); Beep(30); LCD_ShowString(0, 1, "Low Balance "); } else { /* 通信异常 */ Beep(30); } Remember_Card(uid); /* 记录本次卡号,用于防重复 */ } Refresh_Clock(); /* 秒变化时才刷新时间显示 */ } }主循环里没有任何长延时,所有等待都由定时器超时兜底。这个结构的好处是即使某张卡认证失败,程序也不会卡死,下一轮照样能响应新卡。
5. 联调避坑:五个最容易翻车的地方
硬件焊完、程序烧进去,第一遍不通是常态。这一章列的是实际调试中出现频率最高、又最容易误判的几个问题,每条按现象、原因、解决三步写。按这个顺序排查,能省掉大量「换了三块板子还是不行」的时间。
5.1 硬件侧:读不到卡与电源跌落
现象一:卡贴着天线也读不到,MFRC522 返回错误或全 0。原因通常是天线没谐振在 13.56 MHz。匹配电容取值不对、可调电容没调、或者天线正下方铺了地平面,能量被吸走。解决办法是先断开天线,用示波器看 TX1/TX2 有没有 13.56 MHz 输出;有输出再接天线,用环探头看天线两端幅度,旋动匹配电容找最大点;确认天线区域下方所有层的地和走线都挖空。另一个常被忽略的点是 13.56 MHz 晶振的负载电容,直接照抄 30 pF 可能偏得厉害,按晶振规格书取值。
现象二:换了新板子,MFRC522 上电就发热,或者通信全是 0xFF。原因是电平不匹配。MFRC522 是 3.3 V 器件,AT89S52 的 I/O 输出接近 5 V,把 5 V 直接加到 MFRC522 的输入脚上,轻则通信异常,重则击穿。解决办法是给 MFRC522 单独用 3.3 V 稳压(如 AMS1117-3.3),单片机送过去的信号线串 1 kΩ 电阻做限流分压,或者用专用电平转换芯片。这是血泪经验,一块芯片几块钱,烧一次就是半天工期。
现象三:平时显示正常,一刷卡 LCD 就花屏或者单片机复位。原因是读卡瞬间天线电流突变,3.3 V 和 5 V 电源同时跌落。解决是在 MFRC522 电源脚旁边就近放 0.1 μF 陶瓷电容加 100 μF 电解电容,天线供电和数字部分走线分开,最后单点汇到电源入口;如果用的是 USB 供电,换一个带载能力更强的适配器。
5.2 软件侧:多卡冲突与扣费异常
现象四:两张卡一起放上去,程序卡死或者读到一串乱码 UID。原因是没走完整的防冲突流程,直接发了选卡命令。Mifare 的规则是必须先 Request 寻卡,再 Anticollision 拿序列号,再 Select 选中,缺一步就会在多卡环境下拿到冲突数据。解决是严格按状态机顺序执行,并且在 Anticollision 返回冲突标志时重试,重试三次仍冲突就放弃本次操作,提示用户只放一张卡。
现象五:扣了钱但余额没变,或者余额变成了一个巨大的数。原因基本是把值块当普通数据块读写,或者在减值之后没有执行传输,也可能把普通数据块误当成钱包块。解决办法是确认目标块已初始化为值块,严格使用「减值 → 传输」两步,写之前先读一次余额做范围校验,发现余额大于预设上限(比如 1000 元)就拒绝交易并报错。另外,B 密钥留空或使用出厂默认密钥是很常见的漏洞,课程设计也该把扇区密钥改成自定义值,虽然挡不住专业攻击,但至少不会被人拿手机一贴就改。
现象六:DS1302 走时一天慢好几秒,或者断电重启后时间归零。前者的原因是 32.768 kHz 晶振走线太长、负载电容不匹配,或者晶振本身精度差;后者多半是备用电池没装、电池座虚焊,或者 CE 使能时序太快导致写不进。解决是把晶振紧贴芯片、两根线尽量短、下面不走信号,负载电容按规格书取 6 pF 左右;上电后读一次时间,判断是否合理(年份在 2000~2099 之间),不合理就重新写入默认时间。
注意:调试期建议把每次刷卡结果通过串口打印出来,包含 UID、认证结果、扣费前后余额、时间。没有这行日志,以上六个现象里至少四个只能靠猜。
6. 验收与答辩:拿什么证明这套刷卡机真的能跑
课程设计最容易吃亏的环节不是做不出来,是说不清楚做出来了。答辩现场如果只演示「卡一贴、响一声、屏上跳数字」,评审很难判断这是真跑通了还是写死的。我的做法是提前准备一张验收表,每一行都对应一个可现场复现的动作,边演示边报数据。
| 验收项 | 测试方法 | 合格标准 |
|---|---|---|
| 读卡距离 | 卡片从远到近缓慢靠近,记录首次响应距离 | 稳定读卡 ≥ 5 cm |
| 单次交易时间 | 从卡片进入到蜂鸣器响,串口打时间戳 | ≤ 300 ms |
| 扣费准确性 | 连续刷 20 次,记录每次前后余额 | 差额每次都等于票价 |
| 余额不足 | 用余额小于票价的卡刷 | 不扣费,双短鸣,屏显提示 |
| 多卡冲突 | 两张卡叠放在天线上 | 只选中一张,不卡死 |
| 掉电恢复 | 扣费瞬间断电再上电 | 余额为扣费前或扣费后,不出现非法值 |
| 时钟精度 | 连续走 24 小时与标准时间对比 | 误差 ≤ 5 s |
| 重复刷卡 | 卡片贴住不放 10 秒 | 只扣一次费 |
表格里最值得在答辩时主动演示的是「掉电恢复」和「余额不足」这两行。前者能证明你理解「减值 + 传输」的原子性,后者能证明异常分支不是空的。很多方案演示只走成功路径,一问「钱不够怎么办」就答不上来,这是最容易被扣分的地方。
串口日志是另一件低成本高回报的事。在交易成功后把记录按固定格式打出来,答辩时把串口助手窗口投到屏幕上,每刷一次卡就多一行,说服力比口头讲强得多。
/* 交易记录上报:一行一条,便于串口助手直接查看和后期解析 */ void Uart_Report(unsigned char *uid, unsigned short fare, unsigned short balance, unsigned char *timestr) { Uart_SendString("UID="); Uart_SendHex(uid, 4); /* 4 字节卡号,十六进制 */ Uart_SendString(" FARE="); Uart_SendNumber(fare); /* 本次扣费金额 */ Uart_SendString(" BAL="); Uart_SendNumber(balance); /* 扣费后余额 */ Uart_SendString(" TIME="); Uart_SendString(timestr); /* 从 DS1302 读到的 yyyy-mm-dd hh:mm:ss */ Uart_SendString("\r\n"); /* 回车换行,串口助手按行显示 */ }输出格式定成「键=值」的扁平结构,好处是不用写解析器就能肉眼核对,后期要导成表格也只需要按空格切分。字段顺序固定下来,答辩现场刷十次卡,评审一眼就能看出余额是递减的、时间是递增的、卡号是同一张。
答辩陈述的节奏我一般这样安排:先用一分钟讲清楚为什么用 Mifare One 加 MFRC522 这套组合,重点落在「值块的加减值由卡片硬件保证,扣费事务天然安全」;再用两分钟讲系统框图和四个模块的职责,让评审看到你是分层设计的,不是把所有代码堆在 main 里;最后五分钟全给演示,按验收表从上到下走一遍,每个动作说一句「现在验证的是哪一项」。整个过程不提「我学了很多」,只报数据和结果。
从那以后我每次焊完板子、烧程序之前,都会先拿万用表量三样东西:3.3 V 是不是真的 3.3 V、5 V 和 3.3 V 之间有没有短路、天线两端有没有对地短路。这三步花不到一分钟,却能挡掉后面百分之八十的返工。希望帮到你。
本文还有配套的精品资源,点击获取