1. 项目概述:为什么有人要DIY一个NFC门禁卡复制器
先聊点实际的。NFC门禁卡复制器这个项目,我在不同平台上看到过很多次提问,讨论热度一直不减。原因很简单:生活中需要用到门禁卡的场景太多了——公司大楼、小区单元门、地下车库、健身房、甚至部分酒店的电梯楼层权限,都是一张卡片的事。但问题在于,卡片容易丢、容易忘带,或者你需要在多个地方用卡,但只有一张实体卡。
Arduino配合RC522模块做NFC读写器,是目前门槛最低、资料最全、成本最友好的方案。整套硬件成本控制在20到30元以内,软件部分使用现有的MFRC522库,代码量也不大,哪怕你只是刚接触Arduino的小白,基本也能在一个晚上搞定。这个项目解决的核心问题有三个:一是把实体门禁卡的数据读取出来;二是将数据写入到空白卡或UID可写的卡中;三是在此基础上理解RFID低频/高频通信的基本原理。
需要提前说明的是,我做这个项目纯粹是为了技术学习和研究。门禁卡复制涉及安全与合规问题,不同地区对门禁卡复制的法律法规不同,请务必确保你的操作只用于自己的设备、自己的卡,并且征得管理方的同意。本文的所有内容,出发点都是帮助你理解RFID/NFC的工作原理,而不是提供违法工具。
适合看这篇博文的人包括:刚入门Arduino的电子爱好者、工作中需要批量配置门禁卡的运维人员、对RFID原理感兴趣的软件开发者,以及单纯觉得“读卡写卡”这件事很酷、想自己试试的折腾型玩家。如果你是零基础,也没关系,我会从选型、接线、代码到调试一步步讲清楚,过程中你踩过的坑,大概率我早期也都踩过。
2. 硬件准备与接线
2.1 RC522模块到底是个什么东西
RC522是NXP(恩智浦)推出的一款低功耗、低成本的非接触式读写芯片,工作在13.56MHz频段,支持ISO/IEC 14443A协议。市面上几乎所有常见的M1卡(也就是S50卡)、NTAG系列标签,都兼容这个协议。RC522模块的通信方式有三种:SPI、I2C和UART,实际使用中绝大多数人选择SPI,因为速度最快、稳定,而且Arduino的SPI库非常成熟。
模块正面通常是一块印刷天线(那圈线圈),背面是RC522芯片和外围电路,引脚一般引出8个:SDA、SCK、MOSI、MISO、IRQ、GND、RST、VCC。很多新手容易搞混的是SDA和RST,SDA在SPI模式下是片选信号(SS),不是I2C的数据线;而RST虽然名字和Arduino的复位引脚一样,但这里不是Arduino的复位,是RC522芯片的复位输入,用来控制芯片的使能和复位时序。
关于模块的供电,RC522芯片本身工作电压是2.5V到3.3V,但这不意味着你只能从Arduino的3.3V引脚取电。常见的模块上一般都带了电平转换电路,所以可以直接接5V的VCC。不过这里有个容易踩坑的点:不同厂家的模块电路设计略有差异,有的模块确实标注支持5V供电,但有的比较老的模块或者出厂质量不稳定的,直接接5V可能导致芯片发烫。我个人的习惯是,如果手头方便,直接从Arduino的3.3V供VCC,这样最稳妥,尤其当你用的是Arduino Uno或Nano时,3.3V输出电流足够RC522使用了。
2.2 与Arduino Uno的接线对照表
整张接线表其实非常固定,下面是我惯用的接法,照着接基本不会出错。这里以最常见的Arduino Uno为例,如果你用的是Nano,引脚定义也是完全相同的(Nano和Uno的SPI引脚都在同一位置,但要注意Nano的D11对应MOSI、D12对应MISO、D13对应SCK,D10是SS)。如果你用的是ESP8266、ESP32、STM32等其他板子,就需要查一下对应芯片的SPI引脚定义,不要盲目照搬。
| RC522引脚 | Arduino Uno/Nano引脚 | 说明 |
|---|---|---|
| SDA | D10 | SPI片选信号,低电平有效,也就是SS |
| SCK | D13 | SPI时钟信号 |
| MOSI | D11 | 主机输出,从机输入 |
| MISO | D12 | 从机输出,主机输入 |
| IRQ | 不接 | 中断请求脚,本项目中不用 |
| GND | GND | 共地,必须接 |
| RST | D9 | RC522复位引脚 |
| VCC | 3.3V | 建议从3.3V取电 |
有几个细节值得单独说一下。
IRQ引脚在绝大多数入门教程里都是悬空不接的,这个没问题。如果你以后想做一个“靠近即触发”的感应门而不是轮询读卡,才需要把IRQ接到Arduino的外部中断引脚上,配合中断方式来节省CPU资源。但在这个项目里,我们只需要循环检测有没有卡片靠近,IRQ用不上,悬空即可。
RST引脚接D9,这个是可配置的。在MFRC522库里,构造函数会接收两个参数,第一个是SS引脚,第二个是RST引脚,你完全可以把RST接到其他数字脚,只要代码里对应改一下就行。我之所以默认D9,是因为大家在写代码时习惯用MFRC522 mfrc522(SS_PIN, RST_PIN),而SS_PIN是10、RST_PIN是9这个组合在几乎所有示例代码里都能见到,算是社区事实标准。
VCC这里我再强调一次。虽然很多模块板载了稳压和电平转换,但我建议优先接3.3V。RC522的天线驱动电流不大,实测在3.3V供电下读卡距离大概能到3到5厘米,5V供电下读卡距离也不会显著增加,反而可能因为模块上的LDO发热导致性能不稳定。供电电压的稳定比电压高低更重要,这一点在后面的调试部分还会再提。
2.3 准备一张可写的空白卡
做复制器,至少需要一张空白卡作为“复制目标”。你需要仔细区分两种卡:
第一种是M1标准卡,也就是S50卡,容量1KB,出厂时默认会有官方传输密钥(KeyA和KeyB通常都是FFFFFFFFFFFF)。这种卡绝大多数门禁系统在用。对于普通的M1卡,只要密钥是默认的,你就能直接读取所有扇区数据,也能写入新的数据。
第二种是UID可写卡,也叫UID卡、CUID卡、FUID卡等。普通M1卡的UID(唯一标识符,也就是卡号)在出厂时一次性写入,之后不能修改。而UID卡允许你修改卡号这一块,方便做“一卡复制多卡”之类的门禁卡复制操作。如果你只是想把A卡的数据完整复制到B卡,那么B卡必须是这张UID卡,否则卡号那一段写不进去。
去淘宝搜“UID卡”或者“M1空白卡”,一搜一大把。需要注意买卡的时候看清楚FAQ说明,有些便宜的空卡是S50标准卡,它的UID写不了,只能写扇区数据,这对于本项目的“完整复制”目标是不够的。预算充足的话,CUID卡是很稳的选择——它比UID卡多了一层兼容性处理,理论上可以被更多读卡器识别,不容易出现那种“复制完读卡器反而不认”的情况。
3. 核心原理:M1卡的存储结构到底长什么样
S50卡(M1卡)的内部存储结构,很多人看了很多遍源码依然记不清楚,但一旦你想做门禁卡复制器的读写操作,这部分是绕不开的。
M1卡的总容量是1KB,分为16个扇区(编号0到15),每个扇区由4个块组成(块0到块3),每个块16字节,所以总数是16乘4乘16等于1024字节,正好1KB。其中,每个扇区的最后一个块(块3)叫“尾部块”,存放这个扇区的KeyA、访问控制位、KeyB。KeyA在地址0到5字节,访问控制位在第6到9字节,KeyB在第10到15字节。普通情况下,读卡验证时只要KeyA对了,就能读取这个扇区的块0到块2的数据。而访问控制位的核心功能是决定KeyA和KeyB各自能对这个扇区做什么操作。
扇区0的块0是特殊区域,它被叫作厂商块,存放卡片的UID(4字节)、UID校验位、厂商数据和厂商自定义数据。即使你用的是UID卡,厂商块也不是随意改的,需要相对特殊的写操作,一般的复制流程里我们会单独处理它。这就是为什么M1卡数据复制不能简单地“整体拷贝”,而要分区处理、分块处理。
真正有价值的门禁数据(比如楼层权限、有效日期、工号等),储存在扇区1到15的数据块里,具体哪个扇区放什么,完全取决于门禁厂家的自定义设计。常见的小区门禁系统使用的是扇区1的块0或块1来存储卡号,具体位置没法从一个模块的封装看出,只能通过读卡之后的实际数据去推测,或者查阅对应门禁品牌的文档。
此外,门禁卡并不一定都是M1卡。很多新小区的门禁已经换成了CPU卡(如FM1208、NXP Desfire EV1等),这些卡是高安全性卡,RC522模块通过ISO 14443A能读到的只有UID,无法读取扇区数据,更不可能复制。所以你在操作之前,建议先把卡放在安卓手机的NFC功能下读一下(很多安卓手机自带“触碰付款”里能看到卡类型),或者用RC522读一下,看看返回的卡片类型是MIFARE Classic 1K还是其他型号。如果说这个卡压根不是M1卡,那本方案直接作废。
4. 软件配置与完整代码解析
4.1 安装MFRC522库
代码方面,首先需要一个开源库:MIGUEL GOMEZ编写的MFRC522库,这是目前Arduino社区最常用的RC522驱动库。在Arduino IDE的库管理器里搜索MFRC522,通常第一个就是。安装最新版本即可。
这里有个小坑要提醒一下:另一个常见的库叫RFID,作者是Miguel Balboa,功能类似,但API和MFRC522库不同。很多新手从网上复制一段代码,然后发现自己装的库和代码对不上号,编译报错一头雾水。我个人的建议是直接认准库里头的头文件名字:如果你在代码里看到#include <SPI.h>和#include <MFRC522.h>,那么你装的就是MFRC522库。如果看到#include <RFID.h>,那就要换成RFID库。本文全部使用MFRC522库。
4.2 完整代码:读取卡片UID和扇区数据
#include <SPI.h> #include <MFRC522.h> #define SS_PIN 10 #define RST_PIN 9 MFRC522 mfrc522(SS_PIN, RST_PIN); void setup() { Serial.begin(115200); SPI.begin(); mfrc522.PCD_Init(); Serial.println(F("RC522 Ready, place card near reader...")); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } Serial.print(F("Card UID: ")); dump_uid(mfrc522.uid.uidByte, mfrc522.uid.size); MFRC522::MIFARE_Key key; for (byte i = 0; i < 6; i++) { key.keyByte[i] = 0xFF; } dump_all_sectors(&key); mfrc522.PICC_HaltA(); mfrc522.PCD_StopCrypto1(); } void dump_uid(byte *uid, byte uidSize) { for (byte i = 0; i < uidSize; i++) { if (uid[i] < 0x10) { Serial.print(F("0")); } Serial.print(uid[i], HEX); if (i < uidSize - 1) { Serial.print(F(" ")); } } Serial.println(); } void dump_all_sectors(MFRC522::MIFARE_Key *key) { byte buffer[18]; byte block = 0; byte status; for (byte sector = 0; sector < 16; sector++) { status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, sector * 4, key, &(mfrc522.uid)); if (status != MFRC522::STATUS_OK) { Serial.print(F("Auth failed at sector ")); Serial.println(sector); continue; } for (byte blockOffset = 0; blockOffset < 3; blockOffset++) { block = sector * 4 + blockOffset; byte size = 18; status = mfrc522.MIFARE_Read(block, buffer, &size); if (status == MFRC522::STATUS_OK) { Serial.print(F("Sector ")); Serial.print(sector); Serial.print(F(" Block ")); Serial.print(block); Serial.print(F(": ")); for (byte i = 0; i < 16; i++) { if (buffer[i] < 0x10) { Serial.print(F("0")); } Serial.print(buffer[i], HEX); Serial.print(F(" ")); } Serial.println(); } else { Serial.print(F("Read failed at block ")); Serial.println(block); } } Serial.println(F("---")); } }这段代码分成几个功能块,下面逐个拆解。
首先,setup()里初始化串口和SPI总线,然后通过mfrc522.PCD_Init()初始化RC522模块。如果你在串口监视器里看不到“Ready”提示,基本就是SPI接线有问题或者RC522的RST引脚接触不良。
其次,loop()里最重要的函数是PICC_IsNewCardPresent(),这个函数检测有没有新的卡片靠近天线区域,注意它是“新卡”检测,如果你把卡一直贴着读卡器,它可能只触发一次,因为检测逻辑是捕捉从无到有的变化。常见的坑是卡放上去没反应,你可以把卡拿开再放一次。PICC_ReadCardSerial()用于读取卡片的UID并完成防碰撞处理,之后mfrc522.uid结构体里就有卡片UID数据。
dump_uid()是把UID按十六进制格式化输出。比如一张卡的UID如果是0x04 0x12 0x34 0x56 0x78,就会打印成04 12 34 56 78。这里有个小技巧:字节小于0x10时补一个前导零,否则输出会乱,尤其是UID里包含0A、0B之类的字节时。
dump_all_sectors()是核心读取逻辑。循环变量sector从0到15,对每个扇区,先通过PCD_Authenticate做密钥认证,这里用的密钥是6个0xFF字节,也就是MFare Classic卡出厂默认的KeyA。如果门禁系统没有修改过密钥,这个认证就会成功。认证通过后,再读这个扇区的块0到块2,每个读操作16字节,保存在buffer中打印出来。扇区的块3(尾部块)我没有打印,因为里面是密钥和访问位,属于安全敏感数据,而且对普通复制操作而言,块3的数据在写入时通常也要重新计算,不是简单照抄即可。
4.3 完整代码:写入扇区数据到空白卡
读取之后是写入。写卡代码的骨架和读卡非常相似,核心差异是把MIFARE_Read换成MIFARE_Write。下面这段代码会把data数组中的16个字节写入到指定的块中。
#include <SPI.h> #include <MFRC522.h> #define SS_PIN 10 #define RST_PIN 9 MFRC522 mfrc522(SS_PIN, RST_PIN); void setup() { Serial.begin(115200); SPI.begin(); mfrc522.PCD_Init(); Serial.println(F("RC522 Writer ready.")); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } MFRC522::MIFARE_Key key; for (byte i = 0; i < 6; i++) { key.keyByte[i] = 0xFF; } // 把下面的16字节数据修改成你从读卡步骤中获取到的数据 byte data[16] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }; byte targetBlock = 4; // 举例:扇区1的块0,实际根据门禁系统而定 byte status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, targetBlock, &key, &(mfrc522.uid)); if (status != MFRC522::STATUS_OK) { Serial.print(F("Auth failed: ")); Serial.println(status); } else { status = mfrc522.MIFARE_Write(targetBlock, data, 16); if (status == MFRC522::STATUS_OK) { Serial.println(F("Write OK")); } else { Serial.print(F("Write failed: ")); Serial.println(status); } } mfrc522.PICC_HaltA(); mfrc522.PCD_StopCrypto1(); }这段代码里最需要注意的是,MIFARE_Write要求目标块是数据块(不是尾部块),而且块地址必须和认证时使用的扇区对应。我上面默认写入到扇区1的块0,也就是块地址4,这只是一个示例。现实情况中,你大概率需要将从原卡读出来的所有扇区数据逐一搬到新卡中,所以我建议把写入逻辑写成循环遍历扇区,类似于读卡时的遍历方式,只不过把打印换成写入。
写入区块有一点是非常容易踩坑的:如果你写入的数据块在原卡里存储的数据本身带有CRC校验,那么你把扇区数据写过去了,但对应的校验字节却没有跟着更新,门禁系统读卡时会认为数据无效,表现为“能读到卡号但打不开门”。所以如果你在做完整复制,最稳妥的方式是:除了UID需要单独处理之外,把扇区1到15的每个数据块原样搬过去,同时把尾部块的访问控制位和密钥也按照原卡计算后写进去。不过访问控制位的计算不是简单的16进制拷贝,需要做位运算和权限推导,这部分在技术上相对复杂,KJD的话,如果你只是复制家门门禁卡,很多系统认的就是UID加一两个数据块,不用把所有扇区都搬过去。你可以先只复制UID和数据块,如果门禁不认,再进一步做扇区级的完整镜像。
4.4 关于UID写入的特殊处理
如果你买的是UID卡(支持修改UID的M1卡),你还需要写厂商块。厂商块的块地址是0,里面前4字节是UID,第5字节是UID校验位,第6到第10字节是厂商数据,后面还跟着其他设定。直接用普通的MIFARE_Write写块0是无效的,RC522会拒绝这一操作,因为厂商块有特殊保护机制。
处理思路有几种。第一种是用MFRC522库中专门针对UID写入的私有命令(比如MIFARE_UL Write、PICC_CMD_HLTA等),但MFRC522库本身对厂商块的写入支持不太友好,很多人在这个环节会卡住。第二种是换一个库,使用MFRC522Extended或者直接调用RC522的命令寄存器做底层访问,实现过程较为复杂。第三种是使用UID卡自带的后门指令,市场上很多CUID卡带有出厂后门,比如说特定指令允许你随意改写厂商块,但这依赖卡厂的实现,不一定所有卡都支持。
如果你只做入门实验,我建议先放弃写UID这一步,重点放在扇区数据的读写上。因为很多门禁系统验证的是扇区里的数据(比如工号、日期),不一定是UID;而且M1卡在默认密钥下的扇区数据读写已经能让你跑通整个项目流程。等后面你确实有需求要改UID,再去研究对应卡型的数据手册,方向不至于跑偏。
5. 实操过程:从读取到复制一份可用的门禁卡
5.1 操作流程总览
整个项目的实操流程,我的建议是先跑通“读取”,再跑“写入”,最后才做“完整复制”。很多人一上来就想着把卡复制出来,结果连串口都没调通,无从下手。正确顺序是:
- 接线,上传读取程序,打开串口监视器,确认能打印出卡片的UID。
- 用原卡做一次扇区数据扫描,记录数据内容。
- 用空白卡做一次写入测试,确认能写入任意数据并重新读出来。
- 对照原卡的数据,逐扇区写入到空白卡中。
- 在门禁机或手机NFC上测试复制卡是否正常使用。
接下来我把每个步骤讲细一些。
第一步接线,说实话没什么技术含量,但最容易出问题。Arduino Uno的SPI引脚和RC522的引脚对应关系我已经在表格里列出来了。有一个很容易被忽略的坑:部分RC522模块的引脚丝印是反的,我之前买过一版模块,SDA和SCK印反了,接上去怎么都读不到卡,折腾了半天排查发现是丝印错误。所以如果你严格按照表格接线却读不到卡,先用万用表测一下引脚通断,或者干脆换一块模块试试。
第二步读卡。上传完代码,打开串口监视器,波特率选115200。将原卡靠近天线,串口窗口会打印“Card UID: xx xx xx xx”,紧接着打印各个扇区的内容。如果卡使用了非默认密钥,PCD_Authenticate会返回16进制错误码,扇区会显示Auth failed,这时你就需要知道原卡的密钥,或者放弃读取该扇区。很多门禁卡的密钥不是默认0xFF,这种情况门禁卡的数据读取就非常困难了,除了暴力破解(理论上可以离线破解M1卡,但这属于另一个大话题)之外,没有什么简单办法。
第三步写测试数据。把空白卡放到读卡器上,运行写卡程序,确认串口打印Write OK,然后立刻用读卡程序重新读一下这块空白卡,看看刚写入的16字节是否能在对应块中被正确读出来。这块操作主要验证模块的写功能是否正常,以及空白卡是真正的可写卡。如果你写入后重新读取发现数据是乱的或者读出失败,大概率是卡质量问题,建议换一张不同品牌的卡试试。
第四步执行完整复制。这一步需要你先把原卡所有扇区的块0到块2数据记录下来(用4.2节代码的打印结果),然后修改4.3代码中的data数组,逐块写入到空白卡。我这里建议把原卡的数据整理到一个结构体或二维数组里,然后在循环里批量写入,而不是手工一个个改,这样效率高还不容易出错。写完后,最好再用读卡程序扫描一遍写入结果,和原卡数据逐字节比对,确保没有因为写入中途断电或者接触不良造成数据丢失。
最后一步拿到门禁上测试。把复制卡靠近门禁读卡器,观察门禁的反应。如果门禁里有“滴滴”声或者亮绿灯,说明卡通过了认证;如果完全没反应,可能的原因有很多:UID没写进去、访问控制位不对、扇区密钥不对,甚至门禁系统启用了防复制机制。这部分排查我会在下一节详细说明。
5.2 实操中的记录:一次典型的复制过程演示
为了让你有更直观的感觉,我这里模拟一次实际操作的输出。假设原卡是一张M1卡,UID为04 A2 15 6B 33 80,通过读卡程序得到扇区1的数据如下,这里只列出前几个关键字节:
Sector 1 Block 4: 01 23 45 67 89 AB CD EF 00 00 00 00 00 00 00 00 Sector 1 Block 5: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Sector 1 Block 6: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Sector 2 Block 8: 12 34 56 78 90 12 34 56 78 90 12 34 56 78 90 12从扇区1的块0(块地址4)开头是01 23 45 67这一段来看,这很可能不是卡号,因为卡号就是UID本身。这块的89 AB CD EF有可能是一个日期或自定义编号,具体含义要靠门禁厂家的数据格式才能确定。扇区2的块0(块地址8)数据看起来像是重复的卡号或数据密钥,也在常理之中。
对应的写入步骤中,我会把一个空的UID卡,先不修改UID,然后把扇区1的块0写入01 23 45 67 89 AB CD EF 00 00 00 00 00 00 00 00,扇区2的块0写入12 34 56 78 90 12 34 56 78 90 12 34 56 78 90 12,其他块根据原卡逐一写入。
这样一次典型的复制,如果使用的是默认密钥,整个项目在20分钟内就能从零跑到测试。如果门禁系统只认UID不认其他扇区数据,那么这种情况下你还得想办法把UID复制过去,才能达到完整复制的效果。
6. 常见问题与排查技巧实录
6.1 读不到卡:先别怀疑模块坏了
读不到卡是最常见的现象,新手第一时间都会怀疑模块坏了、卡坏了。实际上,最大的嫌疑是接线,其次是供电。这里列一个典型排查顺序:
- 确认SPI的4根线(SCK、MOSI、MISO、SDA)连线无误。MISO和MOSI接反是高频错误。
- 确认RST接的是D9,并且代码里的
RST_PIN定义是9。如果你把RST接到了8,代码里还写着9,那模块就一直处于复位状态,怎么读都是失败。 - 确认VCC供电。优先接3.3V。如果你接的是5V,可以先用万用表量一下模块3.3V引脚是不是真的有3.3V输出。很多模块自带LDO,如果模块没有LDO,直接接5V会烧芯片。
- 用示波器或逻辑分析仪看SPI总线上有没有数据波形。如果没有示波器,可以在初始化之后打印
mfrc522.PCD_ReadRegister(mfrc522.VersionReg)的返回值。正常情况下返回值是0x92或0x12,如果读到0x00或0xFF,说明SPI通信异常,模块没有被Arduino识别。 - 卡片类型必须是ISO 14443A的M1卡。如果你拿一张电梯里的CPU卡或者银行卡来测试,RC522只能读出UID,后面的扇区读取会失败,但至少UID能打印出来。如果连UID都不出,就说明卡片太近、太远、或被金属外壳屏蔽了。
6.2 读卡距离太短
RC522的有效读卡距离本来就不大,标称5厘米,实际在3厘米左右都很正常。如果你发现距离不足2厘米,可能的原因有:供电不足、天线线圈周围有金属干扰、模块天线设计和卡不匹配。
改善距离的办法有几个:确保模块背面不要直接贴在金属桌面上,最好垫一层塑料片或者悬空安装;检查供电,不要和舵机、继电器这种大电流设备共用电源;给天线区留足空间,不要在模块正上方放置螺丝钉等金属物体。另一个容易被忽视的是,如果你的Arduino通过USB供电,而USB线质量很差、电阻大,电流供应不够也会导致读卡距离缩水。
6.3 写入数据后重新读出来是乱的
这一般是写入数据时发生的问题,或者卡本身有问题。先用读卡程序确认目标块在写前是否全为零。有些卡出厂不是全零,而是带有测试数据,如果你没擦除就直接写入,新数据会覆盖旧数据,理论上不会乱,但有些卡的内部状态机在区块没有正确擦除时会写入失败,返回状态码不是OK。
然后检查你是不是在写尾部块(块3)。尾部块需要特殊处理,如果把密钥和访问控制位写错了,这个扇区会直接废掉,后续认证都过不了。我建议新手先避开块3,只操作块0到块2。
另外,写入速度也有影响。MIFARE_Write写入一个块需要几十毫秒,如果在写入过程中卡片被移走了,就会写入失败或写入部分数据。所以写卡时卡片要放稳,不要动它。
6.4 复制出来的卡在门禁上没反应
这种情况比较多见,逐一分析原因。
第一,原卡不是M1卡,而是CPU卡或Desfire卡。RC522只能读出UID,复制出的卡在门禁系统看来是一张“陌生卡”,自然没反应。你可以用读卡程序看一下PICC_GetType返回的卡片类型,如果显示的是PICC_TYPE_MIFARE_DESFIRE或其他类型,说明这卡根本不适合本方案。
第二,门禁系统认的是UID。如果你没有把UID复制过去,只是复制了扇区数据,门禁不认很正常。解决方式是买UID可写卡,然后研究厂商块的写入方法。
第三,门禁系统有“离线白名单”校验。很多门禁系统在发卡时会往卡内写入一个唯一的随机数或者加密证书,门禁控制器在认证时不仅检查UID和扇区数据,还要校验加密签名。这种复制出来的卡即使数据一模一样,在门禁上也会被拒绝。这是目前门禁防复制的常见手段之一,没有什么简单方案能绕过,我也不建议为了绕过去而采取任何行动——合规使用才是底线。
第四,门禁的频率不匹配。极少数门禁系统使用的是125kHz的低频ID卡(也叫EM4100卡),而不是13.56MHz的M1卡。RC522无论如何都读不了125kHz的卡。如果你的门禁卡是一张很薄的卡,放到手机上检测时手机没有任何NFC响应,那它很可能就是低频ID卡,这种情况你应该去搜索“ID卡复制”而不是“NFC复制”。
6.5 串口打印乱码
串口打印乱码,基本上都是波特率不匹配。确认代码里Serial.begin设置的波特率,和你电脑上串口监视器右下角的波特率选择一致。本文代码统一用的是115200,如果你用的示例代码是9600,那串口监视器也要同步改成9600。乱码之后你什么都看不清,这会让你误以为程序出了问题。
6.6 模块发热严重
RC522模块发热严重,一般有两个原因。一个是VCC接了5V,但板载LDO质量不好,发热大;另一个是SPI速率太高,导致芯片内部耗电上升。解决方法是VCC改接3.3V,同时在SPI.begin后面手动降低SPI时钟频率。可以用SPI.setClockDivider(SPI_CLOCK_DIV16),把SPI频率降到1MHz左右,RC522在这个频率下工作完全够用,而且稳定很多。
7. 一些经验心得与安全建议
做完这个项目之后,我最大的感受是:Arduino加RC522这个组合,很适合作为RFID入门的第一块跳板。它低成本、低门槛、资料多,而且从硬件的角度来看,你接触到的SPI通信、M1卡存储结构、密钥认证机制,这些都是通用知识,换到其他平台一样适用。比如你以后想做一个基于ESP32的NFC配网工具,或者想给树莓派的项目加一个NFC感应模块,思路和代码逻辑基本可以平移过去,只需要调整硬件引脚和库的适配。
还有一点个人体会:MFRC522这个库虽然简单,但它把很多底层细节封装得太好了,反而容易让人忽略真正重要的部分。比如你知道MIFARE_Read该怎么调用,但如果不了解块地址和扇区地址之间的关系,你根本不知道为什么要传4这个参数、为什么扇区1对应块地址4到7。建议你花点时间把M1卡的存储结构读透,这比代码本身有价值得多。
写代码的时候,我建议你习惯性地在读写前后添加日志输出,方便排查问题。在你读写卡的过程中,很多问题并不是逻辑问题,而是模块和卡之间的物理交互问题,日志里多打印状态码和读回的数据,能让你很快定位问题出在哪一步。
最后,安全方面的提醒必须再次强调:NFC/RFID技术本身是中性的,但复制他人门禁卡、未经授权读取他人卡片信息,明显涉及个人隐私和公共安全问题,有可能违反当地法律法规。我在“项目概述”里就说了,这是一个学习项目,请把它用于你自己的卡、自己的门禁,或者在有明确授权的环境中使用。我更建议大家把这个项目当成一个理解RFID协议的切入点,在此基础上探索门禁系统安全性、数据加密防护,从防护者的角度思考问题,而不要越界做违规的事。
另外,这个项目后续还能怎么扩展,我给你几个方向。第一,加入OLED显示屏和键盘,做成一个离线写卡器,不用每步都依赖电脑串口。第二,换用ESP32,加一个Web服务器,通过手机浏览器远程控制读卡写卡。第三,加一个蜂鸣器和LED,做成一个优雅的“滴一声就完成”的流程,而不是每次看串口输出。第四,尝试用树莓派加PN532模块来处理UID厂商块的写入,功能更全,也更能深入理解RFID底层协议。每个方向都有大量的学习空间,祝你折腾得开心,也折腾得安全。