资讯动态

RFID公交车刷卡系统实战:从硬件选型到离线结算

发布时间:2026/10/14 22:18:30 来源:尧图企业网站定制
简介基于射频识别RFID公交车刷卡系统的完整设计文档面向电子信息、嵌入式及相关专业学生和开发者用于解决城市交通拥堵与公交收费管理问题可作为课程设计或电子系统设计参考。资源仅含 1 个 doc 文档压缩包大小 649KB内容为一份完整的电子系统设计说明书包含目录、引言、系统硬件设计、软件设计、总结与参考文献结构清晰完整。硬件部分详细阐述了非接触式 Mifare One 卡选型、AT89S52 单片机 CPU 选择、晶振电路、蜂鸣器提示音模块、LCD1602 显示模块及 DS1302 时钟模块的设计并给出系统总体结构框图软件部分覆盖系统初始化模块、射频卡与读写器通信设计及总流程图帮助读者理解各模块的实现逻辑。文档还总结了系统在物流管理、医疗保健、身份识别等领域的扩展应用价值能帮助读者快速掌握射频识别刷卡系统的完整设计流程。目前已有 4294 人学习下载。1. 一个刷卡动作背后的完整链路RFID公交车刷卡系统的真问题早晚高峰乘客把卡片往读卡器上一贴“滴”一声绿灯亮起整个过程不到 0.3 秒。这个动作背后就是一套典型的“基于RFID公交车刷卡系统”。它真正要解决的不是能不能把卡号读出来而是三个更硬的问题卡里的余额怎么在离线状态下安全扣掉、扣掉的那笔流水怎么可靠记下来、到了场站之后怎么对账。这套方案适合两类人一类是做课设、毕业设计的学生另一类是要给校车、厂区接驳车、园区通勤车做低成本刷卡改造的工程师。拆开看整个系统就三块读卡硬件、扣费流程、离线数据存储。任何一块出问题刷卡体验都会从“0.3 秒”变成“司机和乘客一起干等”。2. 读卡链路选型与最小驱动13.56MHz方案为什么是公交刷卡的首选2.1 频率与协议对比能写余额才是公交刷卡的第一条件做公交刷卡系统第一步不是选单片机而是选射频频率和协议。市面上能买到的 RFID 方案大致分三档125kHz 低频、13.56MHz 高频、2.4GHz 有源。低频卡便宜、穿透力强但绝大多数只读 ID卡片出场后写不了数据或者写入寿命极差根本没法做余额改写。2.4GHz 有源卡要内置电池几年一换公交这种大批量发卡的场景运维成本直接失控。真正能承担“扣费”职责的几乎只有 13.56MHz 配合 ISO/IEC 14443A 协议这条路线。频率典型协议读写能力典型读卡距离单卡成本公交场景适配性125kHzEM4100 等只读为主5~10cm低只适合门禁不适合扣费13.56MHzISO 14443A可读可写、分扇区、密钥认证3~8cm中适合公交/校园一卡通主流选择2.4GHz有源私有协议可读可写10~50m高不适合电池维护、成本高13.56MHz 方案的另一个优势是卡片内部结构清晰Mifare S50 卡片被分成 16 个扇区每个扇区 4 个块块 0 存放 UID每个扇区的块 3 存放密钥。余额可以放在扇区 1 的数据块里用默认密钥或自定义密钥做认证后读写。这个“分扇区 密钥认证”的结构决定了扣费流程可以做到“读余额→扣减→写回→再读校验”而不是像低频卡那样只能看一个 ID。公交场景还有一个天然要求读卡距离要短。3~8cm 的读卡范围正好防止乘客站在门口就被误刷这个距离范围也是 13.56MHz 无源卡的物理特性决定的。2.2 读卡模块与 MCU 的最小接线读卡芯片选型上最常见也最不容易翻车的做法是用 RC522 这类 MFRC522 内核模块配合一颗带硬件 SPI 的低成本 MCU我用得比较多的是 STM32F103 系列。RC522 支持 SPI、I2C、UART 三种接口实际项目里 SPI 用得最多速率可以跑到 2MHz 以上读一张卡的数据绰绰有余。模块厂商已经把天线、匹配网络、晶振都调好了自己画 PCB 时只需要把模块焊接区照抄就行这是新手最容易上手的一条路。RC522 引脚MCU 引脚说明SDA (NSS)PA4SPI 片选低电平有效SCKPA5SPI 时钟空闲默认低MOSIPA7MCU→RC522 数据线MISOPA6RC522→MCU 数据线RSTPB0硬件复位低电平复位IRQ不接可留给中断引脚最小系统可悬空3.3V3.3V必须用 3.3V不能接 5VGNDGND共地接线里最容易犯的错是把 RC522 当成 5V 设备直接接开发板的 5V 引脚。MFRC522 内核的额定供电是 3.3V接 5V 轻则读卡不稳定重则烧芯片。另外SPI 的片选引脚不要随手接在复用功能引脚上如果用 PA4 做 NSS初始化时要把它配成推挽输出而不是复用开漏否则片选拉不低通信会时好时坏。IRQ 引脚在最小系统里可以不接扣费逻辑靠轮询状态寄存器就能实现只有在做低功耗模式时才需要把 IRQ 引出来。2.3 最小驱动初始化、寻卡、读卡三个函数的参数细节RC522 的驱动看起来长核心就三个动作初始化、寻卡、读写块。初始化分硬件复位和软件复位两步硬件复位是把 RST 脚拉低 100 微秒以上再拉高软件复位是往命令寄存器写 0x0F。两遍复位做完芯片才进入正常收发状态。SPI 配置上波特率预分频给 8约 4.5MHz足够用太快反而可能在长线上出现时序毛刺。// spi2 引脚已初始化结束这里补 RC522 的复位与寄存器配置 static void RF_GPIO_Init(void) { GPIO_InitTypeDef io; io.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; io.GPIO_Mode GPIO_Mode_AF_PP; // PA5/PA6/PA7 复用推挽 io.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, io); io.GPIO_Pin GPIO_Pin_0; // PB0 做 RST 复位脚 io.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, io); RC522_RST_HIGH(); } uint8_t RC522_Init(void) { RF_GPIO_Init(); RC522_RST_LOW(); // 硬件复位拉低再拉高低电平保持至少 100us delay_us(100); RC522_RST_HIGH(); delay_ms(10); RC522_WriteReg(CommandReg, 0x0F); // 软件复位命令芯片进入空闲状态 delay_ms(10); RC522_WriteReg(TModeReg, 0x80); // 开启自动定时器基时钟 1ms RC522_WriteReg(TPrescalerReg, 0xA9); // 分频系数配合 TReloadReg 设定超时 RC522_WriteReg(TReloadReg, 0x30); // 看门狗超时约 25ms防止无卡时死循环 RC522_WriteReg(RFCfgReg, 0x70); // 接收器增益 48%工作在主动模式 return RC522_Check(); // 读 VersionReg验证芯片应答 }这段代码里RFCfgReg 0x70的 0x70 是接收器增益值越大灵敏度越高但过高会把环境噪声也放大出现“没人刷卡却读到错误数据”的假卡现象。TReloadReg 0x30是寻卡超时这个值决定发送寻卡请求后等多长时间算失败太短会漏掉反应慢的卡太长会让刷卡响应变迟钝。RC522_Check 内部读 VersionReg 寄存器不同型号返回 0x91 或 0x92读到这两个值之一就说明 SPI 通路和芯片都正常读不到就先查接线而不是查天线。寻卡和读块是扣费流程的前置动作。寻卡命令发出 7 位帧头 0x52卡收到后返回 ATQA 应答接着做防冲突拿到完整 UID最后选卡。读卡块则需要先对扇区做密钥认证认证通过后才能读块内容。uint8_t RC522_Request(uint8_t reqMode, uint8_t *tagType) { uint8_t status; RC522_ClearBit(FIFOLevelReg, 0x80); // 清空 FIFO 缓冲区 RC522_WriteReg(CommandReg, PCD_TRANSCEIVE); // 进入收发命令状态 RC522_WriteReg(BitFramingReg, 0x07); // 发送 7 位Request 命令固定 7 位帧 RC522_WriteReg(FIFODataReg, reqMode); // reqMode0x52 寻所有卡0x26 寻未休眠卡 RC522_SetBit(CommandReg, 0x10); // StartSend 置位开始发送 // 轮询 CommIRQReg 等待发送完成标志 return status; } uint8_t RC522_ReadBlock(uint8_t block, uint8_t *buf) { uint8_t status; // 前置条件已完成 RC522_Anticoll 拿到 UID并 RC522_Auth 认证扇区成功 RC522_WriteReg(CommandReg, PCD_TRANSCEIVE); RC522_WriteReg(BitFramingReg, 0x00); // 完整字节传输不再发 7 位帧 RC522_WriteReg(FIFODataReg, 0x30); // 0x30 是 Mifare 读块命令 RC522_WriteReg(FIFODataReg, block); // 块地址例如 4 表示扇区1的第一数据块 // 发完命令后从 FIFO 读回 16 字节数据末尾是 CRC 校验 return status; }这里有个细节读块的命令帧是 2 字节0x30 块地址后面的数据全部从 FIFO 里取。如果读回来的是 16 字节乱码但 CRC 校验通过大概率是认证步骤没做对如果 CRC 都不过优先检查 SPI 时序和电源纹波。寻卡请求的 7 位帧是个例外BitFramingReg 0x07只对这次请求生效后续读写块都要把它改回 0x00否则所有后续帧都会错位这是新手最容易踩的驱动坑。3. 扣费流程设计余额读写、防重刷窗口与黑匣子排查3.1 一次扣费要拆成八步Mifare S50 的存储结构与状态流刷卡扣费不是“读到卡号就扣钱”而是一条严格的状态流寻卡→防冲突→选卡→密钥认证→读余额→判断足够→扣减写回→读回校验。中间任何一步失败整个状态机都要回到待机状态不能在“认证失败”之后还继续往下走。Mifare S50 的存储结构是16 个扇区每扇区 4 块块 0 是 UID 区出厂锁定每扇区的块 3 是密钥控制块。余额我一般放在扇区 1 的块 4备份放在块 5块 6 留作计数或扩展。密钥认证是扣费流程里最耗时的一步也是最容易出问题的一步。S50 卡出厂默认密钥是 6 个 0xFF厂家发的空白卡直接用默认密钥就能认证。但如果前一个开发者改过卡密钥自己又不记得认证会一直失败。所以项目起步时第一件事是确认卡片扇区 1 的密钥到底是多少拿一张已知卡先做一次认证测试而不是上来就写全流程。3.2 扣费核心函数实现认证、扣款、写回、校验扣费函数要同时处理“余额不足”“认证失败”“写回校验失败”三种异常。这里最容易被忽略的是写回后的读回校验EEPROM 写入正常情况下是成功的但在电压不稳或卡片离开射频场的瞬间写块命令可能对卡生效而对读卡器返回失败这时候不读回校验卡里的钱已经扣了页面上却报“扣费失败”乘客再刷一次就会被扣两笔。uint8_t BusCard_Deduct(uint8_t *uid, uint32_t fare) { uint8_t blockData[16]; uint32_t balance; uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 出厂密钥上线前必须改 if (RC522_Auth(uid, 1, key) ! OK) // 认证扇区 1uid 是防冲突阶段拿到的卡号 return ERR_AUTH_FAIL; if (RC522_ReadBlock(4, blockData) ! OK) return ERR_READ; balance LE32(blockData); // 余额以 4 字节小端存储偏移 0 if (balance fare) return ERR_INSUFFICIENT; // 余额不足直接返回不写卡 balance - fare; pack_LE32(blockData, balance); RC522_WriteBlock(4, blockData); // 写余额 RC522_WriteBlock(5, blockData); // 写备份区给对账留后悔药 if (RC522_ReadBlock(4, blockData) ! OK || LE32(blockData) ! balance) return ERR_VERIFY; // 读回不一致视为扣费异常需要人工介入 return OK; }这段代码遵循“先读旧余额再算新余额最后写回”的顺序缺一不可。fare不要硬编码在函数里做成全局变量或配置项因为公交票价调整是常态改一处就能全量生效。pack_LE32和LE32负责大小端转换S50 按低字节在前存储ARM 小端字节序正好一致但将来换到其他架构时这一步不能省。备份区写块 5 的目的不是双倍保险而是当块 4 数据损坏时对账系统可以拿块 5 的值做交叉验证判断到底哪一次扣费写坏了。3.3 防重复刷4秒窗口与关RF场双保险乘客刷完卡后把卡片贴在刷卡区不动卡片会持续驻留在射频场内。读卡器如果每秒寻卡一次就会对同一张卡执行两次完整扣费流程这就是最常见的“刷一次扣两次”。解决思路是双层防护一层在应用层记录最近一张卡的 UID 和时间戳窗口期内拒绝再次扣费另一层在物理层扣费成功后主动关闭射频场几百毫秒让卡片离开射频场后再恢复寻卡。uint32_t g_lastUid 0; uint32_t g_lastTick 0; uint8_t BusCard_CheckRepeat(uint32_t uid) { if (uid g_lastUid (HAL_GetTick() - g_lastTick) 4000) return REJECT_REPEAT; // 4 秒窗口内同一张卡只认一次 g_lastUid uid; g_lastTick HAL_GetTick(); return ACCEPT; }4 秒窗口是经验值。窗口太短卡片在射频场里滞留几秒就会二次扣费窗口太长乘客从后门下车再到前门重新刷同一辆车短时间往返会被误挡。我见过现场调到 10 秒导致乘客换乘刷不上司机只能手动让行的翻车案例。另外卡号相同只是防重刷的必要条件完整判断最好加上“本地流水号”每张卡扣费成功后把本地 seq 写进卡片计数块再次刷到同一张卡时比对卡片计数块如果计数已经增加说明上一笔已经生效直接拒绝重复扣费。3.4 余额错乱怎么查备份区交叉校验系统上线后最怕对账时发现某张卡余额和后台不一致。这时候先不急着动卡按流程查三遍第一遍调本地流水看这笔扣费有没有记录第二遍读卡上块 4 的当前余额第三遍读块 5 的备份余额。如果块 4 和块 5 不一致取三者能相互印证的数值作为最终值。块 4 和块 5 都损坏的情况很少见但一旦出现卡基本只能作废补发。所以更稳妥的惯例是把块 6 也利用起来存一个“最近一次扣费后的余额 流水 seq”的复合记录每次扣费三块同时更新。检查时只要有两块能对上就能确定正确余额不会出现“扣了钱却说不清扣了多少”的死局。这个设计在我实际做过的校车项目里帮了大忙一次电源故障导致块 4 写花靠块 5 和块 6 的值人工恢复了 30 多张卡。4. 离线数据存储与结算断电不丢流水、场站能对账4.1 为什么离线可结算是公交场景的硬约束公交车是一个移动节点过隧道、高架桥、郊区路段时网络时好时坏把扣费依赖在“实时联网”上等于把收银台交给运气。所以行业里通行的做法是卡内余额在本地扣流水在本地存到了场站再把流水导出。这套机制有三个硬约束卡内余额是权威数据写卡必须可靠本地流水是凭证掉电不能丢认证在本地完成不依赖服务器在线。理解了这三个约束就能理解为什么流水存储这块值得单独花一半的代码量去做。4.2 流水队列数据结构、写指针与上电恢复流水记录至少要包含卡号、时间、金额、序号、状态五要素。序号seq的作用是去重和对账时找“丢的账”如果第 100 条之后的 seq 出现跳变说明中间有流水没存进去是为了追责和排查的关键索引。typedef struct { uint32_t uid; // 卡片 UID4 字节 uint32_t ts; // 刷卡时刻可使用相对上电时间或外接 RTC uint32_t fare; // 本次扣费金额单位分 uint16_t seq; // 本地递增序号每次扣费 1 uint8_t status; // 0正常 1余额不足 2认证失败 3写卡异常 } __packed BusFlowRec;流水写入不能直接“在当前位置覆盖”因为掉电可能写一半。常规做法是环形队列加写指针但指针本身也要考虑掉电——指针写坏了整个队列就废了。我的习惯是写指针用两个字节冗余存储每次更新写两个副本上电时读两个副本一致才算有效。上电恢复逻辑顺序不能乱先初始化 SPI 和 RC522再读 EEPROM 里的写指针然后扫描队列尾部找到最后一个 status 合法的记录继续沿用它的 seq。如果一上来就开射频场可能乘客已经刷卡流水队列还没准备好就会出现“钱扣了账没记”的空洞。4.3 掉电保护PVD中断与大电容的组合掉电瞬间最容易出事的是“流水只写了一半”。解决思路分两步硬件上加掉电检测程序上在掉电中断里抢时间把当前流水写完。STM32F103 内部自带 PVD 可编程电压检测器当供电电压跌到阈值以下会触发一次中断我们借着主电容里存的那点电能完成最后一条流水的写入。void PVD_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line16) ! RESET) { BusCard_EnterSafeMode(); // 禁止新刷卡关闭射频场等待当前流水写完 EXTI_ClearITPendingBit(EXTI_Line16); } }PVD 阈值一般设置在 2.9V 左右3.3V 电源开始跌落时立刻触发。触发后程序不再执行新的扣费流程只专注把已经在内存里的流水记录刷进 EEPROM。主电容容量按写入时间估算EEPROM 写一页几毫秒留出 10~20ms 余量几百微法到两千微法的电解电容足够。电容太小PVD 中断还没来得及写完就彻底没电电容太大断电后系统进入低电压状态但射频场还开着卡片滞留在场里反而造成乱写。我用 470μF 已经覆盖绝大多数场景个别宽压电源加上去后建议实测掉电波形再定容值。4.4 导出与对账一个能跑通的最小Python脚本流水存完之后场站对账环节需要一个导出协议读卡器进入导出模式停止刷卡动作通过串口把流水按帧发到上位机软件。帧格式不需要复杂每行一条固定文本方便用脚本直接解析。Python 脚本做三条校验seq 连续、无重复卡号、总扣费等于票价乘以刷卡次数。#!/usr/bin/env python3 # 从串口日志解析流水做三条校验 # 1) seq 是否连续 2) 同一张卡同一 seq 是否重复 3) 总扣费是否等于票价 * 刷卡次数 import re import sys flows [] for line in sys.stdin: m re.match(rOK uid([0-9A-F]{8}) fare(\d) seq(\d), line.strip()) if m: flows.append((m.group(1), int(m.group(2)), int(m.group(3)))) seqs [f[2] for f in flows] assert len(seqs) len(set(seqs)), 流水号重复存在重复扣费 seqs_sorted sorted(seqs) for i in range(1, len(seqs_sorted)): assert seqs_sorted[i] - seqs_sorted[i - 1] 1, 流水号不连续存在丢流水 total sum(f[1] for f in flows) print(f流水总数: {len(flows)}, 总扣费(分): {total})这个脚本的断言之于生产线上的意义如果 seq 有跳变说明本地存储掉过流水需要立刻去查掉电保护电路如果同一 uid 出现相同 seq说明防重刷逻辑失效如果总扣费对不上优先检查费率配置和发票卡。脚本十分钟能写完但它是整个离线结算体系的验收标准。没有这个脚本上线后对账只能靠人工翻 Excel出了问题也无从追溯。5. 真车环境的翻车记录5条高频踩坑与排查方法5.1 刷卡两次扣两次驻留重触发与防重窗口的配合现象车辆停在站点乘客刷卡后把卡贴在刷卡区不动读卡器在两三秒内连续“滴”了两声后台对账时发现同一张卡短时间被扣两笔。原因卡片持续驻留在射频场内读卡器周期性寻卡每次都成功触发扣费流程。解决把防重窗口从“扣费之后再判断”改成“扣费成功后主动关射频场 500ms”让卡片物理上离开场区同时保留应用层的 4 秒窗口兜底。只改代码不加物理关场遇到极端驻留场景还是会偶发重扣。5.2 读卡距离从5cm缩到1cm天线匹配是门玄学现象模块在开发板上读卡距离稳定 5cm画到自己的 PCB 上之后卡要贴得很近甚至按住了才能读。原因板载天线的尺寸、铜箔间距和模块原厂不一样天线匹配电容没调发射功率反射过大或者天线背面铺了大面积铜皮寄生电容改变了谐振频率。解决先用网络分析仪或示波器看 13.56MHz 载波幅度再用可调电容从 10pF 附近开始扫匹配点。模块原厂天线走线参数比自己瞎改稳定得多能抄就抄不要自己发挥。我在一个模拟项目 X 上吃过这个亏花了两天才发现是天线下方铺铜导致的偏频。5.3 余额不足和扣费成功共用同一种提示音现象余额不足的卡刷上去“滴”一声司机以为扣成功了乘客也没被提醒到后台一查没扣成纠纷一堆。原因代码里成功和失败走的是同一套蜂鸣器提示没有做声音区分。解决给不同返回值配不同提示成功用绿灯常亮 蜂鸣器长响一声余额不足用红灯亮 短促两声认证失败用红灯闪烁 三声急促鸣叫。声光提示是刷卡系统的人机接口做不好再稳定的扣费逻辑也白搭。5.4 掉电瞬间流水丢了一条先写数据后写状态字现象断电后第二天对账发现少了最后一条或两条流水。原因掉电瞬间流水记录写到一半断电数据区里残留了半条脏数据上电时又把它当成有效记录。解决改写入顺序——先把流水数据写进数据区最后再写状态字例如写 0xA5 表示完整有效。上电扫描时只有状态字为 0xA5 的记录才算有效状态字不是 0xA5一律视为上次写入未完成直接丢弃。这个“先写数据后写标记”的顺序和文件系统里“先写数据后写元数据”是一个逻辑是掉电保护里最便宜也最有效的一招。5.5 车辆点火时读卡器死机电源纹波与看门狗现象车辆点火启动或开关门时读卡器偶尔死机屏幕卡住刷卡没反应必须断电重启。原因12V 电瓶在起动机转时电压掉到 8V 甚至更低门泵电磁阀动作时产生尖峰普通 7805 线性电源扛不住这种波动。解决电源芯片换宽压 DCDC7~35V输入端加 TVS 管吸收尖峰输出端加大容量电解电容程序上开独立看门狗IWDG3 秒内不喂狗自动重启。我之前遇到这个问题的根因还不是 DCDC是 7805 输入端并了一个小容量电容瞬间尖峰直接灌进芯片换 DCDC 后彻底消失。6. 交付前值得养成的三个验证习惯串口回放、卡计数器与断电测试6.1 串口回放模拟刷卡不插卡也能测扣费流程。通过串口按真实时序把“寻卡响应→UID→认证响应”发给 MCU让系统以为自己读到了卡。这个方法用来复现状态机边界特别高效连续两次扣费、余额临界值、认证失败重试都能稳定回放。我一般会在固件里留一个调试模式收到特定串口命令就进入回放状态这样不用每测一次都手动贴卡。6.2 卡片计数器验证不丢不重在卡内额外开一个块每次扣费写入一个递增计数器。配合后台的流水 seq 做两两比对刷卡 100 次计数器和流水号能完全对上说明系统不丢账不重扣。这个验证的价值在于把“RFID 刷卡扣费”变成“可审计的账本”任何一次丢账都能具体定位到是写卡、存流水还是导出环节出了问题。6.3 合盖前的例行测试上真车之前我习惯了把至少三件事跑完再合外壳把一张卡固定在刷卡区不动连刷 20 分钟确认防重刷逻辑稳定模拟 10 次上电掉电每次拔电后重新导出流水核对缺口用带汗的手触摸刷卡区确认防水外壳不会引发误触。吃过亏的地方是在一个模拟项目里没做断电测试就装车结果一晚上下来丢了十几条流水全部靠备份区才捡回来。从那以后“先断电测试再谈稳定性”就成了我交付前必过的一关。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑