做创客小制作最怕的是什么?不是代码报错,也不是焊锡烫手,而是做完一个东西只能摆在桌上吃灰。今天说的这款RFID控制器,是我前后改了三个版本才算定型的实用级小项目:一块Arduino、一个RC522射频读卡模块、一路继电器输出,通过读取高频IC卡的UID来触发开关动作。基于这套核心逻辑,直接延伸出智能门禁、物流追踪、用电设备控制开关三种最常见的用法。整套物料成本不高,源代码我会在后面的章节完整贴出来,新手按接线图搭一遍就能跑通,老手可以直接替换授权逻辑,把输出端接到自己的执行机构上。
这个项目真正值钱的地方在于,门禁、物流、开关这三种看起来毫不相关的场景,底层走的其实是同一条代码路径。读懂这条路径,你以后遇到任何"刷卡触发动作"的需求,都只是在换末端设备而已。
1. 项目拆解:RFID控制器到底在做什么
1.1 三个应用场景的共同底层逻辑
别看标题里写了门禁、物流、开关三个场景,拆开看,它们用的其实是同一套逻辑:读卡器检测到卡片,拿出卡号,判断卡号有没有权限,有权限就接通输出,没权限就拒绝并提示。我把这条链路称为"识别-校验-执行",RFID控制器所有代码都是围绕这三步写的。
可以打一个生活里的比方:RFID卡就是一张电子车牌,读卡器是路口的识别探头,Arduino是后台管理系统。车经过探头,系统先看清车牌,再查黑名单,符合条件的抬杆放行,不符合的亮灯报警。换成门禁就是刷卡开门,换成物流就是货物经过节点时自动登记,换成开关控制就是刷卡上电。理解了这一层,后面的源代码你就不会觉得是一段段零散的外设操作,而是一个完整的状态机。
这个项目里,Arduino扮演的角色其实很轻,它不需要理解"门禁"或者"物流"这种业务概念,它只关心三件事:卡号是否被读到、卡号是否在白名单里、要不要拉高或拉低某个引脚。业务含义全部由执行机构和外部环境去承载,主控代码可以长时间保持稳定,不需要跟着业务逻辑频繁修改。
1.2 方案选型:为什么用RC522而不是其他读卡方案
市面常见的RFID读卡方案有三种视角。第一种是125kHz低频方案,常见的是EM4100 ID卡模块,这种模块读卡距离能做到十几厘米,价格也便宜,但卡片没有加密能力,UID在出厂时固定,普通制卡机就能复制,用在门禁上安全等级偏低。第二种是13.56MHz高频方案,RC522、PN532都属于这一类,RC522专门针对Mifare卡优化,PN532还能读NFC手机卡、支持更多协议,但价格和API复杂度都上去了。第三种是433MHz、2.4GHz有源标签,适合远距离车辆管理,成本最高,不是自制小项目的第一选择。
RC522在这几条路线里属于折中选项:模块加上几片空白卡成本不高,Arduino生态里的MFRC522库非常成熟,网上资料多,改造成门禁、物流登记、开关控制都有现成案例。它的读卡距离确实短,贴着刷才有稳定效果,但家用门禁、桌面设备开关本来就要求近距操作,这一点反而变成一种防误触的好处。所以我在这个项目里最终选了RC522。
选型时要盯住三个参数:读卡距离、卡复制难度、协议多样性。如果实际场景需要隔空感应,比如刷卡距离要10厘米以上,RC522就不合适了,要么改用低频ID模块,要么使用更大尺寸的天线板。如果项目以后要对接手机NFC,PN532会更省事。搞清楚需求边界再选硬件,比先买模块再想用途要靠谱得多。
1.3 材料清单与预算参考
| 物料 | 推荐规格 | 数量 | 用途 |
|---|---|---|---|
| Arduino主控 | UNO R3或Nano | 1 | 主控逻辑 |
| RFID读卡模块 | RC522,带天线板 | 1 | 读取卡片UID |
| 高频IC卡 | Mifare S50空白卡 | 5-10 | 作为权限凭证 |
| 继电器模块 | 1路5V,低电平触发 | 1 | 开关输出 |
| 蜂鸣器 | 有源3.3V/5V | 1 | 授权提示音 |
| LED | 红、绿各1 | 2 | 状态指示灯 |
| 电磁锁/电插锁 | 12V,含独立电源 | 可选 | 门禁执行 |
| 面包板与杜邦线 | 公对公、母对公 | 若干 | 快速搭建验证 |
不含电磁锁的话,一套基础物料成本大概在一顿快餐的花销范围,含12V电磁锁的话预算会明显上去。先备齐基础套件,把读卡逻辑跑通,再按场景补执行器,这是比较省钱也不容易翻车的顺序。
RC522模块有两个常见版本,一种是带PCB天线的一体板,还有一种引脚式小板。创客用前者就好,后者要自己焊天线,调试麻烦。备好料之后不要急着接线,先把Arduino IDE装上,在库管理器里搜索MFRC522并安装,后面编译才不会报缺库。
2. 硬件接线与电源设计
2.1 RC522与Arduino的SPI接线明细
RC522模块一共有8个引脚,名字分别是SDA、SCK、MOSI、MISO、IRQ、GND、RST、VCC,其中SDA在SPI协议里就是从机选择线SS。接线时SDA接Arduino的数字引脚D10,SCK接D13,MOSI接D11,MISO接D12,RST接D9,GND接GND,VCC接3.3V。IRQ这个引脚走中断功能,用不到就空着。
| RC522引脚 | Arduino引脚 | 说明 |
|---|---|---|
| SDA | D10 | SPI从机选择 |
| SCK | D13 | SPI时钟 |
| MOSI | D11 | 主出从入 |
| MISO | D12 | 主入从出 |
| IRQ | 不接 | 中断响应可选 |
| GND | GND | 共地 |
| RST | D9 | 硬件复位 |
| VCC | 3.3V | 严禁接5V |
我见过不少人第一次接RC522,直接把VCC怼到Arduino的5V上,模块发热严重,甚至有芯片直接烧掉。RC522的工作电压上限不高,虽然模块上很多带稳压电路,但按手册要求老老实实接3.3V最稳妥。把VCC接到3.3V,是新手最容易踩的一个坑,好在这个坑只会烧器件,不会造成其他连带损坏。
2.2 继电器模块与执行器的接法
继电器模块上有几个关键接口:VCC、GND、IN,以及一组输出端子,通常标为COM、NO、NC。VCC接Arduino的5V,GND和Arduino共地,IN接数字引脚D6。输出端子那侧,COM是公共端,NO是常开,NC是常闭。
如果是门禁场景接电磁锁,电磁锁大多需要12V供电,Arduino的5V根本带不动,必须用独立的12V电源。接线思路是:12V电源的正极接继电器模块的COM,12V电源的负极接电磁锁的负极,电磁锁的正极接继电器模块的NO。这样Arduino只控制继电器内部线圈的通断,12V回路是独立的,主控和功率回路互不干扰。
实际使用时要确认继电器模块是低电平触发还是高电平触发。市面上常见的模块默认IN引脚在低电平时继电器吸合,高电平时断开,所以代码里我让引脚默认为HIGH,需要吸合时置为LOW。如果你手头的模块恰好相反,把代码里的默认值HIGH改成LOW,触发值LOW改成HIGH即可。
涉及220V用电设备时,我不建议自己接线,直接购买成品继电器插座更安全。220V在这类自制项目里不是不能做,但大家一定要把安全放最前面,能买成品就不要手工搭高压线,这也是对读者负责。
2.3 电源适配:3.3V与5V的取舍
Arduino UNO的3.3V引脚由板载稳压器提供,电流有限,RC522在读卡瞬间的电流尖峰有可能把3.3V拉低,导致读卡不稳定、死机。快速验证用面包板供电没问题,如果是长时间运行的版本,我惯用的做法是外挂一片低压差线性稳压模块,比如AMS1117-3.3,从5V取电转换成3.3V给RC522单独供电,Arduino板的3.3V只留下给其他传感器用。
继电器模块的线圈电流在几十毫安到一百多毫安之间,直接让Arduino的5V引脚带动问题不大,但如果同时接蜂鸣器、LED、显示模块,5V引脚的负载就会偏大。稳妥的做法是给继电器模块单独供电,只在IN引脚上与Arduino共用信号和地线,让主控只提供控制信号而不是电流。
我把电源规划总结成一句经验:能用外部电源供电的模块,尽量不要从Arduino板上取电,Arduino只负责逻辑,不负责搬运功率。这样能规避大部分死机、复位、读卡失败问题,排查故障的时候也会轻松很多。
2.4 布局走线的3条经验
第一,RC522的天线板不要紧贴金属物体。金属会对13.56MHz射频场形成涡流损耗,读卡距离会肉眼可见地变短。安装时让天线板距离金属表面至少留5厘米,如果必须贴近金属安装,使用抗金属标签会好些。
第二,SPI走线不要太长。D10到D13这四根线尽量控制在20厘米以内,长了以后时钟信号容易失真,表现为有时候读得到、有时候读不到。面包板上飞线几十厘米的排线在低频电路里无所谓,在SPI这种几兆赫兹的通信里就是风险。
第三,所有模块共地。Arduino的GND、RC522的GND、继电器模块的GND、外接电源的GND必须连在一起,否则信号没有参考电平,继电器可能乱跳,串口输出也可能乱码。这一步看起来不起眼,却是多模块项目里最常见的隐形故障来源。
3. 源代码结构与核心逻辑拆解
3.1 读卡与UID提取流程
MFRC522库把底层SPI通信都封装好了,调用PICC_IsNewCardPresent()检测有没有卡片靠近,再调用PICC_ReadCardSerial()读取卡片序列号。读到的序列号是一组字节,通过循环把每个字节转成两位十六进制拼接起来,就得到类似"B3F9A2C1"的字符串。这个字符串就是UID,也就是一张卡的身份证号。
需要说明的是,MFRC522读取的是卡的UID,并不需要知道扇区密码。Mifare S50卡内部有16个扇区,其中一部分扇区可以存储业务数据,但作为控制器我们只关心UID就行,密码相关的操作完全不会涉及。所以大家拿到新卡,什么都不用做,直接贴上去就能读出来。
代码中对UID做大小写转换,统一转为大写,避免后面白名单比对时因为大小写不一致而误判。一块小细节容易被忽略,但就是这种小处决定程序稳不稳,省掉的是那种你排查半天才发现只是字母大小写问题的尴尬时间。
3.2 白名单授权与触发输出
白名单我用String数组来存,数组里每一项是一张授权卡的UID。isAuthorized()函数遍历整个数组,一旦找到匹配项就返回true。这种做法代码直观,适合三五张卡的小规模门禁。如果卡数量变多,比如几十张,数组手动维护就很痛苦,届时可以改EEPROM存储,或者用SD卡存一个名单文件,启动时读入内存。
触发输出部分,思路是授权成功后把继电器引脚拉低吸合,再延时3秒后断开,这种模式适合门禁的磁力锁:刷卡开锁,人推门进去,锁自动吸合。如果做的是开关类设备,延时3秒可能不合理,你可以直接把accessGranted()里的delay去掉,改成继电器状态翻转,刷卡一次开、再刷一次关。
这里留给读者一个扩展点,不把代码框死。控制类的场景,核心就是把延时闭合再断开改成翻转状态并保持。我在后面4.2节会给出对应的修改建议。
3.3 完整源代码(可直接烧录)
以下代码基于Arduino IDE和MFRC522库,库文件可以在库管理器里搜索MFRC522安装。代码结构分成setup、loop、isAuthorized、accessGranted、accessDenied五个部分,逻辑都在前面解释过了,烧录后打开串口监视器,把波特率设为9600,就能看到每一次刷卡的UID和授权结果。
#include <SPI.h> #include <MFRC522.h> #define RST_PIN 9 #define SS_PIN 10 MFRC522 rfid(SS_PIN, RST_PIN); #define RELAY_PIN 6 #define BUZZER_PIN 5 #define LED_GREEN 7 #define LED_RED 8 // 授权卡UID白名单,读取新卡后把串口显示的卡号填到下面 String authorizedUID[] = {"A1B2C3D4", "E5F6A7B8"}; void setup() { Serial.begin(9600); SPI.begin(); rfid.PCD_Init(); pinMode(RELAY_PIN, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); pinMode(LED_GREEN, OUTPUT); pinMode(LED_RED, OUTPUT); // 默认断开继电器。如果继电器是高电平触发,这里改为 LOW digitalWrite(RELAY_PIN, HIGH); } void loop() { // 1. 检测是否检测到卡片 if (!rfid.PICC_IsNewCardPresent()) { return; } // 2. 读取卡数据 if (!rfid.PICC_ReadCardSerial()) { return; } // 3. 转成十六进制字符串 String uid = ""; for (byte i = 0; i < rfid.uid.size; i++) { if (rfid.uid.uidByte[i] < 0x10) { uid += "0"; } uid += String(rfid.uid.uidByte[i], HEX); } uid.toUpperCase(); Serial.println("UID: " + uid); // 4. 白名单比对 if (isAuthorized(uid)) { accessGranted(); } else { accessDenied(); } // 5. 结束本次读卡 rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); } bool isAuthorized(String uid) { for (int i = 0; i < 2; i++) { if (authorizedUID[i] == uid) { return true; } } return false; } void accessGranted() { Serial.println("ACCESS GRANTED"); digitalWrite(LED_GREEN, HIGH); digitalWrite(RELAY_PIN, LOW); // 低电平触发继电器 tone(BUZZER_PIN, 1000, 200); delay(200); tone(BUZZER_PIN, 1500, 200); delay(3000); // 保持继电器吸合3秒 digitalWrite(RELAY_PIN, HIGH); digitalWrite(LED_GREEN, LOW); } void accessDenied() { Serial.println("ACCESS DENIED"); digitalWrite(LED_RED, HIGH); tone(BUZZER_PIN, 300, 600); delay(1000); digitalWrite(LED_RED, LOW); }代码里有两个占位卡号A1B2C3D4和E5F6A7B8,实际操作中你先烧录一版,读取自己卡片的UID,然后替换这两个占位值再上传一次。这个过程大概两分钟,却是最容易搞混的一步:上传完第二次后,掉电再上电,白名单就已经生效了。
3.4 扩展:串口学习模式与EEPROM存储
基础版的授权卡是写死在代码里的,每次换卡都要改代码重烧,对动手能力强的朋友无所谓,但对实际部署来说太麻烦。我改到第二版时加了一个串口学习功能:在串口监视器输入字母A,程序进入学习状态,下一张读到的卡片如果还没在白名单里,就自动把它的UID追加到白名单,并保存到EEPROM。
实现思路不算复杂:在loop开头检查Serial.available(),收到字符A就置一个learnMode标志位,在读到卡的分支里判断这个标志,如果为true就把uid写入EEPROM的下一块地址。启动setup时从EEPROM读取已保存的卡号数量,逐个填充到白名单数组。这里用到的核心库是EEPROM.h,Arduino自带的,不需要额外安装。
考虑到文章篇幅,完整的学习模式代码我这里不展开贴,但流程大家就能自己补:启动读数量,循环读卡号,新增时数量加一写回。EEPROM的写入寿命是十万次量级,门禁系统每天写不了几次,完全够用。
4. 三种场景的实操改造方案
4.1 智能门禁:加上电磁锁与备用密码
门禁的核心执行器是磁力锁或者电插锁。磁力锁断电即开门,适合办公区常开状态;电插锁断电即上锁,适合需要常闭的房间。选哪种,取决于你希望停电时门是开着还是锁着,这是一个很容易被忽略的选型问题,买错了再换很麻烦。
接线方面按2.2节的方式,把继电器NO端接到锁的正极。代码层面需要改动的地方很少,白名单和延时逻辑完全复用。我实际做的那版门禁还加了两个东西:一个备用机械密码键盘,在串口或按键板输入错误三次时锁死一分钟,防止暴力刷卡;一个出门按钮,按下后直接触发开锁,逻辑相当于把刷卡成功分支复制一份。
安装时有一点特别提醒:磁力锁在通电瞬间有较大的电流冲击,如果12V电源选得偏小,锁吸合无力,刷卡时咔哒声听着就软。电源功率按锁体标称电流的1.5倍以上配,这是让锁长期稳定工作的最简单手段。
还有一个小经验:把蜂鸣器声音做区分,授权成功响两短声,失败响一长声。这套反馈在光线不好的环境下比LED灯更明显,别人看你刷卡时的听声就能判断门禁状态,体验提升很明显。对经常拎着东西进门、腾不出手看灯的人尤其友好。
4.2 用电设备开关控制:继电器负载选择与安全防护
当RFID控制器用来控制工位灯、风扇、水泵这些用电设备时,继电器模块是关键。常见1路5V继电器模块标称参数是10A 250VAC和10A 30VDC,但标称值不等于可以长期满负荷跑,我习惯把实际负载控制在其额定值的二分之一以下。比如标称10A的继电器,长期带负载不超过5A,这样触点寿命和发热表现都更稳。
负载类型也要区分。阻性负载比如灯泡、加热器,电流波形是正弦的,继电器相对轻松;感性负载比如电机、变压器,断开瞬间会产生反电动势,触点之间容易拉弧,对继电器寿命影响很大。控制感性负载时,最好在负载两端并联RC吸收电路或压敏电阻,这个电路做起来不复杂,但能显著减少触点烧蚀。
如果是纯DC低压设备,比如12V的LED灯带、小水泵,其实不一定要用继电器。继电器有机械寿命,通断时有咔嗒声,换成MOS管模块或者直接用逻辑电平驱动大功率MOS管,开关速度更快、无火花、无噪音。RFID控制器在这里只负责提供一个通断信号,输出级完全可以按负载类型灵活替换。
安全这条我依然放在最前面:但凡涉及220V交流,不要自己搭开放式接线,请直接购买成品继电器插座或带外壳的控制器套件,把高压部分封装在绝缘壳里。创客做的是逻辑控制,不是把家里的配电箱重新发明一遍,用电安全没有讨价还价的余地。
4.3 物流追踪:刷卡时间戳与台账记录
物流追踪的精髓不在识别卡,而在记录事件。货物箱上贴一张RFID卡,经过某个站点的读卡器时,控制器把UID和当前时间记录下来,后台把这些事件串起来,就能还原一箱货物从入库到出库的全路径。
Arduino本身没有时钟,要记录时间需要外接一颗实时时钟模块,常见为DS3231或DS1302。实时时钟模块用纽扣电池保持走时,掉电不丢时间,这样每条刷卡记录的什么时候读到才有参考意义。在这基础上再接一张SD卡模块,每次读卡就把CSV格式的一行追加写入,格式大概是时间、卡号、站点ID三列,后面用电脑打开就能直接统计。
我接触过的一个仓库改造案例,就是在大门和货架区各放一套这样的读卡器,货物进出时自动打卡,月底导出一份台账。RC522在仓储环境里最大的敌人是金属货架和电机干扰,读卡器要避开大块金属面安装,必要时候使用抗金属标签。如果场地太大、读卡节点太多,就要考虑把数据通过串口转发给上位机,而不是在Arduino本地存卡号。
其实把SD卡换成串口Wi-Fi模块,数据就能实时上报到后台,项目就从一个单机打卡机变成一个物联网节点。扩展的方向很多,但先聚焦在离线的识别、校验、记录这层,把基础链路跑稳了再谈联网。
5. 常见问题与排查技巧实录
5.1 读卡距离过短怎么办
RC522的天线做在PCB板上,实际稳定读卡距离通常在2到4厘米,贴着刷是最可靠的。如果你发现必须贴得很死才能读出来,先检查供电:3.3V电压是不是被拉低了?把读卡模块单独接到外部3.3V稳压管上再试。其次看天线板周围有没有金属物体,金属影响射频场会让距离进一步缩短。
MFRC522库还提供了调整天线增益的函数,PCD_SetAntennaGain()可以试着把发射功率调高。实测下来增大增益对距离有帮助,但幅度有限,别指望2厘米变成10厘米。真要远距离读卡,就得换125kHz低频模块或者加大尺寸天线板,这一步属于硬件的方案级调整,不是软件能绕过去的。
5.2 刷卡瞬间Arduino重启
这个现象十有八九出在继电器供电上。继电器吸合的瞬间,线圈电流突变会在5V电源线上制造一个很大的毛刺,如果Arduino和继电器共用同一个5V引脚,这个毛刺足以让主控复位。解决办法是给继电器单独供电,或者至少把继电器模块的VCC从Arduino的5V引脚挪开,改到外部5V电源的正极。
如果手头只有一块电源,也可以尝试在Arduino的5V电源入口并联一个470uF到1000uF的电解电容,把电源毛刺吸收掉一部分。再配合把继电器驱动改为光耦隔离模块,主控侧就基本干净了。这个问题的本质是电源回路不能和控制回路混流,分开供电之后,绝大多数重启问题都会消失。
5.3 卡号刷不出来或输出异常
先分清是完全读不到卡还是读到但显示乱码。完全读不到,优先检查接线,SDA接D10、RST接D9这两根线接错会导致模块初始化失败。其次看串口监视器是否有乱码,乱码一般是波特率不一致,统一用9600再试。
还有一种常见情况是卡放上去后偶尔读到、偶尔读不到。这种多数是天线板或连接线接触不良,杜邦线插在面包板上久了氧化松动,碰一下就会掉线。把连接处换成焊线或重新插紧,基本能解决。RC522模块本身也比较脆弱,天线线圈虚焊、模块被静电打坏都会导致读写不稳定,换块模块验证是最快的排查手段。
5.4 多张卡号码一样是怎么回事
一张空白Mifare S50卡,出厂时UID是唯一的,正常人正常渠道买的散装卡不会出现两张同号。如果你发现两张卡读出来的UID完全一样,那基本可以确定其中一张是UID卡,也就是可以改UID的卡。这种卡本来是给开发调试用的,被用在门禁上就可以用写卡器把另一张卡写成相同UID,等效于复制钥匙。
这种复制在门禁安防语境下是个风险。RC522只能读UID,不校验扇区内容,所以低成本的RFID门禁只能防君子不能防小人,真正需要高安全等级的场合,必须用CPU卡或带加密认证的读卡方案。自制项目知道这个边界就好,别把休闲级方案当银行级方案用,该提醒的边界还是要提醒。
5.5 复制卡、手机NFC能不能用
我实测过一部分手机NFC的模拟卡能被RC522读到,但稳定性依赖手机品牌和模拟方式,不能当作可靠方案,因为手机模拟的是HCE卡片,有些手机会在屏幕解锁后自动断开模拟,刷卡体验很差。苹果手机对第三方NFC模拟的限制更严格,指望手机当门禁卡,大部分情况下行不通,至少不能作为唯一方案。
复制卡方面,市面常见的UID卡可以正常被RC522读出,甚至可以把原卡的UID写到另一张卡。这个技术本身是调试工具,用途是否正当取决于使用者。对个人门禁来说,我建议直接多买几张空白卡,每张卡单独授权,真要加人加卡,改白名单数组重烧一版代码就行,这个流程足够简单,没必要碰复制卡。
这个项目我前后做了三版,第一版在面包板上跑通读卡逻辑,第二版把继电器接上电磁锁做了门禁,第三版才把电源隔离、EEPROM白名单、蜂鸣器反馈这些细节补齐。如果你只是周末想搭个东西玩玩,第一版就够;如果真要装到门上用,请务必把独立供电和安全接线放在第一位。后续我打算给这套控制器接一个OLED屏,刷卡时直接显示卡号和授权状态,再把白名单改成手机App写入,那就完全是下一个项目的事了。先把这套基础逻辑吃透,你会发现所有RFID场景都只是在读卡、比对、执行这条链路上换不同末端而已。