简介:面向电子信息工程、物联网及嵌入式课程设计的学习者,这份文档围绕RFID公交车刷卡系统展开,回应城市交通拥堵背景下公交一卡通在通行效率与环境质量上的需求。内容从系统总体方案切入,硬件部分涉及Mifare One非接触式IC卡选型、AT89S52单片机、晶振电路、蜂鸣器提示音、LCD1602显示与DS1302时钟模块;软件部分梳理初始化、射频卡与读写器通信及总流程图,并讨论低成本、成熟器件的可靠性与经济性取舍。包内共1个doc文件,约649KB,以文字方案和电路说明为主,适合按章节查阅硬件模块与软件流程。已有4297人学习,可作为课程设计、电子系统设计说明书或公交刷卡终端方案的参考底稿,也便于延伸到物流、身份识别等应用场景。
1. 从一张公交卡说起:RFID 刷卡终端的整体定位
公交车上那声"滴",大概是城市里最没存在感的提示音,但真把它拆开,几十毫秒里跑的是一整套 RFID 刷卡终端动作:寻卡、防冲突、挑一张卡、三次认证、读钱包余额、扣款、写回、LCD 刷新、蜂鸣器响。这份电子系统设计说明书讲的就是这么一台小终端的完整设计——AT89S52 做 MCU,MFRC522 做 13.56MHz 射频前端,Mifare One(S50)卡当电子钱包,外挂 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 位机都嫌富余。真正吃资源的是"稳定"和"便宜",而不是"快"。
| 维度 | AT89S52 | DSP/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; /* 硬复位,低电平有效 */ /* ---- LCD1602:8 位并口,数据走 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)还是数据(1),RW 决定读还是写,本系统只写不读,可以长期拉低省一根控制线的操作麻烦。
注意:P0口在 8051 里是开漏输出,直接驱动 LCD1602 的数据线带不动,必须在 P0 与 5V 之间挂一排 10kΩ 上拉电阻。这一条在原理图上很容易漏,漏了的表现就是屏只亮背光、不显字符。
3. 电路模块逐个过:晶振、蜂鸣器、LCD1602 与 DS1302
3.1 晶振电路:11.0592MHz 与 30pF 负载电容的配合
系统时钟选 11.0592MHz,而不是更"整"的 12MHz,理由很实际:11.0592 能被 9600、19200 这些常用波特率整除,串口通信的误差接近 0;12MHz 下算 9600 波特率会偏 8% 以上,短帧能凑合,长帧就翻车。晶振接在 XTAL1、XTAL2 两个引脚之间,两端各挂一只 30pF 无极性电容到地。
这两只电容叫负载电容,它不是滤波电容,作用是配合晶振标称的负载电容值,把谐振频率拉到标称点上,同时影响起振的幅度和稳定性。常见做法是取值在 15pF 到 33pF 之间,具体看晶振手册给的 CL 值:CL ≈ C1*C2/(C1+C2) + Cs,Cs 是 PCB 杂散电容,一般按 3~5pF 估。取 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建议取 50~100,太短听不清,太长会拖慢主循环。这套系统是裸机轮询结构,DelayMs是阻塞的,所以不要在刷卡主流程里连续调多次 Beep,否则会错过下一张卡的寻卡窗口。如果后面要改成"刷卡成功两声、失败一声长鸣",把Beep拆成BeepOn()/BeepOff()两个非阻塞函数,用定时器计数控制时长,这是二次开发时第一个该动的地方。
3.3 LCD1602 显示:HD44780 兼容带来的便利
LCD1602 是 16×2 的字符型点阵模块,每行 16 个字符,每个字符位由 5×7 或 5×11 点阵构成。它好用的地方在于控制器基本都兼容 HD44780 指令集,也就是说,一套初始化序列可以套用到市面上绝大多数同规格屏上,换供应商不用重写驱动。
初始化流程是固定的:上电延时(>15ms)→ 写 0x38(8 位数据、2 行显示、5×7 点阵)→ 写 0x0C(开显示、关光标)→ 写 0x06(写入后地址自增)→ 写 0x01(清屏,需要额外延时)。这几条顺序不能乱,尤其是清屏指令执行时间长,紧接着写数据会丢。
/* LCD1602 底层写操作:rs=0 写指令,rs=1 写数据 */ 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/1,col: 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.5V~5.5V,掉电后靠纽扣电池维持。它用三线同步串行和 MCU 通信:RST(复位/片选)、SCLK(时钟)、IO(双向数据)。
通信规则是"低位先行":先拉高 RST,然后每个 SCLK 上升沿写一位、下降沿读一位,命令字节的最低位 D0 先发。命令字节的 bit7 固定为 1,bit6 决定读(1)还是写(0),bit5~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 为命令字节(bit7=1, bit6=0 表示写) */ 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 是写保护寄存器,bit7=0 才允许写时间 */ 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 工作在方式 2(8 位自动重装)来产生:
/* 系统初始化:11.0592MHz 晶振,串口 9600-8-N-1 */ void SystemInit(void) { EA = 0; /* 先关总中断,配置期间不被打断 */ TMOD &= 0x0F; /* 清掉定时器1的高4位 */ TMOD |= 0x20; /* 定时器1 方式2:8位自动重装 */ TH1 = 0xFD; /* 11.0592MHz 下 9600 波特率的重装值 */ TL1 = 0xFD; TR1 = 1; /* 启动定时器1,产生波特率 */ SCON = 0x50; /* 串口方式1,8位UART,允许接收 */ PCON |= 0x80; /* SMOD=1,波特率加倍 */ ES = 1; /* 开串口中断 */ RC522_Reset(); /* 硬复位读卡芯片,拉低 RST 再拉高 */ LCD_Init(); /* LCD1602 初始化序列 */ DS1302_AllowWrite(); /* 解除时钟芯片写保护 */ EA = 1; /* 全部就绪,开总中断 */ }参数说明:TH1/TL1 = 0xFD是 11.0592MHz、SMOD=1 时 9600 波特率的标准重装值,误差为 0;如果晶振换 12MHz,这个值要重算,误差也会变大,这就是前面选 11.0592MHz 的实际原因。SCON = 0x50拆开看是 SM0SM1=01(方式 1,8 位异步)、REN=1(允许接收)。初始化的最后一步才是EA = 1,顺序颠倒会导致中断在配置到一半时进来,读到半初始化的状态。
4.2 寻卡与防冲突:从 REQA 到拿到 4 字节 UID
寻卡这一段是整个系统的核心链路,也是协议味最重的部分。Mifare One 卡在进入读写器射频场后,靠感应取电上电复位,然后进入 IDLE 状态等命令。读写器先发 REQA(短帧,0x26)或 WUPA(0x52),场内所有处于 IDLE 的卡同时用自己的 UID 做负载调制回应,这就是"冲突"——多张卡同时说话,读回来的是乱码。
防冲突机制就是解决这个的:读写器发 ANTICOLLISION(0x93 0x20),卡回自己的 UID;多卡冲突时,读写器用位冲突检测逐位筛选,每轮锁定 1 位,8 位一轮,三轮之后剩一张卡被选中,返回完整 UID(4 字节,S50 是单尺寸 UID)。选中之后发 SELECT(0x93 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 = REQA,0x52 = 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传0x26(REQA)时只能唤醒 IDLE 态卡,传0x52(WUPA)能连 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 块(块 0~3),每块 16 字节,每扇区的块 3 是密钥区(Key A 6 字节 + 存取控制 4 字节 + Key B 6 字节)。认证用的是三次互相确认:卡发一个随机数给读写器 → 读写器用扇区密钥加密后连同自己的随机数回给卡 → 卡验证通过后回传加密结果。
认证命令字很明确:0x60用 Key A 认证,0x61用 Key B 认证。认证通过后,对扇区的读写就用加密流通信了;换一个扇区必须重新认证,不能复用。
/* 校验扇区密码,mode: 0x60=KeyA, 0x61=KeyB,blockAddr 为绝对块号 */ 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 的块 0~3 对应 0~3,扇区 1 对应 4~7,以此类推。传参时最容易犯的错是把"扇区内块号"当绝对块号传进去,结果认证的是别的扇区,返回 MI_ERR 但代码不报错,现象就是"能读到卡号但读不出余额"。
钱包块的格式值得单独说。S50 的电子钱包不是一个普通数据块,它有固定结构:字节 0~3 存值(小端),4~7 存值的反码,8~11 再存一遍值,12 存块地址,13 存地址反码,14 存地址,15 存地址反码。只用普通 Read/Write 直接改数字是不行的,必须用加值(0xC1)/减值(0xC0)+ 传输(0xB0)这组指令,卡内部会自动维护反码一致性,这也是 S50 钱包能做离线扣费的可靠性来源。
扣费的标准动作序列是:读钱包块确认余额够 → 减值指令(0xC0 + 块号 + 4 字节金额)→ 卡把结果暂存在内部数据寄存器 → 传输指令(0xB0 + 块号)把寄存器内容写回块。少了传输这一步,减值只发生在缓冲区里,卡一离开射频场就丢了,余额不变——这是"扣费不成功但系统显示扣了"的经典原因。
4.4 主循环总流程:一次刷卡的完整时间线
把上面的原子操作串起来,主循环是一个状态机:轮询寻卡 → 有卡则防冲突拿 UID → 选卡 → 认证钱包扇区(通常用扇区 1 或更高,扇区 0 的块 0 是厂商区只读)→ 读钱包余额 → 判断余额是否够本次票价 → 够则减值 + 传输 + 蜂鸣器短响 + LCD 刷新余额,不够则蜂鸣器长响 + LCD 提示 → 发 HALT 让卡休眠 → 回到轮询。
这条链路上每一步都有超时,任何一步失败都得回到最开始重新寻卡,而不是在原地死等。轮询周期建议控制在 100~200ms,太快了卡还没上电复位完成,太慢了用户体验差。HALT 一定要发,否则同一张卡会一直被读到,出现"刷一次扣两次"的问题。
5. 避坑与排查:这套板子最容易翻车的五处
5.1 卡贴上去完全没反应,程序停在寻卡
现象:烧录正常、LCD 能亮能显示、串口能打印,但卡放上去PICC_Request永远返回无卡,示波器测天线两端看不到 13.56MHz 载波。
原因:三种可能,按概率排序——一是 RC522 的硬复位没做对,芯片还在掉电复位状态;二是 SPI 地址算法错了,读版本号寄存器(VersionReg,0x37)读回来不是 0x91/0x92;三是天线匹配网络的电容参数不对,谐振点偏了。
解决:先单独写一个 10 行的自检程序,只做一件事——读 VersionReg 并打印。这一步能砍掉一半的排查路径。读不到正确版本号就是通信层问题,别去动天线;读得到版本号但寻不到卡,才是射频层问题。射频层先量天线两端有没有 13.56MHz 正弦波,没有就查 RC522 的TxControlReg(0x14)有没有把 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 与卡之间的缓冲区;后者是刷卡后没发 HALT(0x50),卡一直停留在 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_EN=1; LCD_EN=0;连写可能只有几十纳秒,中间加一句短延时最保险。
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,查起来像在黑匣子里摸。从那以后我每次加新外设,都强制先走一遍裸驱动压测,再谈业务。希望帮到你。
本文还有配套的精品资源,点击获取