STC89C52与RC522读写M1卡源码解析:从SPI到防碰撞认证流程
发布时间:2026/9/11 20:05:43 锦皓数字建站

简介面向嵌入式初学者、电子竞赛参赛者和 RFID 应用开发者这份 STC89C52-RC522 源码包以 STC89C52 单片机控制 RC522 模块完整实现 M1 非接触式 IC 卡的读写操作解决从底层驱动到扇区密钥验证、数据存取的落地问题。压缩包内共三十二个文件大小约九十五 KB以 C 源文件、Keil 工程文件、编译中间文件和可烧录的 hex 文件为主同时包含少量备份与配置日志目录结构清晰方便打开工程直接学习。目前已有 1018 人学习下载。项目代码覆盖 MFRC522 初始化、ISO14443A 标准下的防碰撞与选卡流程、M1 卡各扇区密钥认证及读写操作并附带 1602/12864 液晶显示驱动便于实时观察卡片信息与调试结果。读者可从中提取可复用的 RC522 驱动框架理解单片机与射频芯片的时序配合进而迁移到门禁、校园卡等实际场景。1. STC89C52 搭配 RC522 读写 M1 卡为什么源码比现成模块更值得拆很多人以为用一块串口转 RC522 的现成读写模块发几条 AT 指令就能把 M1 卡搞定。实际放到门禁、签到、防伪这类需要稳定批量读写的场景里卡没放稳、扇区密钥错误、天线信号弱任何一个环节抖动都会导致读写失败。核心问题不在卡片协议本身而在单片机端的状态控制防碰撞流程走没走对、认证用的是 KeyA 还是 KeyB、块数据有没有按 16 字节对齐。STC89C52-RC522 这套源码正好把这些步骤完整串起来了从最底层的 SPI 读写寄存器到 PcdRequest、PcdAnticoll、PcdAuthState、PcdRead 逐层向上适合刚接触 RFID 的嵌入式工程师也适合想脱离模块指令集、直接操作 MFRC522 寄存器的人。下面按工程落地顺序把硬件接口、扇区结构、状态机、调试手段逐个展开。2. STC89C52 与 RC522 的引脚映射、SPI 电平与时序初始化2.1 硬件连接与 5V/3.3V 电平处理STC89C52 是标准 5V 单片机而 RC522 模块核心 MFRC522 的工作电压是 3.3V。市面常见的 RC522 模块通常已经板载 RT9193 或类似 LDO 芯片和电平转换电路可以直接与 5V 单片机连接但仍要确认模块丝印上是否标有 3.3V 和 5V 两个供电引脚。推荐连接方式RC522 的 VCC 接模块上的 3.3V 或 5V 引脚取决于模块设计大多数模块标注 VCC 就是 5V 输入、板上自转 3.3V。SDA 对应 SPI 的片选 CSSCK 接 P1.0MOSI 接 P1.1MISO 接 P1.2RST 接 P1.3。IRQ 引脚在这个项目中不接轮询方式已经足够。RC522 引脚STC89C52 引脚SPI 功能备注SDAP2.0片选 CS低电平有效SCKP1.0时钟空闲低电平MOSIP1.1主机输出单片机写数据MISOP1.2主机输入单片机读数据RSTP1.3复位低电平复位IRQ不接中断请求悬空或接 P3.2 均可接好线后先测量 RC522 模块上的 3.3V 引脚电压确认稳压输出正常再上电操作卡片。2.2 RC522 初始化寄存器配置MFRC522 上电默认处于软复位后的不确定状态必须先通过 CommandReg 写入软复位命令再配置定时器、天线和中断使能。这个项目里的PcdReset()和PcdAntennaOn()是初始化入口通常放在Rc522_Init()里。常见初始化序列如下void Rc522_Init(void) { PcdReset(); // 软件复位 RC522 PcdWrite(ModeReg, 0x3D); // 定义发送和接收的编码模式 PcdWrite(TxModeReg, 0x00); // 设置发送模式默认编码 PcdWrite(RxModeReg, 0x00); // 设置接收模式 PcdWrite(TxControlReg, 0x94); // 开启发送器内部调制器使能 PcdWrite(TModeReg, 0x0D); // 定时器模式自动启动定时器 PcdWrite(TPrescalerReg, 0x3E); // 定时器分频值决定通信波特率 PcdWrite(TReloadReg, 0x1E); // 重装载低字节 PcdWrite(TReloadReg 1, 0x00); // 重装载高字节 PcdAntennaOn(); // 打开天线等待寻卡 }TModeReg、TPrescalerReg、TReloadReg三者共同决定 RC522 内部定时器的溢出时间直接影响与卡片通信的等待超时。上述0x0D、0x3E、0x1E00是 MFRC522 数据手册常用推荐值配合 13.56MHz 晶振可得到约 25ms 的接收超时窗口足够覆盖卡片防碰撞和认证流程。如果通信环境干扰大可以把TReloadReg调大到0x3E00延长等待窗口。2.3 SPI 底层读写函数RC522 的 SPI 模式固定为模式 0即 CPOL0、CPHA0时钟空闲为低电平数据在上升沿采样。读寄存器时主机先发送地址 | 0x80再读取一个字节写寄存器时发送地址后跟数据。unsigned char PcdRead(unsigned char addr) { unsigned char value; CS_LOW(); // 片选拉低开始传输 SPI_WriteByte(addr 0x7E); // 读命令最高位置1地址左移一位 value SPI_ReadByte(); // 读取寄存器内容 CS_HIGH(); // 片选拉高结束传输 return value; } void PcdWrite(unsigned char addr, unsigned char data) { CS_LOW(); SPI_WriteByte(addr 0x7E); // 写命令最高位为0 SPI_WriteByte(data); CS_HIGH(); }SPI_WriteByte在 STC89C52 上通常用软件模拟位操作实现。STC 单片机没有硬件 SPI也可以使用但要注意时钟翻转频率不要超过 RC522 的 10MHz 上限普通延时方式即可。地址左移一位是因为 RC522 的命令帧格式中地址位放在低 7 位且支持连续地址访问不用的最高位要清零。3. M1 卡读写从防碰撞、密码认证到块读取的完整状态流3.1 M1 卡扇区结构与访问控制位M1 卡即 Mifare Classic 1K内部有 16 个扇区编号 0 到 15每个扇区包含 4 个块每块 16 字节。第 0 扇区第 0 块是厂商代码区出厂时写入唯一 UID通常不可修改。扇区内块 3 是扇区尾部保存密钥 A、访问控制位、密钥 B一般情况下不要在读写数据时向块 3 写入内容否则可能把密钥覆盖导致整卡报废。扇区号块号主要用途0块 0厂商代码 UID0块 1、2用户数据区0块 3KeyA 访问控制 KeyB1~15块 0~2用户数据区1~15块 3密钥与访问控制位访问控制位由块 3 的第 6~9 字节组成默认值FF 07 80 69表示允许用 KeyA 或 KeyB 执行读、写、认证。只要用户不主动改写块 3大部分门禁场景下用默认密钥0xFFFFFFFFFFFF即可完成读写。3.2 寻卡与防碰撞的状态变换RC522 寻卡时PcdRequest 发送PCD_REQUEST命令字0x26进入防碰撞循环。PcdAnticoll 会连续读回 5 字节数据前 4 字节是卡号 UID第 5 字节是校验位。这个过程对应 ISO14443A 的 Type A 短帧格式卡片检测必须在卡片进入射频场并完成ATQA 应答后才能发生。unsigned char PcdRequest(unsigned char req_code, unsigned char *pTagType) { unsigned char status; unsigned char len; unsigned char ucComMF522Buf[MAXRLEN]; unsigned char x, y; memset(ucComMF522Buf, 0, sizeof(ucComMF522Buf)); ucComMF522Buf[0] req_code; status PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 1, ucComMF522Buf, len); if ((status MI_OK) (len 2)) { x ucComMF522Buf[0]; y ucComMF522Buf[1]; *pTagType (y 8) | x; // 返回卡片类型字 ATQA } return status; }PcdComMF522是 RC522 收发命令的统一入口第一个参数PCD_TRANSCEIVE表示发送并接收第二个参数是发送缓冲区第三个是发送长度第四个是接收缓冲区第五个是接收长度指针。这里卡片对寻卡请求返回的ATQA通常为0x0400若返回0x4400则可能是非标 M1 卡或卡片处于损坏状态。防碰撞函数PcdAnticoll需要连续读取 4 次串行数据帧格式先发送0x93级联级 1再发位计数和 UID 校验。时序要求比较严如果这里出现MI_NOTAGERR多半是天线路过长或供电不足不是代码逻辑问题。3.3 密钥认证的三种类型M1 卡的认证命令PcdAuthState需要指定认证模式PICC_AUTHENT1A、PICC_AUTHENT1B分别表示使用密钥 A 或密钥 B。STC89C52 源码中默认使用PICC_AUTHENT1A因为绝大多数卡片出厂 KeyA 是可读的。unsigned char PcdAuthState(unsigned char auth_mode, unsigned char addr, unsigned char *pKey, unsigned char *pSnr) { unsigned char status; unsigned char len; unsigned char i; unsigned char ucComMF522Buf[MAXRLEN]; ucComMF522Buf[0] auth_mode; // 0x60 表示 KeyA0x61 表示 KeyB ucComMF522Buf[1] addr; // 目标块地址 for (i 0; i 6; i) { ucComMF522Buf[2 i] pKey[i]; // 复制 6 字节密钥 } for (i 0; i 4; i) { ucComMF522Buf[8 i] pSnr[i]; // 4 字节 UID } status PcdComMF522(PCD_AUTHENT, ucComMF522Buf, 12, ucComMF522Buf, len); if ((status ! MI_OK) || (!(PcdRead(Status2Reg) 0x08))) { status MI_ERR; // 认证失败或内部状态寄存器未置位 } return status; }认证失败时Status2Reg的第 3 位MFInMark不会置 1此时不能对扇区做任何读取操作。项目源码里这个判断很关键直接过滤了密钥错误和地址越界两类问题。如果需要更换密钥先认证当前 KeyA再向块 3 写入新密钥和访问控制位此时必须使用PcdWrite按 16 字节整块写入不能只写前 6 字节。3.4 读块数据与写块数据的边界条件M1 卡读写以块为单位每块 16 字节写操作PcdWrite内部是先发0xA0命令字再发块地址最后发送 16 字节数据。RC522 会对数据做 CRC 校验因此写函数必须一次性把整个缓冲区补齐 16 字节。unsigned char PcdWrite(unsigned char addr, unsigned char *pData) { unsigned char status; unsigned char len; unsigned char i; unsigned char ucComMF522Buf[MAXRLEN]; memset(ucComMF522Buf, 0, sizeof(ucComMF522Buf)); ucComMF522Buf[0] PCD_WRITE; // 0xA0 ucComMF522Buf[1] addr; for (i 0; i 16; i) { ucComMF522Buf[2 i] pData[i]; // 写入完整 16 字节 } status PcdComMF522(PCD_TRANSCEIVE, ucComMF522Buf, 18, ucComMF522Buf, len); if ((status ! MI_OK) || (len ! 1) || (ucComMF522Buf[0] ! 0x0A)) { status MI_ERR; // 卡片返回 0x0A 表示写入成功 } return status; }这里注意卡片对写入成功的回执是长度 1 且数据位0x0A而不是普通数据传输的 2 字节应答。工程里经常有人把0x0A误认成返回数据导致只看MI_OK而忽略内容校验。实际项目中建议把PcdRead读出的块数据与写前原始数据做一次memcmp对比确认落盘后再返回给上层业务。4. 源码级拆解MAIN.C 状态机、MFRC522.H 命令字及显示驱动适配4.1 MAIN.C 中的按键与卡片检测状态机这个源码包的 MAIN.C 不是简单顺序执行而是用了一个状态机轮询上电初始化、等待按键、循环寻卡、执行读写、显示结果。状态机的核心价值在于把寻卡、防碰撞、认证、读写串成一步避免卡片离开射频场后程序还在等待收数。void Card_Process(void) { unsigned char status; unsigned char card_id[4]; unsigned char rd_buf[16]; unsigned char key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; status PcdRequest(0x52, card_id); // 0x52 寻所有卡片 if (status ! MI_OK) return; status PcdAnticoll(card_id); // 取 UID if (status ! MI_OK) return; status PcdAuthState(PICC_AUTHENT1A, 4, key, card_id); // 认证扇区1块4 if (status ! MI_OK) return; status PcdRead(4, rd_buf); // 读取 16 字节 if (status ! MI_OK) return; ShowCardData(rd_buf); // 送到 LCD 或串口显示 }PcdRequest的请求码0x52表示寻卡范围内所有卡片0x26表示只找单张卡。多卡同时进入射频场时0x26或0x52都可能触发碰撞因此PcdAnticoll返回的 UID 需要在业务层再做一次去重防止同一张卡被连续计数。4.2 MFRC522.H 中命令字与返回码定义MFRC522.H 的作用是把所有寄存器地址和命令字集中管理避免魔法数散落各处。源码里常见的命令字见下表实际移植到其他单片机时只需改动CS_LOW、CS_HIGH、SPI_WriteByte三个底层宏上层协议代码可以原样保留。宏定义数值用途PCD_IDLE0x00关闭当前命令PCD_AUTHENT0x0E密钥认证PCD_RECEIVE0x08接收数据PCD_TRANSCEIVE0x0C发送并接收PCD_RESETPHASE0x0F复位PCD_CALCCRC0x03计算 CRC返回码集中在MI_OK0、MI_NOTAGERR1、MI_ERR2三个值上。RC522 数据手册把错误原因细分为 CRC 错误、奇偶校验错、缓冲区溢出等但项目里统一收敛为MI_ERR更利于业务判断。如果要做精细排错可以拆开ErrorReg寄存器的第 2~6 位分别对应WrErr、TempErr、BufferOvfl、CollErr、CRCErr和ParityErr。4.3 1602 与 12864 显示驱动的切换点源码包里同时出现了 1602.c 和 12864.c说明作者把显示层和 RFID 层做了隔离。两者共用ShowCardData这类接口函数内部实现不同。移植到 12864 时注意 12864 的LCD_WriteCmd和LCD_WriteData需要区分中英文显示模式而 1602 的写地址命令是0x80 列地址12864 是0x80 行列偏移。建议在show_cmd和show_data函数上各做一层薄封装未来换屏只改这两个函数。#ifdef LCD_1602 #define LCD_CMD_ROW0 0x80 #define LCD_CMD_ROW1 0xC0 #elif defined(LCD_12864) #define LCD_CMD_ROW0 0x80 #define LCD_CMD_ROW1 0x90 #endif void ShowCardData(unsigned char *buf) { unsigned char i; LCD_WriteCmd(LCD_CMD_ROW0); for (i 0; i 4; i) { LCD_WriteData(BufToHex(buf[i])); } LCD_WriteCmd(LCD_CMD_ROW1); LCD_WriteString(CARD OK); }BufToHex把 16 进制字节拆成两个字符显示避免 LCD 把不可见字符直接刷成乱码。这个函数在调试 M1 卡数据时非常实用读取到的扇区内容很多是0x00或非 ASCII 码直接字符串输出会什么都看不见。5. 用串口打印替换 LCD 调试快速验证 RC522 读写 M1 卡的每一步5.1 串口初始化与 printf 重定向调试这套源码最省事的方式是接一个 USB 转 TTL 到 STC89C52 的 P3.0 和 P3.1把printf定向到串口 1。STC89C52 的波特率由定时器 1 产生常见配置是 12MHz 晶振下使用 9600 波特率重装载值TH1 0xFD。void Uart_Init(void) { SCON 0x50; // 8 位可变波特率允许接收 TMOD 0x20; // 定时器 1 工作在方式 2自动重装 TH1 0xFD; // 9600 波特率 TL1 0xFD; TR1 1; TI 1; // 置位 TI准备发送 } void Uart_SendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; } char putchar(char c) // Keil C51 重定向 printf 到串口 { Uart_SendByte(c); return c; }TI 1必须在printf之前置位否则第一次调用printf时会因为TI0进入死循环。这是 Keil C51 标准库的已知行为很多新人在调试串口时卡在这一行。5.2 把返回码打印成可读的错误码在Card_Process的每一步后面加上状态码打印可以精确区分卡未放好、密钥错误、写入失败三种情况。不要只打印数值最好用映射表转成文字。const char *ErrMsg(unsigned char code) { if (code MI_OK) return OK; if (code MI_NOTAGERR) return NO TAG; return ERR; } printf(Req:%s , ErrMsg(status)); printf(Auth:%s\r\n, ErrMsg(auth_status));配合 5.1 的串口初始化冷启动后放一张卡到 RC522 天线上应该看到Req:OK、Anticoll:OK、Auth:OK、Read:OK。如果Req一直返回NO TAG重点查天线电路和TxControlReg是否使能如果Auth失败检查密钥数组是否 6 字节完备以及目标地址是否落在扇区尾部块 3 上。5.3 扩展多扇区轮询读卡与掉电唤醒验证单块读写无误后可以把PcdRead的地址从固定值改成循环变量依次读取某扇区内的 3 个数据块。注意跨扇区时必须重新认证因为每个扇区的密钥空间是独立的。RC522 的CommandReg写入0x00进入空闲态后可以把 STC89C52 置为掉电模式通过外部中断唤醒重新寻卡这套代码稍加改造就能用在低功耗刷卡门锁上。最后记得在写操作后回读一遍数据并与原值比较这一条比任何协议细节都更能保证实际项目可靠性。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。