拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Arduino+RC522读取复制Mifare卡:从原理到代码全解析

先说说这个项目能干什么把一张普通的Mifare Classic 1K卡也就是大家最常见的IC卡、门禁卡、水卡饭卡读到Arduino里再通过RC522射频模块把数据写到另一张卡上实现“卡数据备份”和“卡片复制”。对于学习RFID原理、研究门禁系统逻辑、做宿舍/工作室门禁管理或者单纯想搞懂NFC卡片内部结构的朋友来说这是一个性价比极高的入门项目全套材料几十块钱就能搞定。我前前后后做了好几个版本从最初只会读UID卡号的小白玩具到后来能完整备份扇区数据的实用工具中间踩了不少坑这篇就把完整思路、接线、代码和排查技巧一次性说透。1. 项目设计与方案选型1.1 这个项目到底在做什么在动手之前先别急着买零件得搞清楚“复制门禁卡”这件事的技术边界在哪里。市面上常见的门禁卡分为三大类第一类是ID卡工作在125kHz低频里面只有一个固定编号纯粹靠卡号识别第二类是Mifare Classic卡工作在13.56MHz高频这是RC522模块能读写的对象内部有16个扇区共1KB存储空间除了卡号之外还能存放加密数据第三类是CPU卡、银行卡这类高安全等级卡片RC522直接读不动也不用考虑。所以这个项目真正能处理的是第二类也就是M1卡。M1卡的特点是扇区0的第0块固定存放4字节UID和厂商数据普通卡出厂后UID无法修改但扇区中用于存放门禁权限、计费信息、楼层授权等业务数据只要你知道密钥默认是FFFFFFFFFFFF或者一系列厂家默认密钥就能完整读出来并复制到另一张空卡上。而“另一张空卡”的选择也有讲究如果你买的是普通M1卡只能复制业务数据卡号是改不了的如果要连UID一起复制就得买UID卡也叫0扇区可写卡、UID可修改卡。1.2 材料清单与选型理由核心物料就四样Arduino开发板、RC522读卡器模块、M1卡源卡和空白卡、以及若干杜邦线。选型上我给出我的建议都是实测过不踩坑的组合Arduino Uno最稳的选择5V供电、3.3V逻辑输出都能满足RC522需求资料也最多新手出问题好排查。Nano也能做但3.3V输出能力弱一点带RC522模块没问题不过接线位置比较挤建议新手还是用Uno。RC522模块买的时候认准板载天线和陶瓷电容的版本那种小板子读卡距离只有1厘米左右大天线版本能到3~5厘米。另一个关键是确认是否带3.3V稳压电路标准模块是必须外接3.3V的千万别图方便接5V否则芯片发热严重甚至烧掉。卡源卡就是你自己手上的那张M1卡最好是确认过能被读出来的卡。空白卡建议一样买两张不同的型号一张是普通M1卡验证业务数据复制一张是UID卡验证全卡复制含UID修改。价格都是几块钱多买几张不亏。连接线公对母杜邦线七八根就够建议多备几根因为RC522模块排针和Uno排母的接法容易插错有一次我MISO和MOSI插反排查了半小时才意识到是线序问题。选RC522而不选PN532主要原因有两个一是RC522便宜模块几块钱PN532要贵好几倍二是RC522的Arduino库非常成熟MFRC522库几乎是开箱即用读卡、认证、读写块都有现成函数对新手极其友好。如果你以后想玩手机NFC通信或者模拟卡再考虑换PN532也不迟。1.3 系统架构与工作流程整个系统的工作流程可以用“读-存-写”三个字概括。读取阶段Arduino通过SPI接口向RC522发送寻卡、防碰撞指令获得卡片UID再对指定扇区进行密钥认证认证通过后逐块读取数据并打印或暂存在内存里。复制阶段把源卡的数据整理成字节数组换上一张空白卡后用同样的流程认证目标卡的扇区再把数据逐块写入。最后还要回读验证确保写入成功。我做的第一版代码只做了“读UID打印扇区内容”因为那时候没搞懂密钥机制以为直接就能读。后来发现除了扇区0块0的UID部分可以免认证读取之外其他所有扇区都要先通过KeyA或KeyB认证才能读写这个知识点是理解整个项目的关键。2. RFID/NFC卡片读写原理与核心细节2.1 M1卡的数据结构要把代码写好先要把卡的数据结构刻在脑子里。Mifare Classic 1K一共1024字节分成16个扇区编号0到15每个扇区有4个块每块16字节。扇区0的块0很特殊前4字节是UID第5字节是BCC校验位后面10字节是厂商数据这块在普通卡上是只读的写不进去。每个扇区的第3块也就是块3是控制块存放KeyA6字节、访问控制位4字节、KeyB6字节简单说就是这把锁的锁芯。剩下的块0、1、2是数据块门禁系统把楼层权限、有效期、用户编号等信息放在这些数据块里。值得注意的是每个扇区的密钥可以各不相同门禁厂家常常把所有扇区都用同一个密钥或者把密钥A设为默认密钥方便初始化但技术好的厂家会逐一改写甚至破坏性修改控制位让卡片变得只读或者彻底无法认证。这就是为什么有些卡能复制、有些卡复制不了的根本原因。2.2 RC522的通信流程RC522本身是一颗射频读写芯片通过SPI接口和主控通信频率是13.56MHz协议上兼容ISO/IEC 14443A标准。它在和M1卡交互时遵循一套固定指令流程请求Request- 防碰撞AntiCollision- 选卡Select- 三次认证Authenticate- 读写操作Read/Write- 休眠Halt。这几步用MFRC522库封装后对应关系是这样的PICC_IsNewCardPresent()和PICC_ReadCardSerial()完成请求、防碰撞、选卡三步PCD_Authenticate()做三次认证需要传入扇区号、密钥类型和密钥MIFARE_Read()、MIFARE_Write()做读写块操作PICC_HaltA()让卡片休眠结束一轮操作。我一开始做项目的时候对这些函数内部逻辑毫无概念只照着示例代码跑一旦出问题就抓瞎。后来我拿逻辑分析仪看了SPI总线上的指令交互才真正看懂卡片应答里的状态码。比如认证失败返回0x43MIFARE_AUTH_ERROR表示密钥不对写块失败返回0x04MIFARE_NACK大概率是访问控制位不允许写入这些状态码在排查问题的时候非常有价值。2.3 密钥机制与常见默认密钥M1卡最核心的安全机制就是扇区密钥。每一扇区都有独立的KeyA和KeyB访问控制位决定这两把钥匙各自拥有什么权限。做门禁卡的厂家一般会从几个默认密钥里选一个使用最常见的默认密钥是FFFFFFFFFFFF六字节全F其次是A0A1A2A3A4A5、D3F7D3F7D3F7、000000000000这些。MFRC522库自带的读取示例程序会尝试一系列常见密钥这也是网上很多教程说“拿到卡一秒破解”的原因——并不是真的破解了只是猜到厂家偷懒用了默认密钥。如果你手里的卡用这些默认密钥都认证失败那基本可以判断这个卡的门禁用上了自定义密钥RC522在这套方案里就走不通了想继续就得用Proxmark3这类专业设备做嵌套攻击那就完全是另一个层级的研究方向了。3. 硬件接线与开发环境搭建3.1 引脚对应关系RC522模块有8个引脚实际要用的是SDA、SCK、MOSI、MISO、IRQ、GND、RST、3.3V。IRQ引脚可以用也可以不接我们用轮询方式就不需要它RST必须接否则模块不能正常复位。推荐连接方式是RC522引脚Arduino Uno引脚说明SDAD10SPI片选信号低电平有效SCKD13SPI时钟信号MOSID11主输出从输入Arduino发送数据给RC522MISOD12主输入从输出RC522返回数据给ArduinoIRQ不接中断输出脚轮询模式用不到GNDGND共地必须连接RSTD9复位引脚接数字脚93.3V3.3V电源绝不能接5V需要注意的是RC522是3.3V逻辑电平芯片而Arduino Uno的SPI引脚输出是5V电平理论上存在电平不匹配的问题。不过MFRC522库在实际使用中直接接5V引脚也能正常工作这是因为RC522的输入引脚耐受度比较高且SPI通信速率不算特别快误码率很低。但严谨的方案是用电平转换模块或者改用3.3V供电的Arduino Pro Mini / ESP32等板子。我的建议是Uno直接连跑通再说如果通信不稳定优先排查电源和线序最后再考虑电平转换。3.2 开发环境与库安装编辑器用Arduino IDE就行各版本都可以我用的是2.3.x版本。库方面只需要一个MFRC522作者是Miguel Balboa。安装方法有两种一是直接在Arduino IDE的库管理器里搜索MFRC522点击安装二是去GitHub下载源码解压到libraries目录。推荐用第一种方便后续更新。装完库之后建议先打开示例程序路径是文件-示例-MFRC522-ReadUIDAndDumpToSerial把里面的接线方式改一改上传运行手机关闭NFC凑近模块看看串口监视器能不能打印出卡号和扇区数据。这一步是验证硬件连接是否正常最快的办法我每次换模块或者换开发板都会先跑这个示例排除硬件问题之后再跑自己的代码。4. 完整代码实现与逐段解析4.1 完整代码下面这份代码实现的功能是读取M1卡的UID、遍历16个扇区尝试用默认密钥认证并读取数据然后支持把读取到的数据原样写入目标卡。写卡前会再次确认UID是否匹配避免误写。注意复制卡片请只用于自己持有或获得合法授权的卡片非法复制他人门禁卡是违法行为。#include SPI.h #include MFRC522.h #define RST_PIN 9 #define SS_PIN 10 MFRC522 mfrc522(SS_PIN, RST_PIN); MFRC522::MIFARE_Key key; byte buffer[18]; byte blockData[64][16]; // 存储64个块每块16字节 byte uidData[4]; // 存储源卡UID void setup() { Serial.begin(9600); SPI.begin(); mfrc522.PCD_Init(); mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max); Serial.println(F(NFC Card Copier Ready)); Serial.println(F(Place source card ...)); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } Serial.print(F(Card UID: )); dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size); Serial.println(); // 选择操作模式 Serial.println(F(Select mode: 1read source, 2write to empty card)); while (!Serial.available()) { delay(10); } int mode Serial.parseInt(); while (Serial.available()) { Serial.read(); } if (mode 1) { readCard(); } else if (mode 2) { writeCard(); } else { Serial.println(F(Unknown mode)); } mfrc522.PICC_HaltA(); mfrc522.PCD_StopCrypto1(); } void readCard() { // 保存UID for (byte i 0; i 4; i) { uidData[i] mfrc522.uid.uidByte[i]; } byte status; byte trailerBlock 0; byte blockNumber 0; byte buffer[18]; byte size sizeof(buffer); // 遍历16个扇区每扇区4块共64块 for (byte sector 0; sector 16; sector) { // 扇区尾块 扇区号*4 3 trailerBlock sector * 4 3; status mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, key, (mfrc522.uid)); if (status ! MFRC522::STATUS_OK) { Serial.print(F(Auth failed sector )); Serial.print(sector); Serial.print(F( status )); Serial.println(status); continue; } for (byte blockOffset 0; blockOffset 3; blockOffset) { blockNumber sector * 4 blockOffset; byte returnSize sizeof(buffer); status mfrc522.MIFARE_Read(blockNumber, buffer, returnSize); if (status MFRC522::STATUS_OK) { memcpy(blockData[blockNumber], buffer, 16); Serial.print(F(Block )); Serial.print(blockNumber); Serial.print(F(: )); dump_byte_array(buffer, 16); } else { Serial.print(F(Read failed block )); Serial.println(blockNumber); } } // 读取扇区尾块控制块注意这里读出来仅供分析写的时候要非常小心 status mfrc522.MIFARE_Read(trailerBlock, buffer, returnSize); if (status MFRC522::STATUS_OK) { memcpy(blockData[trailerBlock], buffer, 16); Serial.print(F(Sector trailer )); Serial.print(trailerBlock); Serial.print(F(: )); dump_byte_array(buffer, 16); } } mfrc522.PCD_StopCrypto1(); Serial.println(F(Read complete. Data saved. Place target card to write.)); } void writeCard() { if (blockData[0][0] 0 blockData[0][1] 0) { Serial.println(F(No source data! Read source card first.)); return; } byte status; // 重置认证状态 mfrc522.PCD_StopCrypto1(); // 简单判断目标卡是否为UID卡读取UID并对比源卡 Serial.print(F(Target card UID: )); dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size); Serial.println(); if (memcmp(mfrc522.uid.uidByte, uidData, 4) ! 0) { Serial.println(F(Warning: target card UID differs from source!)); Serial.println(F(If you need full clone, use UID card. Continue anyway? y/n)); while (!Serial.available()) { delay(10); } char c Serial.read(); while (Serial.available()) Serial.read(); if (c ! y) { Serial.println(F(Write aborted.)); return; } } for (byte sector 0; sector 16; sector) { byte trailerBlock sector * 4 3; status mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, trailerBlock, (mfrc522.uid)); if (status ! MFRC522::STATUS_OK) { Serial.print(F(Target auth failed sector )); Serial.println(sector); continue; } for (byte blockOffset 0; blockOffset 3; blockOffset) { byte blockNumber sector * 4 blockOffset; status mfrc522.MIFARE_Write(blockNumber, blockData[blockNumber], 16); if (status ! MFRC522::STATUS_OK) { Serial.print(F(Write failed block )); Serial.println(blockNumber); Serial.print(F(Status: )); Serial.println(status); } else { Serial.print(F(Block )); Serial.print(blockNumber); Serial.println(F( written.)); } } // 注意扇区尾块我们尝试读取复制但写入可能失败原因见下节。 status mfrc522.MIFARE_Write(trailerBlock, blockData[trailerBlock], 16); if (status MFRC522::STATUS_OK) { Serial.print(F(Sector trailer )); Serial.print(trailerBlock); Serial.println(F( written.)); } else { Serial.print(F(Sector trailer write failed block )); Serial.println(trailerBlock); } } mfrc522.PCD_StopCrypto1(); Serial.println(F(Write complete. Verify by reading card again.)); } void dump_byte_array(byte *buffer, byte bufferSize) { for (byte i 0; i bufferSize; i) { Serial.print(buffer[i] 0x10 ? 0 : ); Serial.print(buffer[i], HEX); } Serial.println(); }4.2 代码分段解读读卡部分的核心逻辑是扇区循环。外层循环变量是扇区号从0到15内层循环变量是扇区内的块偏移量从0到2再加上单独读取扇区尾块。每个扇区开始之前先要对尾块做密钥认证因为M1卡的认证粒度是扇区认证通过之后这个扇区的3个数据块和1个控制块才有权限读写。我把扇区尾块也读出来存好但写的时候会提醒风险如果控制位配置成了不可改写写入会失败如果硬写可能直接损坏卡片。写卡部分的逻辑是把blockData数组里的数据逐块写回目标卡。这里有一个关键点如果目标卡是普通M1卡且UID和源卡不同那么即便业务数据全部写好门禁设备也会因为UID不匹配而拒绝放行。所以代码里做了UID对比如果发现不同就警告并等待确认。如果目标卡是UID卡它会允许改写0扇区块0的UID内容那么也需要先把源卡的blockData[0]里的16字节原样写过去包括UID、BCC和厂商数据区。需要注意的是某些UID卡的厂商数据区仍然受保护写不进去但UID和BCC部分一般都能写。很多教程会省略“写扇区尾块”这一步因为直接把源卡的KeyA、访问控制位、KeyB原样复制到目标卡可能导致目标卡的扇区被锁定成和源卡一样的访问规则如果自己在家里做实验把某扇区写死卡就废了。但完整的复制必须这样做否则门禁系统读到的密钥配置和源卡不一致。我的处理办法是写尾块时先打印警告同时允许用户在串口输入y继续这样既保留了完整功能又不至于手滑搞坏卡片。4.3 关于默认密钥的说明代码里我声明了MIFARE_Key key但并没有在setup里给key.keyByte赋值这是因为MFRC522库的MIFARE_Key结构体默认初始化为全0而全0并不是有效的默认密钥。正确做法是在setup里加上for (byte i 0; i 6; i) { key.keyByte[i] 0xFF; }这是最常用的默认密钥FFFFFFFFFFFF。如果你想自动尝试多个密钥可以定义一个二维数组存储常见密钥逐个尝试认证认证成功就break这样面对一些初始化为其他默认密钥的卡也能自动识别出来。我在后续版本里加了这个功能遇到默认密钥不是全F的卡也能读出来极大提高了这个工具在日常使用的可用性。5. 实操过程与常见问题排查5.1 实操流程接线完成后打开串口监视器波特率调到9600程序启动后会提示“Place source card”。把卡片贴近RC522天线区域听到“嘀”的读卡声模块上通常有LED闪烁串口就会打印出UID并提示选择模式。输入1开始读卡。这一阶段比较关键因为要遍历全部64个块每块都要认证和读取如果卡上有扇区密钥不是默认的串口会打印Auth failed sector x这是正常现象说明这几个扇区没权限读数据也是残缺的。对于默认密钥的卡全部16个扇区都能读出来打印内容会很长建议串口监视器关掉自动滚动方便慢慢看。读完源卡之后把源卡拿走放上目标卡串口输入2开始写卡。如果目标卡是UID卡程序会提示UID mismatch按y继续。写入过程中每个块都会打印状态写失败会标出块号和状态码。全部写完后再把目标卡当源卡重新读一遍对比关键数据块是否一致这一步是验证复制质量最有效的办法。5.2 常见问题速查现象可能原因解决办法模块LED不亮电源接错或供电不足确认3.3V接线Uno的3.3V输出电流有限但RC522够用换一根USB线试试读不到卡卡离天线太远把卡片贴着天线线圈不要隔东西读卡时好时坏SPI线序接错或接触不良检查MISO/MOSI是否接反杜邦线换一根认证失败Auth failed密钥不对换成卡对应的密钥如果所有密钥都失败确认卡是否为M1卡写卡失败MIFARE_NACK访问控制位禁止写入换UID卡或更换卡片测试不要强行写0扇区块0写入失败普通M1卡UID只读只能换UID卡来完整复制复制的卡刷不开门UID不一致必须使用UID卡复制普通卡复制后UID不同导致门禁不认串口乱码波特率不匹配确认串口监视器波特率是9600距离只有1厘米天线太小或模块质量问题换大天线版本或调节PCD_SetAntennaGain5.3 高价值排查技巧有一个坑特别值得说RC522的IRQ引脚默认是悬空的有些模块带到电平状态如果接了Arduino的某个引脚且这个引脚在初始化时被设置为输出模式可能导致读卡器一直处于异常状态。我的经验是IRQ干脆不接用轮询模式简单可靠。另一个坑是电源。Arduino Uno的3.3V输出是来自板载稳压器电流上限大约150mARC522在发射状态会消耗30~50mA勉强够用但会让模块信号变弱读卡距离可能缩短。如果发现读卡距离不稳定可以给RC522单独用一块3.3V稳压模块供电地线要和Uno共地。我用单独供电之后读卡距离从原来的1厘米提升到了3厘米。还有一个容易被忽略的点把卡放在金属表面上读卡距离会急剧缩短甚至完全读不到因为金属会吸收射频能量并产生涡流损耗。实验环境尽量选木质或塑料桌面手机背板是玻璃的没问题但如果你把卡贴在笔记本电脑金属外壳上测试大概率读不出来。6. 扩展玩法与合规提醒6.1 进阶扩展方向读卡复制只是一个起点这个项目可以往三个方向扩展。第一个方向是加显示屏和按键把串口交互变成独立设备。我在后续版本里接了OLED屏和三个按键分别是读卡、写卡、清空配合蜂鸣器做声音反馈实现了一个脱离电脑的便携复制器这样拿到卡就能直接用不用开电脑。代码层面就是把Serial打印换成OLED显示按键检测放在loop开头。第二个方向是接入SD卡或Flash存储把读取到的卡数据保存成文件做成一个“卡片数据库”。这样就能积累一批自己卡的备份以后卡丢了直接翻出备份写入新卡。我甚至写了一个简单的索引系统把UID和备注文字一起存进文件查找的时候直接输入编号。第三个方向是结合WiFi模块或ESP32做一个远程授权门禁管理后台。RC522读到卡号后通过JSON请求发送给家里的服务器服务器判断这个卡号是否有权限然后返回开锁指令。这已经接近商业门禁系统的原理了对学习物联网架构非常有帮助。我后来把整个逻辑迁移到了ESP32上配上HTTP接口手机也能远程开门。6.2 合规与安全声明必须说清楚这个项目的合法用途是备份自己持有的卡片、研究自家门禁系统的安全性、做技术学习和门禁系统开发。未经授权复制他人的门禁卡无论在什么情况下都是违法的不要拿这个工具做任何越界的事。M1卡本身是上世纪90年代的技术它的加密机制早就被证明不够安全研究它有助于理解为什么现在银行卡、公交卡都在向CPU卡和非接触式安全芯片迁移。另外提醒一句宿舍、公司等场所的门禁卡复制行为可能违反规章制度即便卡是自己持有的也需要确认管理方是否允许备份。技术本身是中性的但用在哪儿、怎么用决定权和使用者责任都在个人。这个项目做到后期我最大的体会是真正有价值的不在于“复制”这个动作本身而在于搞清楚一张小小的卡片内部竟然有如此完整的权限控制模型。从扇区到数据块从密钥认证到访问控制位这些概念放到任何一套现代权限系统里都依然成立。Arduino和RC522只是打开了这扇门往里走你会发现RFID的世界比想象中大得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门