首页 / 资讯中心 / 文章详情

RC522+51单片机门禁读卡开发:软件SPI、寻卡选卡与M1扇区读写

RC522+51单片机门禁读卡开发:软件SPI、寻卡选卡与M1扇区读写 ★ FEATURED ARTICLE
简介面向51单片机与RFID入门及进阶开发者的RC522学习资料包围绕13.56MHz非接触通信与ISO/IEC 14443A标准解决从硬件连接到读写程序实现的完整流程问题。内容涉及RC522与51单片机的SPI接线及SCK、MISO、MOSI、SS、INT引脚功能SPI时钟极性与相位配置复位、工作模式选择、天线调谐等初始化命令流程以及卡片检测、防冲突、数据加密解密和读写响应处理。压缩包约91.47MB上游未提供文件总数及类型明细实际内容以解压后的程序与文档为准。已有1219人学习。资料价值在于把零散知识点连成可复用的开发路径可参考C语言或汇编示例理解命令收发与错误处理借助门禁、电子标签读写器等案例把握系统设计思路并补充天线设计与调谐、电源管理等容易被忽略的工程细节适合课程设计、毕业设计和DIY项目查漏补缺。1. 从一张门禁卡说起RC522 加 51 单片机这套资料能跑出什么接手一个门禁小改款甲方只给了一片 51 和一张现成的 IC 卡预算刚好够一个读卡模块。翻完手上这包 RC522 基于 51 单片机的资料它算是把该给的都铺开了13.56MHz 读卡芯片的 SPI 时序、寄存器配置、寻卡防冲突、M1 扇区读写全在 C 文件里配着接线图和最小系统原理图P1 口四根线接上去卡号就能从串口吐出来。它适合三类人课程设计要交差的学生、拿 51 做门禁考勤小产品的开发者、以及想弄明白无接触卡到底怎么被读出来的自学者。别指望它是个封装好的库代码偏底层连寄存器都得自己移位拼地址但正因为底层出了问题不用对着黑匣子猜。下面按接线、驱动、卡操作、踩坑、二次开发的顺序拆一遍照着走能跑通也能改成自己的东西。2. 硬件与通信层引脚接线、3.3V 供电和软件模拟 SPI2.1 引脚接线与供电的现实取舍51 系列基本没有硬件 SPI所以这套资料走的是软件模拟。好处是引脚随便挪坏处是时序全靠代码保证接错一根就读回一堆 0xFF。常见接法是把 RC522 挂在 P1 口因为 P1 是标准准双向口做输出时写 1 即可做输入时也能靠内部弱上拉顶一下。模块引脚51 侧接法说明SDA / NSSP1.3片选低电平有效每个寄存器访问都要完整拉高一次SCKP1.0时钟主机输出MOSIP1.1主机输出从机输入MISOP1.2主机输入读之前 P1 口要先写 1RSTP1.4复位低有效上电必须拉低再放开VCC3.3V裸模块严禁 5V带稳压的红板另说GNDGND必须共地别指望 USB 那头的地能顶替IRQ悬空这套代码走轮询中断脚用不上供电这块是第一个分水岭。裸模块蓝色小板只吃 2.5~3.3V5V 上去基本就是一次性用品市面上常见的红色小板背后焊了一颗 AMS1117-3.3VCC 可以给 5V但它的 SDA/SCK/MOSI 这些信号脚依旧是 3.3V 电平51 输出 5V 高电平长期打进去并不合规。我一般会在 MOSI、SCK、NSS 三根线上各串一个 1k 电阻限流成本几毛钱比换芯片便宜。如果是 3.3V 的 STC 型号比如 STC15W 系列那就直接对接省掉这一层。天线部分没什么可调的模块上的 TX1/TX2 和匹配电容出厂已经调好别手贱去拆。真正影响读距的是周围环境和 RFCfgReg 里的接收增益这个后面再说。2.2 软件模拟 SPI8 个循环把时序钉死RC522 支持 SPI 模式 0CPOL0CPHA0高位先行一次 8 位。翻译成操作就是时钟空闲为低数据在时钟低电平期间准备好从机在上升沿采样主机在时钟高电平期间读 MISO。/* 引脚集中定义换板子只改这一处 */ sbit RC522_SCL P1^0; sbit RC522_MOSI P1^1; sbit RC522_MISO P1^2; sbit RC522_NSS P1^3; sbit RC522_RST P1^4; /* 全双工发一个字节的同时收一个字节 */ unsigned char SPI_WriteByte(unsigned char dat) { unsigned char i; unsigned char recv 0; for (i 0; i 8; i) { RC522_SCL 0; /* 时钟拉低摆数据 */ if (dat 0x80) /* 先发最高位 */ RC522_MOSI 1; else RC522_MOSI 0; dat 1; RC522_SCL 1; /* 上升沿RC522 采样 MOSI */ recv 1; if (RC522_MISO) /* 高电平期间读回 MISO */ recv | 0x01; } RC522_SCL 0; /* 收尾把时钟放回空闲低 */ return recv; }参数dat是待发送的字节返回值是同一时刻从 MISO 上读回来的字节。SPI 是全双工所以写寄存器时第二个字节随便发什么都行而读寄存器时必须发一个哑字节 0xFF 把数据“顶”出来——这一点新手最容易卡住觉得读操作凭啥还要写。速度上不用太担心。11.0592MHz 晶振、12T 模式下这个循环一轮大概 8~10 个机器周期折算下来 SPI 时钟不到 1MHz而 RC522 的 SPI 上限是 10Mbps余量很大。但如果你换成了 1T 的 STC 且主频拉到 35MHz那最好在 SCL 拉高后插一两个_nop_()。这种问题很玄学实验室里跑一天没事装到现场偶发读错示波器一挂才看得见时钟毛刺。2.3 复位、软复位与版本寄存器自检上电之后别急着寻卡先把芯片叫醒并确认它到底在不在。这套资料里的初始化顺序是硬复位、软复位、配定时器、开天线一步都不能省。void RC522_Init(void) { RC522_RST 0; /* 硬复位至少保持 10ms */ DelayMs(10); RC522_RST 1; DelayMs(50); /* 等内部振荡器稳定 */ RC522_WriteReg(CommandReg, 0x0F); /* SoftReset */ while (RC522_ReadReg(CommandReg) 0x10); /* 等 PowerDown 位自清 */ /* 定时器预分频 0x0D3E 339013.56MHz/(2*33901) ≈ 2kHz */ RC522_WriteReg(TModeReg, 0x8D); /* 低 4 位是预分频高位 */ RC522_WriteReg(TPrescalerReg, 0x3E); RC522_WriteReg(TReloadRegH, 0x00); RC522_WriteReg(TReloadRegL, 30); /* 30/2kHz ≈ 15ms 超时 */ RC522_WriteReg(TxASKReg, 0x40); /* M1 卡必须 100% ASK */ RC522_WriteReg(ModeReg, 0x3D); /* CRC 初值 0x6363 */ RC522_WriteReg(RFCfgReg, 0x70); /* 接收增益 48dB */ RC522_AntennaOn(); /* TxControlReg 低两位置 1 */ }几个参数值得单独说。TModeReg 0x8D的低 4 位是预分频高 4 位和TPrescalerReg拼起来才是完整的 12 位预分频值很多人只改低 8 位结果超时时间对不上程序在等中断的地方卡死。TxASKReg 0x40是 M1 卡能不能被寻到的关键默认调制方式不对的话卡贴到天线上也只会返回“无卡”。ModeReg 0x3D把 CRC 预设值设成 0x6363M1 卡的 CRC_A 就是这个初值设错会导致选卡永远失败。初始化完补一句自检能省掉后面 80% 的排查时间unsigned char RC522_CheckVersion(void) { unsigned char v RC522_ReadReg(VersionReg); /* 地址 0x37 */ if (v 0x91 || v 0x92) return 1; /* 原厂片 */ if (v 0x12) return 2; /* 兼容片能用但增益偏低 */ return 0; /* 0x00 或 0xFF通信没通 */ }读回 0x00 和 0xFF 是两种不同的病0x00 多半是 MISO 一直为低线没接、或者 P1 口没先写 10xFF 多半是 NSS 没拉高或者模块根本没供电。这个判断比拿万用表一根根量快得多。3. 驱动层拆解从寄存器读写到寻卡选卡的完整链路3.1 地址移位RC522 的 SPI 地址字节规则RC522 的寄存器地址是 6 位的但 SPI 上发出去的地址字节是 8 位最高位是读写标志1 读 0 写中间 6 位是地址最低位固定 0。所以写寄存器发(addr 1) 0x7E读寄存器再或上 0x80。void RC522_WriteReg(unsigned char addr, unsigned char val) { RC522_NSS 0; /* 片选拉低一帧开始 */ SPI_WriteByte((unsigned char)((addr 1) 0x7E)); /* 写bit7 0 */ SPI_WriteByte(val); RC522_NSS 1; /* 帧结束必须拉高 */ } unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; RC522_NSS 0; SPI_WriteByte((unsigned char)(((addr 1) 0x7E) | 0x80)); /* 读bit7 1 */ val SPI_WriteByte(0xFF); /* 哑字节顶出数据 */ RC522_NSS 1; return val; } void RC522_SetBit(unsigned char addr, unsigned char mask) { RC522_WriteReg(addr, RC522_ReadReg(addr) | mask); } void RC522_ClearBit(unsigned char addr, unsigned char mask) { RC522_WriteReg(addr, RC520_ReadReg(addr) (unsigned char)~mask); }RC522_SetBit和RC522_ClearBit是读改写看着简单但它们是后面 FIFO 清空、启动发送、开天线的通用手段。注意RC522_NSS必须在每次寄存器访问的末尾拉高不能一口气连着读好几个寄存器只拉高一次——RC522 靠 NSS 的上升沿判定一帧结束不拉高会出现地址错位读回来的值全是对的但位置全错这种 bug 查起来非常难受。3.2 PcdComMF522一条命令从 FIFO 到天线的全过程所有跟卡交互的动作最终都汇到这一个函数里把命令字塞进 FIFO启动发送等中断再从 FIFO 把应答捞出来。它是整个驱动的中枢。#define PCD_IDLE 0x00 #define PCD_AUTHENT 0x0E #define PCD_TRANSCEIVE 0x0C #define PCD_CALCCRC 0x03 #define MI_OK 0 #define MI_NOTAGERR 1 #define MI_ERR 2 unsigned char PcdComMF522(unsigned char cmd, unsigned char *pIn, unsigned char inLen, unsigned char *pOut, unsigned int *pOutLen) { unsigned char status MI_ERR; unsigned char irqEn 0x00; unsigned char waitFor 0x00; unsigned char n, lastBits; unsigned int i; switch (cmd) { case PCD_AUTHENT: /* 认证等空闲中断或错误中断 */ irqEn 0x12; waitFor 0x10; break; case PCD_TRANSCEIVE: /* 收发等接收完成或空闲 */ irqEn 0x77; waitFor 0x30; break; default: return MI_ERR; } RC522_WriteReg(ComIEnReg, irqEn | 0x80); /* bit7 是中断总使能 */ RC522_ClearBit(ComIrqReg, 0x80); /* 清 Set1 标志 */ RC522_WriteReg(ComIrqReg, 0x7F); /* 清全部中断标志位 */ RC522_WriteReg(CommandReg, PCD_IDLE); RC522_SetBit(FIFOLevelReg, 0x80); /* FlushBuffer清空 FIFO */ while (inLen--) /* 命令字 参数进 FIFO */ RC522_WriteReg(FIFODataReg, *pIn); RC522_WriteReg(CommandReg, cmd); if (cmd PCD_TRANSCEIVE) RC522_SetBit(BitFramingReg, 0x80); /* StartSend启动发射 */ i 2000; /* 软件超时防死等 */ do { n RC522_ReadReg(ComIrqReg); } while ((--i) !(n 0x01) !(n waitFor)); RC522_ClearBit(BitFramingReg, 0x80); /* 关掉 StartSend */ if (i 0) /* 硬件没响应直接退出 */ return MI_ERR; if (RC522_ReadReg(ErrorReg) 0x1B) /* 溢出/奇偶/协议错误 */ return MI_ERR; if (n waitFor) { status MI_OK; if (n irqEn 0x01) status MI_NOTAGERR; /* 定时器到点没卡应答 */ if (cmd PCD_TRANSCEIVE) { lastBits RC522_ReadReg(ControlReg) 0x07; /* 最后一字节有效位数 */ n RC522_ReadReg(FIFOLevelReg); *pOutLen lastBits ? ((n - 1) * 8 lastBits) : (n * 8); if (n 0) n 1; if (n 16) n 16; /* FIFO 只有 64 字节保护一下 */ for (i 0; i n; i) pOut[i] RC522_ReadReg(FIFODataReg); } } else { status MI_ERR; } return status; }irqEn和waitFor是两个位掩码前者决定打开哪些中断源后者决定等哪些标志才算结束。认证命令等的是 IdleIRq0x10收发命令等的是 RxIRq0x20加 IdleIRq。i 2000那句软件超时是血泪经验早期版本没有它模块一旦没接触好程序就永久停在这个 do-while 里看门狗都救不回来。*pOutLen算的是有效位数而不是字节数因为寻卡返回的是 16 位2 字节选卡返回 24 位3 字节读写块返回 128 位16 字节全用位来对齐才统一。3.3 寻卡、防冲突、选卡三帧握手一张卡从进入射频场到被选中要走三步寻卡REQA、防冲突Anticollision、选卡Select。这三步走完你才算真正拿到了一张卡的 UID。/* 第一步寻卡req_code 0x26 寻空闲卡0x52 唤醒休眠卡 */ unsigned char PcdRequest(unsigned char req_code, unsigned char *tagType) { unsigned char status; unsigned int backBits 0; RC522_WriteReg(BitFramingReg, 0x07); /* 只发 7 位短帧 */ tagType[0] req_code; status PcdComMF522(PCD_TRANSCEIVE, tagType, 1, tagType, backBits); if ((status ! MI_OK) || (backBits ! 0x10)) /* ATQA 一定是 16 位 */ status MI_ERR; return status; } /* 第二步防冲突拿回 4 字节 UID 1 字节 BCC */ unsigned char PcdAnticoll(unsigned char *snr) { unsigned char status, i, bcc 0; unsigned int backBits 0; RC522_WriteReg(BitFramingReg, 0x00); /* 冲突检测用整字节 */ snr[0] 0x93; /* 防冲突命令字 */ snr[1] 0x20; status PcdComMF522(PCD_TRANSCEIVE, snr, 2, snr, backBits); if (status ! MI_OK) return status; for (i 0; i 4; i) bcc ^ snr[i]; /* BCC 是 UID 四字节异或 */ if (bcc ! snr[4]) status MI_ERR; return status; } /* 第三步选卡UID BCC CRC 一起发过去 */ unsigned char PcdSelect(unsigned char *snr) { unsigned char status, i, buf[9]; unsigned int backBits 0; RC522_WriteReg(BitFramingReg, 0x00); buf[0] 0x93; buf[1] 0x70; for (i 0; i 5; i) buf[i 2] snr[i]; /* UID BCC */ PcdCalcCRC(buf, 7); /* 后两字节填 CRC_A */ status PcdComMF522(PCD_TRANSCEIVE, buf, 9, buf, backBits); if ((status MI_OK) (backBits 0x18)) /* SAK 是 24 位 */ return MI_OK; return MI_ERR; }PcdCalcCRC走的是芯片自带的 CRC 协处理器把数据写进 FIFO发PCD_CALCCRC命令等DivIrqReg的 bit2CRCIRq置位再从CRCResultRegL/H读回两个字节。这比软件查表算 CRC 快得多也少写一堆代码。PcdRequest里BitFramingReg 0x07那句别漏REQA 是 7 位短帧不是 8 位。设成整字节发出去卡是能收到但协议上不合法某些卡会直接不应答——这类“有的卡能读有的卡不能读”的怪现象八成就是这里。3.4 主循环把卡号从串口吐出来三帧走完就拿到了 4 字节 UID剩下的事就是格式化输出。资料里的主流程很朴素初始化串口9600、8N1初始化 RC522然后死循环寻卡。void main(void) { unsigned char tagType[2], uid[5], i; UART_Init(); /* 9600, 8, N, 1 */ RC522_Init(); if (RC522_CheckVersion() 0) { UART_SendString(RC522 not found\r\n); while (1); } UART_SendString(RC522 ready\r\n); while (1) { if (PcdRequest(0x52, tagType) MI_OK) { /* 0x52 能唤醒休眠卡 */ if (PcdAnticoll(uid) MI_OK) { if (PcdSelect(uid) MI_OK) { UART_SendString(UID:); for (i 0; i 4; i) { UART_SendHex(uid[i]); /* 两位十六进制 */ UART_SendByte( ); } UART_SendString(\r\n); PcdHalt(); /* 让卡休眠避免重复读 */ } } } DelayMs(100); /* 别把 CPU 跑满 */ } }PcdHalt()这一句很关键。M1 卡被选中后一直处于激活态如果不发 HALT 命令下一次寻卡它不会重新应答表现就是“第一次能读第二次读不到”。HALT 命令0x50 0x00 CRC会让卡回到休眠下次用 0x52 唤醒就行这也是为什么主循环里用 0x52 而不是 0x26。4. 卡片数据操作M1 扇区结构、密钥认证与读写块4.1 一张 M1 卡里到底存了什么M1 卡也就是常见的 IC 卡、门禁卡容量 1K 字节分成 16 个扇区每个扇区 4 个块每块 16 字节。第 0 扇区第 0 块是厂商固化区存着 UID 和厂商数据只读每个扇区的第 3 块块号 3、7、11……63是密钥区存密钥 A6 字节、访问控制位4 字节、密钥 B6 字节出厂默认通常是FF FF FF FF FF FFFF 07 80 69FF FF FF FF FF FF。块号用途可否写0厂商块UID BCC SAK只读1、2数据块可写需认证3密钥区密钥 A 控制位 密钥 B可写写错整扇区报废4~6第 1 扇区数据块可写7第 1 扇区密钥区谨慎……以此类推共 16 扇区 64 块访问控制位决定每个块含密钥区自己用密钥 A 还是密钥 B 认证、能读能写还是只能加减。出厂值FF 07 80 69的含义是密钥 A 可读可写数据块用密钥 A 认证后可读可写。搞门禁小项目直接用出厂密钥 A 认证就够别轻易动控制位。4.2 认证、读块、写块三道必过的关读任何一个数据块之前必须先对该块所在扇区做一次密钥认证认证通过后Status2Reg的 bit3MFCrypto1On会置 1之后同扇区的块可以直接读换扇区要重新认证。/* 认证mode 0x60 用密钥 A0x61 用密钥 B */ unsigned char PcdAuthState(unsigned char mode, unsigned char block, unsigned char *key, unsigned char *uid) { unsigned char status, buf[12], i; unsigned int backBits 0; buf[0] mode; buf[1] block; for (i 0; i 6; i) buf[2 i] key[i]; /* 6 字节密钥 */ for (i 0; i 4; i) buf[8 i] uid[i]; /* 4 字节 UID */ status PcdComMF522(PCD_AUTHENT, buf, 12, buf, backBits); /* 光看返回值不够必须确认加密单元真的打开了 */ if ((status ! MI_OK) || !(RC522_ReadReg(Status2Reg) 0x08)) status MI_ERR; return status; } /* 读块返回 16 字节 */ unsigned char PcdReadBlock(unsigned char block, unsigned char *data) { unsigned char status, buf[4]; unsigned int backBits 0; buf[0] 0x30; /* MIFARE READ */ buf[1] block; PcdCalcCRC(buf, 2); /* 补 CRC_A凑够 4 字节 */ status PcdComMF522(PCD_TRANSCEIVE, buf, 4, data, backBits); if ((status ! MI_OK) || (backBits ! 0x80)) /* 16 字节 128 位 */ status MI_ERR; return status; } /* 写块16 字节数据 2 字节 CRC等 4 位 ACK */ unsigned char PcdWriteBlock(unsigned char block, unsigned char *data) { unsigned char status, buf[18], i; unsigned int backBits 0; buf[0] 0xA0; /* MIFARE WRITE */ buf[1] block; for (i 0; i 16; i) buf[2 i] data[i]; PcdCalcCRC(buf, 18); status PcdComMF522(PCD_TRANSCEIVE, buf, 20, buf, backBits); if ((status ! MI_OK) || (backBits ! 0x04)) return MI_ERR; if ((buf[0] 0x0F) ! 0x0A) /* ACK 是 4 位 1010b */ return MI_ERR; return MI_OK; }PcdAuthState里那句Status2Reg 0x08是很多人漏掉的。认证命令返回 MI_OK 只代表“命令发出去了”不代表“认证成功了”真正的判据是加密单元有没有打开。少了这行密钥错的时候你会在读块那一步收到一堆乱码然后往通信线路上找问题白折腾半天。写块的 ACK 判断也要注意返回长度是 4 位不是 8 位backBits要等于 4数据在buf[0]的低 4 位等于 0x0A 才是成功其余值0x00、0x04、0x05都是 NAK代表目标块不可写、密钥权限不够或者卡已经移开。4.3 卡号落地白名单、防重复读与掉电保存读出本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站