资讯动态

RC522与51单片机SPI读卡驱动及M1卡扇区读写实战

发布时间:2026/10/26 18:54:57 来源:尧图企业网站定制
简介面向51单片机与RFID入门及实践者的RC522学习资料包围绕13.56MHz非接触通信与ISO/IEC 14443A标准展开适合门禁、读卡器、电子标签等课程设计或小型项目参考。内容重点可覆盖RC522与51单片机的SPI硬件连接、通信时序配置、复位初始化与命令框架、卡片检测与防冲突、读写流程、错误处理及天线调谐和功耗优化并配有示例程序与典型应用案例便于对照理解从底层通信到功能实现的完整链路。资源包约91.47MB文件总数与主要类型明细上游暂未提供检索时需结合目录自行整理目前已有1219人学习或下载具备一定参考热度。读者可据此搭建实验环境、梳理读写程序结构并借鉴排错思路与应用案例提升RC522项目落地效率。1. 从门禁卡到串口日志RC522 在 51 单片机上是个什么活门禁卡往读头前一贴串口助手就吐出一串十六进制卡号这是 RC522 配 51 单片机的经典场景。不少人第一次碰 13.56MHz 读卡都是从一份 RC522 基于 51 单片机资料起步的里面一般有原理图、SPI 驱动、寻卡读卡例程四根信号线接对就能出数据。这套组合特别适合做课程设计、门禁考勤原型或者只想低成本验证“卡号能不能读到、扇区能不能写”的开发者。51 的 IO 速度慢、RAM 小但驱动 RC522 刚好够用程序结构不复杂真出了毛病也容易顺着信号线往回查。资料包真正的价值不在于例程多花哨而在于它把读卡这条链路拆成了能自己改的几段引脚、SPI、寻卡、认证、读块。后面几章就按这个顺序拆。2. 硬件与工程结构四根 SPI 线到底怎么接资料包里有什么2.1 引脚连接与供电先看这几根线RC522 模块对外一般是八个脚SDA片选 CS、SCK、MOSI、MISO、IRQ、GND、RST、3.3V。51 单片机没有硬件 SPI 的型号居多即使用 STC89C52 这种也常直接用 GPIO 模拟。典型接法是这样RC522 模块51 单片机说明SDA / CSP1.3片选低电平有效一根线独占SCKP1.4SPI 时钟主机输出MOSIP1.5主机发、从机收MISOP1.6从机发、主机收RSTP1.7复位高电平有效部分模块可悬空3.3V3.3V必须 3.3V不能直接上 5VGNDGND共地供电这条要单独强调。RC522 芯片工作是 3.3V模块上的电平转换和天线匹配也按 3.3V 设计。5V 直接灌进去轻则读卡距离骤降重则芯片发热报废。如果主控是 5V 系统SPI 信号线要做电平转换或者至少串 1k 电阻限流很多资料包会附一张电平转换的参考图别跳过。天线部分是另一个容易被忽略的点。模块自带 PCB 天线周围 2 到 3 厘米内不要放金属、电池、排针座否则读卡距离从 3 厘米掉到贴卡才响应。这一点在桌面上调试时最容易翻车板子往笔记本金属掌托上一放现象就变了。2.2 资料包目录与文件职责一份完整的 RC52251 资料通常长这样目录名可能不同但职责类似RC522_51_Demo/ ├── Doc/ │ ├── RC522_DS.pdf # 芯片数据手册寄存器定义都在这 │ └── S50_扇区说明.pdf # M1 卡存储结构 ├── Hardware/ │ ├── RC522_51_SCH.pdf # 原理图 │ └── RC522_51_PCB.pdf ├── Src/ │ ├── main.c # 主流程初始化、寻卡、串口输出 │ ├── rc522.c # 驱动层SPI、寄存器、命令、寻卡、读写 │ ├── rc522.h # 引脚宏、命令字、函数声明 │ └── uart.c # 串口打印波特率 9600 └── Keil/ └── RC522_51.uvproj # 工程文件Keil C51 打开看一个 51 工程先看rc522.h里的引脚宏和命令字再看main.c的主循环最后才钻进rc522.c。顺序反过来容易在寄存器里迷路。2.3 51 单片机的资源分配与最小系统RC522 驱动本身占的资源不多GPIO 五根、一个定时器做超时、串口做输出。以 STC89C52 为例11.0592MHz 晶振配 9600 波特率刚好整除串口误码率低。定时器 0 可以拿来做命令超时计时避免某条命令发出后卡死等中断。RAM 只有 128 字节读一块 16 字节的数据就要占掉八分之一所以缓冲区别开太大能在函数内复用就复用。最小系统要确认三件事晶振起振、复位电路正常、EA 引脚接高电平片内程序存储器。这三条不满足程序烧进去也不跑现象上会误判成“RC522 没读到卡”。提示先把串口能正常打印“上电”两个字再接 RC522。排错顺序从电源和串口开始比一上来怀疑天线要省时间。3. 驱动层拆解一个字节怎么进一张卡怎么被选中3.1 SPI 字节收发与寄存器读写51 上没有硬件 SPI 的时候时序全靠 GPIO 翻转。RC522 的 SPI 是模式 0空闲时钟低电平数据在上升沿被采样下降沿输出。一个字节的收发函数大概长这样// rc522.c 底层 SPI模式 0高位先出 unsigned char SPI_RW(unsigned char byte) { unsigned char i, recv 0; for (i 0; i 8; i) { RC522_MOSI (byte 0x80) ? 1 : 0; // 先放数据位 byte 1; RC522_SCK 1; // 上升沿从机采样 MOSI recv 1; if (RC522_MISO) recv | 0x01; // 高电平期间读 MISO RC522_SCK 0; // 下降沿从机更新 MISO } return recv; }这段函数里byte是要发出去的字节recv是同时收回来的字节。RC522 的 SPI 是全双工发一个地址字节的同时会返回上一次命令留下的数据所以读寄存器时要发一个哑字节0xFF把数据“换”回来。寄存器读写的地址格式是关键地址字节 bit7 是读写位1 读、0 写bit6 到 bit1 是寄存器地址bit0 固定 0。写出来就是// 写寄存器地址左移一位读写位为 0 void RC522_WriteReg(unsigned char addr, unsigned char val) { RC522_CS 0; SPI_RW((addr 1) 0x7E); // 地址左移bit7 清零 SPI_RW(val); RC522_CS 1; } // 读寄存器地址左移后把 bit7 置 1 unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; RC522_CS 0; SPI_RW(((addr 1) 0x7E) | 0x80); val SPI_RW(0xFF); // 哑字节换数据 RC522_CS 1; return val; }addr传的是寄存器原始地址比如CommandReg是0x01函数内部做移位。这里最容易写错的是移位方向一旦写反现象是“所有寄存器读出来都是 0x00 或 0xFF”看着像模块坏了。3.2 命令收发统一封装RC522 的交互模式是“写 FIFO → 启动命令 → 等中断 → 读 FIFO”。把这段流程封成一个函数上层业务就不用每次碰寄存器// 通用命令收发in 是待发数据out 收返回outLen 传入缓冲区大小、传出实际长度 char PcdComand(unsigned char cmd, unsigned char *in, unsigned char inLen, unsigned char *out, unsigned int *outLen) { unsigned char status; unsigned int i; RC522_WriteReg(CommandReg, PCD_Idle); // 先让芯片空闲 RC522_WriteReg(ComIEnReg, 0x77); // 使能收发、定时器中断 RC522_WriteReg(ComIrqReg, 0x7F); // 清所有中断标志 RC522_WriteReg(FIFOLevelReg, 0x80); // 清 FIFO 指针 for (i 0; i inLen; i) RC522_WriteReg(FIFODataReg, in[i]); // 数据压进 FIFO RC522_WriteReg(CommandReg, cmd); // 启动命令 if (cmd PCD_Transceive) RC522_SetBitMask(BitFramingReg, 0x80); // Transceive 需要手动触发发送 i 2000; // 超时计数按主频调 do { status RC522_ReadReg(ComIrqReg); } while ((i--) !(status 0x30)); // 等 RxIRq 或 IdleIRq RC522_ClearBitMask(BitFramingReg, 0x80); if (!i) return 1; // 超时 // 取返回数据 *outLen RC522_ReadReg(FIFOLevelReg); for (i 0; i *outLen i 16; i) out[i] RC522_ReadReg(FIFODataReg); return 0; }cmd是 RC522 内部命令字常用的有PCD_Transceive0x0C收发一体、PCD_AuthEnt0x0E认证。in、inLen控制发出去的内容out是被调用方给的缓冲区长度不能小于预期返回值读块时要给 16 字节。超时计数2000是个经验值主频越高这个数要越大否则命令还没回就判定超时现象是“寻卡偶尔成功、偶尔失败”。3.3 寻卡、防冲突、选卡三步走一张 M1 卡进入天线场流程固定三步Request 让卡回应、Anticollision 拿 UID、Select 选中卡片。寻卡先发短帧0x26REQA只寻空闲卡或0x52WUPA寻所有卡// 寻卡reqCode 用 PICC_REQIDL 或 PICC_REQALL char PcdRequest(unsigned char reqCode, unsigned char *cardType) { char status; unsigned int backLen 2; RC522_WriteReg(BitFramingReg, 0x07); // 短帧7 位有效 cardType[0] reqCode; status PcdComand(PCD_Transceive, cardType, 1, cardType, backLen); if (status ! 0 || backLen ! 2) return 1; return 0; // cardType[0..1] 里是 ATQA }BitFramingReg写0x07表示只发 7 位因为 REQA 是短帧命令。发完卡回两个字节 ATQAbackLen不对就说明场上没卡或者指令格式错了。防冲突拿 UIDchar PcdAnticoll(unsigned char *uid) { char status; unsigned int backLen 5; RC522_WriteReg(BitFramingReg, 0x00); // 整字节模式 uid[0] PICC_ANTICOLL; // 0x93 uid[1] 0x20; status PcdComand(PCD_Transceive, uid, 2, uid, backLen); if (status ! 0 || backLen ! 5) return 1; // 5 字节里前 4 字节是 UID最后 1 字节是 BCC 校验 if ((uid[0]^uid[1]^uid[2]^uid[3]) ! uid[4]) return 1; return 0; }PICC_ANTICOLL是0x93后面紧跟0x20。返回 5 字节前四个是 UID第五个是异或校验。校验不过一般是场上有多张卡同时响应需要做完整的防冲突循环例程为了简单往往只处理单卡场景。选卡发0x93 0x70加 UID 加 BCC卡片回 SAK 和 CRCchar PcdSelect(unsigned char *uid) { char status; unsigned int backLen 3; unsigned char buf[9]; buf[0] PICC_SELECT; // 0x93 buf[1] 0x70; memcpy(buf[2], uid, 5); // UID 4 字节 BCC status PcdComand(PCD_Transceive, buf, 7, buf, backLen); if (status ! 0 || backLen ! 3) return 1; return 0; }三步都过了卡才算真正“选中”后面认证和读写才有意义。跳步骤直接读块返回的一定是认证失败。4. 应用层卡号、扇区和串口例程里最该看懂的三段4.1 卡号处理与格式转换很多资料包的main.c里读卡号就一段寻卡成功就把uid[0..3]打印出来。真正的项目里这点不够用卡号要以整型存、要以字符串显示、还要能比对白名单。常见做法是把四字节 UID 拼成一个 32 位无符号数// uid 4 字节拼成 32 位卡号同时输出十六进制字符串 unsigned long UID_ToCardNo(unsigned char *uid, char *hexStr) { unsigned long cardNo; cardNo ((unsigned long)uid[0] 24); cardNo | ((unsigned long)uid[1] 16); cardNo | ((unsigned long)uid[2] 8); cardNo | (unsigned long)uid[3]; sprintf(hexStr, %02X%02X%02X%02X, uid[0], uid[1], uid[2], uid[3]); return cardNo; }这里uid是防冲突后拿到的四字节序列号hexStr至少要 9 字节8 个字符加结束符。sprintf在 Keil C51 里会吃掉不少 ROMROM 紧张的型号可以换手工拼字符或者直接只打整数。卡号注意区分“正序”和“倒序”。有的例程把 UID 倒着拼换个资料包就对不上做门禁白名单时会遇到“同一张卡两个系统读出两个号”的情况。我们一般以uid[0]为最高字节资料包里如果反着来就在这一层统一掉。4.2 扇区数据与块结构M1 卡 16 个扇区每扇区 4 块每块 16 字节。块 0 是厂商块存 UID 和厂商数据只读块 3 是控制块存 KeyA、访问位、KeyB块 1、2 才是能放业务数据的。控制块默认值一般是块 3FF FF FF FF FF FF | FF 07 80 69 | FF FF FF FF FF FF KeyA(6字节) 访问位(4字节) KeyB(6字节)FF 07 80 69这组访问位表示 KeyA 和 KeyB 都能读写数据块默认密钥都是FF FF FF FF FF FF。资料包里演示写卡多半就是往块 1 写 16 个字节再用读块读回来// 往指定块写 16 字节调用前必须已完成认证 char PcdWrite(unsigned char blockAddr, unsigned char *data) { char status; unsigned int backLen 1; unsigned char buf[18]; buf[0] PICC_WRITE; // 0xA0 buf[1] blockAddr; // 先发写命令和块地址卡回 ACK status PcdComand(PCD_Transceive, buf, 2, buf, backLen); if (status ! 0 || (buf[0] 0x0F) ! 0x0A) return 1; // 再发 16 字节数据 2 字节 CRC memcpy(buf, data, 16); RC522_CalcCRC(buf, 16, buf[16]); // 计算 CRC_A 填到末尾 status PcdComand(PCD_Transceive, buf, 18, buf, backLen); if (status ! 0 || (buf[0] 0x0F) ! 0x0A) return 1; return 0; }blockAddr是要写的块号绝对块号从 0 到 63块 0 不可写。写操作要分两段先发命令拿 ACK再发数据和 CRC 拿第二个 ACK两段都通过才算写完。中间任何一步返回的不是0x0A都说明卡拒绝了这次写。4.3 串口输出与联调调试阶段最简单有效的手段是把每一步结果打到串口。寻卡失败打REQ FAIL、防冲突失败打ANTICOLL FAIL、认证失败打AUTH FAIL比盯着代码猜快得多。串口初始化和发送在原子里通常有现成的void UART_SendString(unsigned char *str) { while (*str) { SBUF *str; // 写入发送缓冲 while (!TI); // 等发送完成 TI 0; // 清标志 } }SBUF是 51 的串口数据寄存器TI是发送完成标志。波特率 9600、晶振 11.0592MHz 时定时器初值TH1 0xFD。串口助手那边记得勾选“发送新行”以外还要看十六进制显示卡号这类数据用 HEX 模式看比 ASCII 直观。联调顺序我一般这样排先只打印上电信息确认串口通路再接 RC522 打印VersionReg地址 0x37这个寄存器正常应读出0x91或0x92读不对就说明 SPI 接线或供电有问题跟寻卡无关。这一步能把“模块坏没坏”和“程序对不对”分开。5. 避坑与常见问题排查读不到卡时先查这四处5.1 现象读卡距离只有贴住才响应现象很典型卡放在天线正上方 1 厘米就没反应必须贴着模块才偶尔读到。原因通常不是代码而是天线环境和供电。模块天线旁边放了金属、杜邦线太长、供电用 5V 串了电阻压降太大都会让发射功率掉下来。解决上先换 3.3V 稳压源单独给模块供电再把模块从金属桌面挪开杜邦线压到 10 厘米以内。还不行就查TxControlReg的0x03两位有没有置上天线没开的时候现象跟这个一模一样。5.2 现象寻卡时成功时失败同一个程序有时候一次就读到有时候连读十几次都没反应。原因多半在超时和 SPI 速度。51 的 GPIO 翻转速度有限PcdComand里的超时计数如果给小了命令还没执行完就被判超时SPI 每位之间没有短延时长排线时时钟沿会畸变。解决是把超时计数从 2000 加到 5000 试SPI 函数里在拉高 SCK 后加一两个_nop_()排线缩短。另外确认ComIEnReg写的是0x77中断没使能全等中断的地方会一直转圈。5.3 现象寻卡成功但认证一直失败卡能选中、UID 也能读出来一到认证就返回失败。原因有两个方向密钥不对或者块地址不对。M1 卡出厂密钥是六个0xFF但有些卡被写过之后密钥改了块地址传成绝对块号比如 4而认证时传的是扇区号两者混用就失败。解决是先用FF FF FF FF FF FF认证块 0 之外的数据块确认能过再改密钥认证函数第二个参数统一按“绝对块号”传要访问某扇区就先算扇区号 * 4。还不行就看Status2Reg的0x08位认证成功后这一位会置起来没置起来就是真失败。5.4 现象写块成功但读回来数据错位写进去 16 字节读回来发现后半段是上一张卡的数据或者整体偏移一个字节。原因是写操作少发了 CRC或者 FIFO 没清干净。PcdWrite第二段必须发 18 字节16 数据加 2 字节 CRC_ACRC 用 RC522 自带的PCD_CalcCRC命令算不要手工拼。另外每次命令前FIFOLevelReg写0x80清 FIFO这条漏了会残留上次的数据。写完立刻读回校验一次比事后查数据对不上要省事。5.5 现象串口打印乱码卡读到了串口助手上却是一堆问号或者乱码。原因基本是波特率不匹配晶振不是 11.0592MHz 却按 9600 算初值或者串口助手选错波特率。解决是先确认板子上的晶振频率11.0592MHz 配 9600 用TH10xFD12MHz 配 9600 误差大要换 4800 或者换晶振。还有一个隐蔽原因是UART_SendString里TI没清发完第一个字节就卡住现象是只出第一个字符。6. 二次开发把“读个卡号”改成能维护的卡片数据结构例程跑通以后真正要动手的是业务层。读卡号只是入口后面往往要存白名单、要记录刷卡时间、要区分“管理卡”和“用户卡”。我不建议直接在main.c里堆if而是先把卡片抽象成一个结构体typedef struct { unsigned long cardNo; // 32 位卡号用于比对 unsigned char uid[4]; // 原始 UID用于写卡和排查 unsigned char type; // 0 普通卡1 管理卡 unsigned char sector; // 数据所在扇区 unsigned char block; // 数据所在块 } CardInfo; // 读取卡号并填充结构体返回 0 表示成功 char Card_ReadInfo(CardInfo *card) { unsigned char uid[5]; if (PcdRequest(PICC_REQIDL, uid) ! 0) return 1; if (PcdAnticoll(uid) ! 0) return 2; if (PcdSelect(uid) ! 0) return 3; memcpy(card-uid, uid, 4); card-cardNo UID_ToCardNo(uid, NULL); return 0; }这个结构体的意义在于把“读卡”和“业务判断”分开读卡函数只负责填CardInfo白名单比对、类型判断、存储都放在上层。以后换 RC522 以外的读头只要新驱动也能填这个结构体上层代码不用动。改造时有两个验证点值得固定下来。第一用一张空白卡和一张已写数据的卡各跑十次确认Card_ReadInfo十次都返回 0排除时序偶发问题。第二写完块之后立刻读回比对不一致就重写一次这个“写后校验”的习惯能挡掉大部分数据错乱。存储结构上我一般把卡号存块 1 的前 4 字节类型存第 5 字节剩余字节留给时间戳或余额块 2 留作扩展区控制块坚决不动密钥保持出厂值等业务稳定了再统一改。从那以后我每次拿到一份新的读卡例程都会先拿空白卡把块 0 和块 15 各读一遍确认 UID 稳定、密钥能过再往下写业务逻辑。这个习惯帮我省掉过好几次“以为是程序错、其实是卡不对”的来回。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑