ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RFID公交卡充值原理与MIFARE 1K数据块读写实战

2026/9/17 5:31:22 拓冰建站 浏览量
RFID公交卡充值原理与MIFARE 1K数据块读写实战 简介射频识别技术是一种无需接触即可读取数据的无线通信技术在智能交通、物流跟踪等领域应用广泛。这款PPT课件聚焦于射频识别技术中的公交卡充值场景适合物联网、电子信息及相关专业的学生用来理解RFID系统的基本构成与典型业务流程。资源共1个文件为PPTX演示文稿压缩包大小2.17MB结构紧凑、重点突出方便教师授课或学习者自学。目前已有92人学习浏览。课件详细说明了公交卡充值操作的完整流程从系统初始化、阅读器开启天线、识别卡片、蜂鸣器提示到查询显示卡号与充值前后余额并最终通过修改存储块1中第1个字节的数据完成金额累加。与此同时还配有清晰的设计步骤和程序流程图可帮助读者快速建立射频识别项目开发的整体思路具有较强的工程参考价值。1. RFID 公交卡充值是假教学、真工程卡内字节才是余额本体我拆过一套 RFID 公交卡充值演示板最初以为刷卡、响蜂鸣、屏幕上余额变化和某个后台数据库扣款一样。直到用逻辑分析仪盯住 MFRC522 与卡片之间的 SPI 交互才意识到这个演示真正教的是 RFID 标签数据的读写过程公交卡余额不是存在服务器里而是以 CPU 能改写的字节形式存放在卡片内部数据块中。本讲主题围绕“RFID公交卡充值.pptx”的流程展开就是把 ISO/IEC 14443-A 通信、MIFARE 存储结构、扇区密钥和读写时序全部压缩进一次充值操作里。预算先用的读卡模块加上 LCD、蜂鸣器不到二十行核心代码就能跑通。对于嵌入式初学者适合作为 SPI 外设实操对于上位机开发人员则是理解 NFC 卡片内存模型的快捷路径。2. 先看懂 MIFARE 数据块才能动手充公交卡RFID 读卡器读到的不是一串浮在空中的“射频识别编码”而是精确到某个块地址上的若干个字节。PPT 流程里写到“存储块1中的第1个字节 blockdata[0] 的数据增加数值”这句话放到 MIFARE Classic 1K 卡上非常具体块block是读写的最小单位一个块 16 字节扇区sector是认证的最小单位一个扇区一般由 4 个块组成。公交卡充值演示用到的高频读卡芯片典型内存布局就是这个模型。如果跳过这一层直接去写代码你只会把 blockAddr 当作一个参数塞进 API等出现“能读不能写”时完全不知道去哪里排查。2.1 MIFARE 1K 的四个块谁只读、谁可写、谁是钥匙串经典 MIFARE 1K 共 16 个扇区每个扇区 4 个块。以扇区 0 为例块 0 是厂商数据区块 1 和块 2 是用户数据区块 3 是扇区尾部。块 0 的前 4 字节是 UIDUID 是卡片在防冲突阶段就已经被读卡器拿走的身份标识厂商出厂时把部分字节写死普通读写器经常无法改写这一块。后面 12 字节包含厂商信息和校验位所以实验中的“充值”绝不会选块 0。块 1 则是本演示的主角16 字节中的第 1 个字节也就是字节索引 0用来存放公交卡余额。块 2 通常闲置如果你要实现双备份这正好是第二位置。块 3 的 16 字节分配是 6 字节 KeyA、4 字节访问控制位、6 字节 KeyB负责该扇区的读写权限。扇区块地址常见作用实验用途0块 0厂商数据区UID 与防冲突数据只读不用于余额0块 1用户数据区本实验余额存放在第 1 个字节0块 2用户数据区可做余额冗余备份0块 3扇区尾部KeyA / 访问控制位 / KeyB认证与权限开关1-15每扇区 4 块结构与扇区 0 类似其他应用或扩展存储访问控制位是“能读不能写”的根源。默认情况下很多空白卡出厂时允许用默认密钥FFFFFFFFFFFF读取块 1但能否写入要看该扇区的访问控制字节怎么配。公交卡充值演示里如果选择旧空白卡或教学卡通常块 1 可读可写如果拿真正的公交卡来试写入操作大概率不成功因为生产环境会在扇区尾部把数据块配置成只读或者使用随机密钥。这不是代码问题是卡片本身的权限模型。2.2 读取块 1 第 1 个字节代码里并不是“余额”字段在 MIFARE 卡上没有字段名、没有结构体程序只能按约定的地址和偏移去解释数据。下面这段是实验中最先出现的关键操作读块 1取出buffer[0]byte blockAddr 1; // 块 1属于扇区 0 byte buffer[18]; // MIFARE_Read 会返回 16 字节数据 CRC byte size sizeof(buffer); MFRC522::StatusCode status rfid.MIFARE_Read(blockAddr, buffer, size); if (status MFRC522::STATUS_OK) { Serial.print(balance); Serial.println(buffer[0], DEC); // 块 1 中的第 1 个字节 }这段代码中的blockAddr 1不是随便选的。块 0 是只读厂商区块 1 才是第一个用户数据块而“第 1 个字节”指的是 16 字节块内偏移 0即buffer[0]。buffer[18]多出的 2 字节用于接收卡片返回的 CRC 校验值实际业务数据只看前 16 个字节。这里要特别区分“块 1”和“第 1 个字节”两个概念块是存储单元字节是块内偏移。把MAX_VALUE设置成模拟的余额上限后单字节能表达的范围是 0 到 255所以 PPT 里“充值 10 元”本质是blockdata[0] 10。2.3 阅读器初始化时到底发生了什么天线使能与射频场建立很多入门代码把PCD_Init()当作一个魔法函数其实它包含了一串关键动作复位 RC522 芯片、加载 FIFO、设置定时器、打开天线、调整射频通道。PPT 里“RFID 阅读器使能及开启电源、天线”这一步对应到寄存器操作就是写TxControlReg把 TX1RF 和 TX2RF 位置 1天线才开始辐射 13.56 MHz 的电磁场。卡片进入感应区后从电磁场获得能量通过负载调制返回数据。如果只是做演示调用库的初始化函数就够了如果你发现读卡距离只有 0.5 厘米就要回头检查天线匹配电路和TxControlReg的配置。硬件连接上MFRC522 模块常用 SPI 接口SDA 接 SS 片选SCK、MOSI、MISO 接对应 SPI 引脚RST 接一个数字引脚。注意模块必须用 3.3 V 供电接 5 V 会直接损伤射频前端。3. 充值全流程落地初始化、识别、读余额、写余额把 PPT 里的流程图翻译成代码最简系统由 MFRC522 读卡模块、LCD、蜂鸣器和主控组成。下面以 Arduino 平台为例逻辑同样可以用在 STM32、ESP32 上。完整步骤是系统初始化、LCD 初始化、阅读器初始化然后循环等待刷卡识别成功蜂鸣并显示 UID读取充值前余额写入新余额再次读取并显示充值后余额。3.1 系统初始化与阅读器上电把外设全部拉起来初始化阶段要完成两件事让主控的 SPI 接口进入工作状态以及让 MFRC522 完成上电和天线开启。同时 LCD 要清屏、打开背光蜂鸣器引脚设为输出。代码如下#include SPI.h #include MFRC522.h #include LiquidCrystal_I2C.h #define RST_PIN 9 #define SS_PIN 10 MFRC522 rfid(SS_PIN, RST_PIN); LiquidCrystal_I2C lcd(0x27, 16, 2); MFRC522::MIFARE_Key key; byte blockAddr 1; byte buzzerPin 7; void setup() { Serial.begin(115200); SPI.begin(); rfid.PCD_Init(); // 内含复位、软电源和天线使能 lcd.init(); lcd.backlight(); pinMode(buzzerPin, OUTPUT); for (byte i 0; i 6; i) { key.keyByte[i] 0xFF; // 默认密钥空卡测试适用 } lcd.print(RFID Recharge); }PCD_Init()不是单纯拉高片选它会通过 SPI 往 RC522 写一连串寄存器最后打开天线发射射频场。这里的key.keyByte是六字节密钥MIFARE Classic 1K 出厂常见默认值是0xFF重复 6 次。使用新卡做实验时没问题但之后如果换用别的卡需要先确认密钥否则后面认证会失败。3.2 刷卡检测、蜂鸣提示与 UID 显示主循环里先判断是否有新卡进入射频场再读取卡片序列号。这两步对应“RFID 阅读器识别公交卡”和“刷卡蜂鸣器响铃提示”void loop() { if (!rfid.PICC_IsNewCardPresent()) return; if (!rfid.PICC_ReadCardSerial()) return; digitalWrite(buzzerPin, HIGH); delay(120); digitalWrite(buzzerPin, LOW); lcd.clear(); lcd.setCursor(0, 0); lcd.print(UID:); for (byte i 0; i rfid.uid.size; i) { lcd.print(rfid.uid.uidByte[i], HEX); } // UID 展示后暂不调用 PICC_HaltA保留卡片为选中状态 recharge(); rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); }PICC_IsNewCardPresent()内部会发送 WUPA/REQA 指令卡片返回 ATQA表示有卡进入场PICC_ReadCardSerial()则执行防冲突和选卡流程把 UID 填进rfid.uid。之所以不在显示完 UID 就调用PICC_HaltA()是因为后续充值还要基于这张被选中的卡进行扇区认证。uid.size通常为 4MIFARE Classic 1K 的 UID 就是 4 字节。蜂鸣提示放在这两个函数之间或之后都行只要确认卡已经被成功选中。3.3 扇区认证与充值前余额读取读写块 1 之前必须先认证扇区 0。认证通过后再调用MIFARE_Read读取一整块数据取buffer[0]作为充值前余额byte buffer[18]; byte size sizeof(buffer); MFRC522::StatusCode status; status rfid.PCD_Authenticate( MFRC522::PICC_CMD_MF_AUTH_KEY_A, blockAddr, key, (rfid.uid) ); if (status ! MFRC522::STATUS_OK) { lcd.print(Auth fail); return; } status rfid.MIFARE_Read(blockAddr, buffer, size); if (status ! MFRC522::STATUS_OK) { lcd.print(Read fail); return; } byte oldBalance buffer[0];PCD_Authenticate的第一个参数指定用 KeyA 还是 KeyB 认证。实验使用 KeyA密钥为默认值。认证的地址是blockAddr1但库会自动定位到块 1 所属的扇区 0并从扇区尾部读取控制位因此你不需要手动换算扇区号。MIFARE_Read成功后将 16 字节拷入buffer其中buffer[0]就是玻项中的blockdata[0]。如果返回值不是STATUS_OK就说明认证失败或访问控制位禁止读取失败后不要再继续充值流程否则后续写入大概率也会失败。3.4 写余额充值不是“set”而是“read-modify-write”充值动作的本质是在旧值基础上增加金额再把新的 16 字节写回块 1。这要求我们在写之前保留原始buffer否则块内其他字节会被意外清成 0。这段代码展示了把buffer[0]增加 10 再回写byte addValue 10; byte newBalance oldBalance addValue; buffer[0] newBalance; status rfid.MIFARE_Write(blockAddr, buffer, 16); if (status ! MFRC522::STATUS_OK) { lcd.print(Write fail); return; } byte check[18]; byte checkSize sizeof(check); status rfid.MIFARE_Read(blockAddr, check, checkSize); if (status MFRC522::STATUS_OK) { lcd.clear(); lcd.print(New:); lcd.print(check[0]); }MIFARE_Write要求传入 16 字节也就是一个完整块不能只写 1 个字节。回读后check[0]应为newBalance。这个回读动作对应 PPT 中“查询 RFID 公交卡充值后余额并显示”也是校验写入是否真的生效的关键。整个过程的状态机可归纳为下表阶段代码动作说明上电PCD_Init()打开天线建立射频场寻卡PICC_IsNewCardPresent()发送 REQA等待卡片应答选卡PICC_ReadCardSerial()防冲突并读取 UID认证PCD_Authenticate()验证扇区密钥获得读写权限读前余额MIFARE_Read()读整块取buffer[0]充写值MIFARE_Write()写回完整块字节 0 增加读后余额MIFARE_Read()回读验证并显示如果check[0]正确充值就算成功。实际项目里充完值后卡片可能再次刷入如果不断电下一次循环PICC_IsNewCardPresent()检测不到同一张卡因为卡已经处于 Halt 状态需要用户把卡移出并重新靠近才能开始下一次充值。这样设计可以避免一张卡贴着模块被反复扣款。4. 调试与写保护读不到卡、写不进块的真实原因RFID 项目调试往往不是算法问题而是射频和权限问题。读不到卡时首先看模块供电和 SPI 接线能读到 UID 却写不进余额时则要从卡片访问控制位和MIFARE_Write返回值入手。4.1 读不到卡的排查顺序从天线到协议如果PICC_IsNewCardPresent()一直返回 false不要先怀疑代码先用逻辑分析仪或示波器观察 MFRC522 的 IRQ 引脚。常见的失败原因有三个一是模块供电不足MFRC522 射频前端需要较大峰值电流用 3.3 V 稳压器供电时若电容过小读卡距离会急剧下降二是 SPI 片选引脚选错SS 引脚不仅要接对还要保证库初始化的SS_PIN和硬件连接一致三是卡片协议不匹配演示板使用的是 ISO/IEC 14443-A而一些门禁卡是 125 kHz 低频卡MFRC522 根本读不到。先用手头的空白 MIFARE 1K 卡排除协议问题再逐步前后移动卡片观察蜂鸣器反应。还有一个容易被忽视的问题是卡被置于 Halt 状态后不重新上电或移出场下一次循环读不到。这是正常的因为 UID 已经返还调试时可以对每张卡调用PICC_HaltA()并打印调试信息避免误以为模块损坏。若读卡距离过近可以检查天线匹配电路上的电容是否虚焊常见做法是并联一个 33 pF 电容补偿。4.2 能读不能写的两类原因密钥错误和访问控制位写入失败时程序里status ! MFRC522::STATUS_OK输出 Write fail但很多人忽略上一行的认证状态。MIFARE 卡在非认证状态直接写块会返回错误所以首先要确认PCD_Authenticate通过。密钥不对时连读取都会失败更别说写入。实验里用默认密钥0xFF可以读一块空白卡但拿到打印过访问位的旧卡读取也许正常写入就不一定了因为扇区尾部的访问控制位可能把块 1 配置为“只读”。访问控制位的表现很直接块 1 的写入权限由扇区尾部的 4 字节控制位决定比如常见的配置是允许读但不允许写。这时候就算密钥正确库返回的也是STATUS_ACCESS_DENIED。我的排查习惯是先用MIFARE_Read读块 3把 4 字节访问控制位打出来对照厂商手册确认当前权限。对于教学演示不要拿真实公交卡做写操作公交卡多启用安全认证和防改写强行充值的结果通常是认证失败或者写保护报错。测试时尽量准备两张空白 MIFARE 1K 卡其中一张专门用来测试写保护后的行为。4.3 同卡重复充值和蜂鸣无响应的状态处理流程图里明确要求“结束本次刷卡动作”但在工程实现中这个动作必须由代码显式完成。PICC_HaltA()会让卡进入 Halt 状态PCD_StopCrypto1()会清掉已经建立的加密通道。若不调用这两部分卡仍停留在 Select 状态下一次刷卡演示可能显示“重复卡”或直接不更新 LCD。蜂鸣器不响原因通常不是引线接反而是pinMode没有执行到位或者蜂鸣器是有源蜂鸣器但使用的电平驱动能力不足。最简单的调试方法是先用Serial.println替代蜂鸣器确认主控程序执行到了响铃分支再去看电路。5. 从演示到可用充值校验、掉电恢复与余额冗余5.1 回读校验不是多余的充值后立即回读一次不仅能让你在 LCD 上看到“充值后余额”还能验证写操作是否落盘。很多演示程序把回读当成可选项但遇到卡块损坏、写保护或者写入半途卡被移走的情况只有回读能发现数据异常。回读时如果发现check[0]不等于newBalance就别更新界面直接提示用户重新放卡。这种“读改写再回读”的闭环才是实际嵌入式系统里可靠写入的标准姿势。5.2 利用块 2 做备份防止掉电写坏卡把余额只放在块 1 的单个字节里存在风险MIFARE_Write写块不是原子的如果在写 16 字节过程中卡片突然脱离射频场块内可能出现部分字节更新、部分字节旧值的情况。工程上可以利用块 2 做冗余备份先写块 2 备份再写块 1 主数据下次读卡时优先比较两块数据若不一致则用块 2 恢复块 1。byte backupBlock 2; status rfid.MIFARE_Write(backupBlock, buffer, 16); if (status MFRC522::STATUS_OK) { status rfid.MIFARE_Write(blockAddr, buffer, 16); } if (status ! MFRC522::STATUS_OK) { // 提示用户再次刷卡避免余额状态不一致 lcd.print(Error, redo); }这里先写备份块再写主块是为了让任何一个时刻至少有一份有效余额。写入完成后回读主块同时读取备份块做交叉核对。如果再配合一个校验字节比如255 - balance一旦主块数据损坏程序可以通过校验字节判断余额可信度。这个技巧在门禁卡、会员卡、校园卡应用里非常常见。如果你接手的是一个没有源码的充值 demo我建议先抓一次 SPI 写块日志确认blockAddr和实际金额字节位置再决定是否沿用这个方案。本文还有配套的精品资源点击获取