基于Arduino与PN532的NFC门禁系统DIY:从原理到实践
1. 项目缘起:从“刷不开门”到“自己造门”的折腾之路
那天晚上,我抱着一堆资料站在公司楼下,对着那台冰冷的门禁读卡器,怎么刷工牌都没反应。保安大哥走过来,轻描淡写地说:“卡消磁了吧,明天找行政补办。” 那一刻,除了尴尬,一个念头冒了出来:这玩意儿到底是怎么工作的?我能不能自己搞一个?不是为了破解什么,纯粹是出于一个技术人的好奇心和“手痒”——想彻底弄明白从卡片靠近到“滴”一声开门,这背后到底发生了什么。
这就是“模拟门禁-NFC标签注册和刷卡”这个项目的起点。它不是一个教你如何破解小区或公司门禁的教程,而是一个深度还原与学习NFC(近场通信)技术在门禁场景中核心工作流程的动手实验。通过这个项目,你可以用极低的成本(几十块钱),搭建一套属于自己的、完全可控的微型门禁系统。你会亲手完成从空白NFC标签的写入(模拟发卡),到用手机或卡片模拟刷卡,再到验证逻辑的完整闭环。这个过程,能让你透彻理解UID、NDEF数据格式、读写器工作模式、以及最简单的“白名单”验证逻辑,这些正是真实门禁系统的简化版核心。
无论你是物联网爱好者、嵌入式新手,还是对身边技术充满好奇的开发者,这个项目都能带你越过“黑盒”,亲手触摸到无线射频识别(RFID)与NFC技术在实际应用中的脉搏。我们不用复杂的门禁控制器,就用最常见的Arduino(或ESP32)和一个PN532 NFC读写模块,从零开始,把“刷卡开门”这件事,自己实现一遍。
2. 核心装备解析:为什么是PN532+Arduino这个组合?
工欲善其事,必先利其器。在开始接线和写代码之前,我们得搞清楚手头工具的能力边界和选型理由。市面上NFC/RFID模块很多,从廉价的RC522到更专业的PN532,甚至集成度更高的模块,为什么我强烈推荐从PN532开始?
2.1 PN532:不止于“读卡”,更是全功能NFC枢纽
RC522是很多入门项目的首选,便宜,但它主要支持Mifare Classic这类高频(13.56MHz)RFID卡,通信协议层面更底层。而PN532不同,它是一个集成了NFC功能的微控制器,支持三种NFC工作模式:读卡器/写入器模式、卡模拟模式、点对点模式。这意味着:
- 兼容性极广:它不仅支持Mifare Classic,还支持Mifare Ultralight、NTAG系列(就是我们常用的NFC标签)、Felica(日本常用),甚至可以通过底层指令与更多类型卡片通信。这为我们模拟不同类型的门禁卡提供了可能。
- 直接支持NDEF格式:NDEF是NFC数据交换格式的标准。很多手机APP(如NFC Tools)写入的信息就是NDEF格式。PN532有库函数可以直接读写NDEF记录,而不用我们去手动拼接复杂的字节块,大大降低了开发难度。
- 集成度高:芯片内部处理了相当部分的射频通信协议,让我们可以通过简单的串口、I2C或SPI命令与之交互,把精力集中在应用逻辑上。
2.2 Arduino/ESP32:灵活的逻辑控制中心
门禁系统不只是读卡,还要判断“这张卡能不能开我这扇门”。这个判断逻辑(我们称之为“访问控制逻辑”)需要一个小型计算设备来完成。Arduino Uno简单可靠,ESP32则功能更强大且自带Wi-Fi/蓝牙,为未来扩展(如联网验证、日志上传)留足空间。在这个项目中,它们扮演“大脑”角色,负责:
- 驱动并指令PN532模块进行读卡操作。
- 接收PN532读取到的卡片唯一标识符(UID)或NDEF数据。
- 将读取到的信息与内部存储的“已授权列表”进行比对。
- 根据比对结果,控制一个继电器模块(模拟电控锁)的通断。
- (可选)通过串口打印调试信息,或驱动一个显示屏显示状态。
2.3 其他关键配角
- 继电器模块:这是连接数字世界和物理世界的桥梁。Arduino的引脚输出5V/3.3V、几十毫安的小电流,根本无法驱动一把12V的电控锁。继电器就是一个用弱电控制强电的电子开关。当授权通过,Arduino给继电器一个高电平信号,继电器内部触点吸合,相当于接通了电控锁的电源回路。
- NFC标签/卡片:这是我们的“钥匙”。建议准备几种:一张Mifare Classic卡(深蓝色,芯片复杂,可加密)、几张NTAG213或215的贴纸(成本低,存储空间适中,兼容性好)。不同类型的卡,UID的读取方式和数据存储结构略有不同,正好用于对比学习。
- 杜邦线与面包板:用于快速、无焊连接。
注意:安全与法律边界本项目所有操作均在你自己购买、完全拥有的设备上进行,目的是学习技术原理。绝对禁止尝试读取、复制或模拟你无权管理的他人门禁卡、银行卡、身份证等,这不仅是非法的,也可能对相关系统造成干扰。我们学习的目的是创造和加固,而非破坏。
3. 硬件连接与基础环境搭建:让设备“对话”
现在,让我们把硬件组装起来,并让Arduino能够指挥PN532。这里以PN532通过I2C接口与Arduino Uno连接为例,因为I2C接线简单(仅需4根线),且库支持完善。
3.1 物理接线图(接线表)
| Arduino Uno 引脚 | PN532 模块引脚 | 说明 |
|---|---|---|
| 5V | VCC | 供电 |
| GND | GND | 共地 |
| A4 (SDA) | SDA | I2C数据线 |
| A5 (SCL) | SCL | I2C时钟线 |
继电器模块的连接:
| Arduino Uno 引脚 | 继电器模块 | 说明 |
|---|---|---|
| 数字引脚 D8 | IN | 控制信号输入 |
| 5V | VCC | 供电(注意继电器模块电压) |
| GND | GND | 共地 |
电控锁(以12V常闭锁为例)连接:
| 电源适配器 (12V) | 继电器 | 电控锁 |
|---|---|---|
| 正极 (+) | COM端 | 红线 |
| 负极 (-) | 黑线(共地) | |
| 常开端 (NO) | 电控锁另一线 |
接线逻辑:当D8输出高电平,继电器吸合,COM与NO接通,12V电路闭合,电控锁动作(开门)。平时D8低电平,电路断开,锁处于关闭状态。
3.2 软件库安装与基础测试
在Arduino IDE中,我们需要一个关键的库来与PN532对话。最常用的是Adafruit_PN532库。
- 打开Arduino IDE,点击“工具” -> “管理库...”。
- 在搜索框中输入“Adafruit PN532”。
- 找到并安装由Adafruit维护的版本。安装时,通常会提示你一并安装依赖的
Adafruit_BusIO库,点击确认即可。
安装完成后,我们可以用一个最简单的示例代码来测试硬件连接是否正常。打开“文件” -> “示例” -> “Adafruit PN532” -> “readMifareClassic”。这个示例专读Mifare卡,但我们可以用它来测试通信。
在上传代码前,务必检查并修改两处:
- 在代码开头的
#define PN532_IRQ (2)和#define PN532_RESET (3)这两行,如果你的模块没有连接IRQ和RESET引脚(我们只用了I2C),需要将其注释掉,并使用Adafruit_PN532 nfc(PN532_I2C_ADDRESS, &Wire);这个构造函数。示例代码通常有I2C初始化的部分,仔细找找。 - 修改后,将代码上传到Arduino,打开串口监视器(波特率115200)。当你将一张Mifare Classic卡片靠近PN532模块时,你应该能在串口看到打印出的卡片UID(一组4字节或7字节的十六进制数,如
0xDE 0xAD 0xBE 0xEF)。
如果能看到UID,恭喜你,最关键的通信链路已经打通!如果没看到,请依次检查:接线是否牢固、库是否安装正确、模块供电是否充足(有时需要外部供电)、代码中的I2C地址是否正确(默认是0x24)。
4. 核心环节一:NFC标签的“注册”(数据写入)
在真实的门禁系统中,“注册”就是物业将你的卡号录入系统白名单。在我们的模拟系统里,这一步就是向一个空白的NFC标签(或卡片)写入我们系统认可的特定数据。我们将设计两种常见的“钥匙”格式:
4.1 方案A:仅验证UID(卡号)这是最简单、也是最不安全的方案,因为很多卡的UID是出厂固化且可读的。但作为学习原理的第一步,它非常直观。 我们的操作是:读取空白标签的UID,然后将这个UID手工添加到Arduino代码里的授权数组里。实际上,对于UID,我们通常不“写入”,而是“读取并记录”。因为标准Mifare Classic卡的UID是不可更改的。但对于一些UID可写的卡片或NTAG标签,我们可以用PN532将其UID改写为我们指定的值。这里以记录UID为例:
// 在Arduino代码中定义一个授权UID列表 byte authorizedUIDs[][4] = { {0xDE, 0xAD, 0xBE, 0xEF}, // 这是你第一张卡的UID {0x12, 0x34, 0x56, 0x78} // 这是你第二张卡的UID }; int numAuthorized = 2; // 授权卡的数量如何获取UID?就是上面基础测试步骤里,用读卡示例代码从串口监视器里抄下来的那串十六进制数。
4.2 方案B:写入并验证NDEF文本记录更接近一些智能门禁的做法,在卡片里写入特定的文本信息(如员工号、房间号)。安全性稍好,因为需要知道写入的格式和内容。 我们需要用PN532向标签写入一个NDEF格式的文本记录。这里使用Adafruit_PN532库的NDEF相关函数会非常方便。
#include <Adafruit_PN532.h> #include <Wire.h> Adafruit_PN532 nfc(PN532_I2C_ADDRESS, &Wire); void setup() { Serial.begin(115200); nfc.begin(); // ... 初始化PN532 ... // 准备要写入的NDEF消息(一个文本记录) const char* website = "ROOM:101"; // 模拟的房间号信息 // 库函数会帮我们打包成NDEF格式 bool success = nfc.ntag2xx_WriteNDEFURI(0, NTAG_URI_HTTP_WWW, website, strlen(website)); // 参数说明:从块地址0开始写,URI类型为HTTP,写入的内容,内容长度 // 注意:ntag2xx_WriteNDEFURI 主要针对NTAG系列标签,对于Mifare Classic需要其他方法。 if (success) { Serial.println("NDEF URI Record written successfully!"); } else { Serial.println("Write failed!"); } } void loop() {}这段代码会将字符串"ROOM:101"以NDEF URI格式写入到NTAG标签的起始块。写入后,你可以用手机上的“NFC Tools”APP去读取这个标签,应该能看到一个网址链接,内容是http://www.ROOM:101。虽然看起来是网址,但我们只是利用了URI这个标准格式来存储自定义字符串。
实操心得:写入前的关键检查在调用写入函数前,务必先执行
nfc.tagPresent()检查标签是否在位,并获取标签类型。因为针对Mifare Classic和NTAG的写入函数是不同的。盲目写入可能导致标签数据混乱。对于Mifare Classic,写入需要先验证扇区密钥,过程更复杂,建议新手先从NTAG标签玩起。
5. 核心环节二:“刷卡”验证与门锁控制逻辑的实现
“注册”完成后,我们就有了“钥匙”。现在要编写系统的“大脑”程序,实现循环读卡、验证、控制继电器这一套流程。
5.1 主循环逻辑设计整个系统的核心是一个状态机,在loop()函数中不断循环:
- 检测标签:调用
nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, &uid, &uidLength)。这个函数会等待一段时间(可配置),如果有符合ISO14443A标准(即Mifare、NTAG等)的卡片进入射频场,它就会把卡的UID读出来,存到uid数组里,并返回true。 - 决策验证路径:读卡成功后,我们需要决定用哪种方式验证这把“钥匙”。
- 路径一(UID验证):直接将读取到的
uid数组,与代码中预存的authorizedUIDs数组进行逐字节比对。匹配成功则授权通过。 - 路径二(NDEF验证):如果UID验证不通过,或者我们想优先进行NDEF验证,则调用
nfc.ntag2xx_ReadNDEF()或类似的读NDEF函数,尝试从标签中读取NDEF消息。然后解析这个消息,检查其中的文本内容是否等于我们预设的字符串(如"ROOM:101")。
- 路径一(UID验证):直接将读取到的
- 执行动作与反馈:
- 授权通过:在串口打印“Access Granted!”;控制数字引脚D8输出高电平(
digitalWrite(RELAY_PIN, HIGH);),继电器吸合,模拟开门;通常开门后需要保持几秒钟(delay(5000);),然后断开继电器(digitalWrite(RELAY_PIN, LOW);),模拟锁闭。 - 授权拒绝:在串口打印“Access Denied!”;可以控制一个红色LED闪烁,或者让蜂鸣器响一声作为提示。
- 授权通过:在串口打印“Access Granted!”;控制数字引脚D8输出高电平(
- 清理与等待:完成一次操作后,需要一个小延迟(
delay(300);),并确保PN532模块准备好进行下一次读卡,避免连续误触发。
5.2 代码结构示例(融合两种验证)
#include <Adafruit_PN532.h> #include <Wire.h> #define RELAY_PIN 8 Adafruit_PN532 nfc(PN532_I2C_ADDRESS, &Wire); // 授权UID列表 byte authorizedUIDs[][4] = {{0xDE, 0xAD, 0xBE, 0xEF}}; int numAuthorized = 1; // 授权的NDEF文本内容 String authorizedNDEFText = "ROOM:101"; void setup() { Serial.begin(115200); pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW); // 初始确保锁关闭 nfc.begin(); uint32_t versiondata = nfc.getFirmwareVersion(); if (!versiondata) { Serial.println("PN532 not found!"); while (1); } nfc.SAMConfig(); // 配置读卡器模式 Serial.println("System Ready. Waiting for NFC tag..."); } void loop() { uint8_t uid[] = {0, 0, 0, 0, 0, 0, 0}; uint8_t uidLength; // 1. 尝试读取卡片UID bool success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 100); // 超时100ms if (!success) { return; // 没读到卡,继续循环 } Serial.print("Card detected! UID: "); nfc.PrintHex(uid, uidLength); // 2. 验证路径:先检查UID bool isAuthorized = false; for (int i = 0; i < numAuthorized; i++) { if (memcmp(uid, authorizedUIDs[i], 4) == 0) { // 假设都是4字节UID isAuthorized = true; Serial.println("UID Authorized!"); break; } } // 3. 如果UID未授权,尝试NDEF验证(针对NTAG标签) if (!isAuthorized) { Serial.println("UID not in list, trying NDEF read..."); // 注意:这里需要根据标签类型调用不同的读NDEF函数,以下为简化逻辑 // 实际中,应先判断标签类型,再调用 nfc.ntag2xx_ReadNDEF() 等函数 // 假设我们读取到一个字符串 `readText` String readText = readNDEFTextFromTag(); // 这是一个需要你实现的函数 if (readText == authorizedNDEFText) { isAuthorized = true; Serial.println("NDEF Text Authorized!"); } } // 4. 执行动作 if (isAuthorized) { Serial.println("*** ACCESS GRANTED ***"); digitalWrite(RELAY_PIN, HIGH); // 开门 delay(5000); // 开门保持5秒 digitalWrite(RELAY_PIN, LOW); // 关门 } else { Serial.println("*** ACCESS DENIED ***"); // 可以在这里添加声光报警 tone(9, 1000, 500); // 蜂鸣器在引脚9响500ms } delay(1000); // 操作完成后等待一段时间,防止重复触发 }这段代码框架展示了融合两种验证方式的逻辑。你需要根据实际使用的标签类型,完善readNDEFTextFromTag()函数的具体实现。
6. 深度排查与进阶优化:从“能用”到“好用”
项目基本跑通后,你会遇到一些“坑”,解决它们的过程正是水平提升的关键。
6.1 常见问题与排查链路
问题一:读卡不稳定,时灵时不灵。
- 排查思路:
- 供电:这是最常见的原因。Arduino的USB口或板载5V输出可能功率不足,尤其在驱动继电器动作时会产生电压波动。尝试给Arduino和继电器模块分别独立供电,或者使用一个输出电流大于2A的5V电源适配器。
- 天线干扰:PN532模块的天线区域(那圈铜线)不要靠近金属物体或电源线。确保标签平行贴近天线中心区域。
- 代码超时:
readPassiveTargetID的超时时间(示例中的100ms)太短可能错过读卡。太长又会降低响应速度。可以调整到200-300ms试试。 - 标签类型:确认你的代码是针对你使用的标签类型(Mifare Classic 或 NTAG)编写的。用读Mifare的代码去读NTAG,可能无法正确识别。
- 排查思路:
问题二:无法写入NDEF数据到标签。
- 排查链路:
- 标签是否支持?确认你的标签是可写的。Mifare Classic 1K卡,0扇区0块(存放UID和厂商信息)是只读的,其他扇区需要先验证密钥A/B才能写。NTAG213/215/216的存储区基本都是可写的。
- 标签是否已锁?有些标签在出厂或经过某些操作后会被“锁死”,变成只读。尝试用手机APP(如NFC Tools)先读一下,看是否能写入。
- 函数用对了吗?
ntag2xx_WriteNDEFURI只适用于NTAG系列。对于Mifare Classic,你需要先mifareclassic_AuthenticateBlock验证扇区密钥(默认密钥通常是0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF或0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5),然后再用mifareclassic_WriteNDEFURI写入,过程繁琐很多。 - 写保护位:在写入NDEF数据时,有些库函数会自动设置“写保护”,防止数据被篡改。一旦设置,这个标签就无法再写入新的NDEF数据了(但可以读)。这是特性,不是bug。
- 排查链路:
问题三:验证逻辑误判。
- 场景:明明写入的是“ROOM:101”,读出来对比却失败。
- 排查:字符串对比要小心!从标签读出的NDEF数据,经过解析后得到的字符串,末尾可能包含不可见的换行符或结束符。使用
String类的trim()方法去除首尾空白字符后再对比。更好的做法是,将授权字符串定义为字符数组(char array),并使用strcmp()函数进行精确对比。
6.2 进阶优化思路
一个基础系统跑通后,你可以从以下几个方向让它变得更“像”一个真实可用的系统:
- 增加管理功能:通过串口输入命令来动态添加/删除授权UID或NDEF文本,而不用每次都修改代码、重新上传。例如,串口输入
ADD_UID DEADBEEF来添加一张新卡。 - 引入显示屏:接一个OLED屏,实时显示“等待刷卡”、“验证中”、“欢迎,101室”、“拒绝访问”等状态,体验感立刻提升。
- 实现“卡模拟”:这是PN532的另一个强大模式。你可以让PN532模拟成一张卡片,然后用你的手机去读它。这需要更复杂的底层指令控制,但能让你理解双向通信。
- 联网与日志:如果使用ESP32,可以轻松连接Wi-Fi。每次刷卡事件(时间、UID、结果)都可以上传到私有服务器或物联网平台(如ThingsBoard、Home Assistant),实现远程日志查看和权限管理。
- 安全性探讨:我们目前方案的安全性非常弱。真实门禁系统使用Mifare Classic卡时,会启用每个扇区的密钥验证,甚至进行双向认证(3DES/AES)。你可以研究
mifareclassic_AuthenticateBlock函数,尝试使用非默认密钥,体验一下“加密扇区”的读写过程,这会让你对门禁卡的“安全”有更具体的认识。
最后一点个人体会:做完这个项目,你再路过门禁时,眼光都会不一样。你会知道那“滴”一声背后,是射频场在耦合能量,是卡片在反射编码后的UID,是读卡器在解调信号并与后台列表比对。技术的神秘感消失了,取而代之的是一种清晰的掌控感。这种通过亲手搭建来解构日常技术黑盒的过程,正是嵌入式开发和物联网学习中最有魅力的部分。从最简单的UID比对开始,到NDEF数据读写,再到未来可能尝试的加密验证,每一步深入的探索,都在你和技术原理之间架起一座更坚实的桥梁。
