简介本资源是基于STM32F407微控制器与MFRC522 RFID模块的完整嵌入式项目面向嵌入式初学者及STM32 HAL库实践者解决RFID非接触识别系统从硬件驱动到业务逻辑的一体化开发问题适用于门禁、考勤、智能卡读写等典型应用场景。压缩包共1552个文件含949个C源码含HAL底层驱动与RFID核心逻辑、269个头文件定义寄存器映射与API接口、125个文本说明含配置说明与协议注释以及Keil MDK-ARM工程文件.uvprojx、.ioc、.sct等整体大小18.64MB。已有117人学习下载。项目结构清晰划分为DriversRC522 SPI通信驱动与MFRC522指令封装、Core标签检测、UID读取、防冲突处理等主流程和MDK-ARM可直接编译调试的完整工程并集成STM32F4系列HAL库全部外设驱动如hal_i2c、hal_tim、hal_rcc_ex等开箱即用大幅降低RFID硬件接入门槛。1. 项目概述当STM32 HAL库遇上RC522 RFID模块如果你手头有一块STM32开发板和一个RC522 RFID模块想快速实现一个刷卡门禁或者简单的身份识别系统那么这篇文章就是为你准备的。RC522作为一款经典且廉价的13.56MHz RFID读写模块在电子DIY、物联网节点、智能门锁等场景中应用广泛。而STM32 HAL库作为ST官方主推的硬件抽象层库以其跨STM32系列的良好兼容性和相对标准库更简洁的初始化流程成为了许多新项目特别是使用STM32CubeMX工具生成代码项目的首选。这个项目的核心就是打通STM32通过HAL库与RC522模块之间的通信链路实现寻卡、防冲突、选卡、读卡、写卡等一系列标准操作。听起来像是简单的SPI驱动实际操作中你会遇到时序要求、寄存器配置、MIFARE Classic 1K卡片的通信协议等一系列细节问题。我花了些时间把HAL库驱动RC522的完整流程、关键代码和踩过的坑都梳理了出来目标是让你拿到代码就能跑起来并且理解每一步背后的逻辑而不仅仅是复制粘贴。2. 核心硬件与通信原理拆解2.1 RC522模块与MIFARE卡片简介RC522是一个高度集成的非接触式读写芯片支持ISO/IEC 14443 A/MIFARE通信协议。我们最常见的那种白色小圆卡或钥匙扣卡很多都是MIFARE Classic 1KS50卡片。它内部有1KB的EEPROM存储空间分为16个扇区Sector 0~15每个扇区有4个块Block 0~3共64字节。每个扇区的最后一个块即Block 3是密钥和控制块存放着该扇区的A密钥、B密钥和存取控制位。其他三个块是数据块。这里有个至关重要的概念每张卡都有一个全球唯一的4字节或7字节序列号UID。我们常说的“读卡”第一步读取的就是这个UID。但MIFARE Classic卡片的安全性在今天看来已经非常脆弱其加密算法已被破解所以它更适合用于门禁、考勤等对安全性要求不高的场合而不应用于支付等场景。RC522与主控STM32通常通过三种方式连接SPI、I2C或UART。绝大多数情况下我们使用SPI接口因为它速度最快通信也最稳定。模块上通常会标明MOSI、MISO、SCK、NSS片选、GND、VCC3.3V和RST复位引脚。2.2 SPI通信时序与HAL库适配要点RC522的SPI通信模式为模式0CPOL0 CPHA0。即时钟空闲时为低电平数据在时钟的第一个边沿上升沿采样。STM32的HAL库SPI配置需要与此匹配。使用HAL库操作SPI外设核心是HAL_SPI_TransmitReceive函数。但RC522的读写时序有一个特殊规定地址字节的最高位bit7决定了操作类型。读操作时bit71写操作时bit70。bit61。所以读一个寄存器的地址实际是(addr 1) | 0x80写一个寄存器的地址是(addr 1) 0x7E。很多初学者驱动失败问题就出在这个地址转换上。HAL库本身不关心这个它只负责把你要发送的数据缓冲区通过MOSI线发出去同时把MISO线上收到的数据存到接收缓冲区。所以我们需要自己构造符合RC522协议的通信帧。注意STM32的SPI时钟频率不宜设置过高。RC522的典型SPI时钟最高可达10MHz但为了稳定起见特别是在面包板或杜邦线连接时建议初始设置为1-2MHz。过高的速率可能导致通信错误表现为读卡不稳定或完全无法识别。3. 基于STM32CubeMX的工程搭建与底层驱动实现3.1 硬件连接与CubeMX配置假设你使用的是STM32F103C8T6Blue Pill这类常见核心板连接方式如下RC522 VCC - 3.3VRC522 GND - GNDRC522 RST - 连接到STM32任意GPIO如PA1RC522 NSSSDA - 连接到STM32的SPI NSS引脚或任意GPIO如PA4软件片选更灵活RC522 SCK - PA5 (SPI1_SCK)RC522 MOSI - PA7 (SPI1_MOSI)RC522 MISO - PA6 (SPI1_MISO)打开STM32CubeMX选择你的芯片型号。配置SPI1模式为“Full-Duplex Master”硬件NSS选择“Disable”我们将使用软件控制GPIO作为片选。参数设置Baud Rate预设分频到1MHz左右如8分频在72MHz系统时钟下为9MHzClock Polarity为“Low” Clock Phase为“1 Edge”。配置GPIO将连接RC522 RST和NSS的引脚如PA1和PA4配置为“GPIO_Output”。配置时钟树确保系统时钟正确例如HSE 8MHz PLL到72MHz。生成代码指定你的IDEKeil MDK或STM32CubeIDE生成初始化代码。3.2 RC522底层驱动函数封装生成的工程中我们在Core/Src目录下新建一个rc522.c和rc522.h文件。底层驱动核心是SPI读写函数。// rc522.h 部分关键定义 #define RC522_SPI_HANDLE hspi1 #define RC522_NSS_PIN GPIO_PIN_4 #define RC522_NSS_PORT GPIOA #define RC522_RST_PIN GPIO_PIN_1 #define RC522_RST_PORT GPIOA #define RC522_READ_CMD 0x80 #define RC522_WRITE_CMD 0x00 // 函数声明 void RC522_Init(void); uint8_t RC522_ReadReg(uint8_t reg); void RC522_WriteReg(uint8_t reg, uint8_t value); void RC522_SetBitMask(uint8_t reg, uint8_t mask); void RC522_ClearBitMask(uint8_t reg, uint8_t mask); uint8_t RC522_Request(uint8_t reqMode, uint8_t *TagType); uint8_t RC522_Anticoll(uint8_t *serNum); void RC522_CalculateCRC(uint8_t *dataIn, uint8_t len, uint8_t *dataOut); uint8_t RC522_ToCard(uint8_t cmd, uint8_t *sendData, uint8_t sendLen, uint8_t *backData, uint16_t *backLen); uint8_t RC522_Auth(uint8_t authMode, uint8_t blockAddr, uint8_t *sectorKey, uint8_t *serNum); uint8_t RC522_ReadBlock(uint8_t blockAddr, uint8_t *recvData); uint8_t RC522_WriteBlock(uint8_t blockAddr, uint8_t *writeData);// rc522.c 核心SPI读写实现 static void RC522_CS_Low(void) { HAL_GPIO_WritePin(RC522_NSS_PORT, RC522_NSS_PIN, GPIO_PIN_RESET); } static void RC522_CS_High(void) { HAL_GPIO_WritePin(RC522_NSS_PORT, RC522_NSS_PIN, GPIO_PIN_SET); } uint8_t RC522_ReadReg(uint8_t reg) { uint8_t addr ((reg 1) 0x7E) | RC522_READ_CMD; // 构造读地址 uint8_t value 0; uint8_t txBuf[2] {addr, 0x00}; uint8_t rxBuf[2] {0}; RC522_CS_Low(); HAL_SPI_TransmitReceive(RC522_SPI_HANDLE, txBuf, rxBuf, 2, 100); RC522_CS_High(); value rxBuf[1]; // 第一个字节是发送的地址第二个字节才是读回的值 return value; } void RC522_WriteReg(uint8_t reg, uint8_t value) { uint8_t addr ((reg 1) 0x7E) | RC522_WRITE_CMD; // 构造写地址 uint8_t txBuf[2] {addr, value}; RC522_CS_Low(); HAL_SPI_Transmit(RC522_SPI_HANDLE, txBuf, 2, 100); RC522_CS_High(); }RC522_Init函数是驱动开始的地方它需要完成一系列寄存器配置让RC522进入工作状态。这个过程通常包括软复位、关闭射频、设置定时器、配置CRC、设置射频增益、天线开启等。网上有很多例程但关键是要理解几个核心寄存器CommandReg (0x01): 用于执行命令如软复位(0x0F)、收发(0x1E)、空闲(0x00)。ComIEnReg (0x02)/DivIEnReg (0x03): 中断使能寄存器我们通常用查询方式可以先不使能中断。ComIrqReg (0x04)/DivIrqReg (0x05): 中断请求寄存器用于判断事件是否发生如定时器中断、发送完成、接收完成等。FIFOLevelReg (0x0A): FIFO数据长度寄存器。ControlReg (0x0C): 控制寄存器用于开启天线(Bit1: Tx1RFEn, Bit2: Tx2RFEn)。BitFramingReg (0x0D): 位帧调节寄存器用于设置发送的最后一个字节的位数。ModeReg (0x11): 模式寄存器用于设置CRC处理器等。TxControlReg (0x14): 发送控制寄存器用于控制天线驱动管脚的输出。一个典型的初始化序列如下void RC522_Init(void) { HAL_GPIO_WritePin(RC522_RST_PORT, RC522_RST_PIN, GPIO_PIN_SET); // 硬件复位引脚拉高 HAL_Delay(50); RC522_WriteReg(CommandReg, 0x0F); // 软复位 HAL_Delay(50); // 等待PowerDown位被清除 while (RC522_ReadReg(CommandReg) (14)); // 设置定时器TModeReg[3:0]0x8D, TPrescalerReg0x3E, TReloadReg_high0x30, TReloadReg_low0x00 RC522_WriteReg(TModeReg, 0x8D); RC522_WriteReg(TPrescalerReg, 0x3E); RC522_WriteReg(TReloadReg_high, 0x30); RC522_WriteReg(TReloadReg_low, 0x00); RC522_WriteReg(TxAutoReg, 0x40); // 调制宽度100%ASK RC522_WriteReg(ModeReg, 0x3D); // 初始化CRC处理器使用CRC预设值0x6363 // 开启天线发送1和2通道 RC522_SetBitMask(TxControlReg, 0x03); }4. 卡片操作流程与核心函数实现4.1 寻卡与防冲突流程操作卡片是一个标准流程寻卡 - 防冲突 - 选卡 - 认证 - 读写。1. 寻卡 (Request)目的是探测天线范围内是否有符合ISO14443A标准的卡片。发送REQA(0x26)或WUPA(0x52)命令。WUPA可以唤醒处于休眠状态的卡片。uint8_t RC522_Request(uint8_t reqMode, uint8_t *TagType) { uint8_t status; uint16_t backBits; RC522_WriteReg(BitFramingReg, 0x07); // 发送7个位 status RC522_ToCard(PCD_TRANSCEIVE, reqMode, 1, TagType, backBits); if ((status ! MI_OK) || (backBits ! 0x10)) { // 返回应为16位(2字节) status MI_ERR; } return status; }RC522_ToCard是核心通信函数它负责将命令和数据通过RC522发送出去并接收返回。2. 防冲突 (Anticollision)如果有多张卡在天线范围内需要通过防冲突机制选出一张进行后续操作。防冲突循环基于卡的UID。对于MIFARE Classic 1K我们通常获取其4字节UID。uint8_t RC522_Anticoll(uint8_t *serNum) { uint8_t status; uint8_t i; uint8_t serNumCheck 0; uint16_t unLen; RC522_WriteReg(BitFramingReg, 0x00); // 清除BitFramingReg serNum[0] PICC_ANTICOLL; serNum[1] 0x20; // 防冲突级别 status RC522_ToCard(PCD_TRANSCEIVE, serNum, 2, serNum, unLen); if (status MI_OK) { // 验证接收到的UID的异或校验位 for (i 0; i 4; i) { serNumCheck ^ serNum[i]; } if (serNumCheck ! serNum[i]) { status MI_ERR; } } return status; }成功执行后serNum数组的前4个字节就是卡的UID第5个字节是前4个字节的异或校验值。3. 选卡 (Select Tag)选定特定UID的卡片激活它以便进行后续的认证和读写操作。选卡命令需要发送完整的UID含校验字节。uint8_t RC522_SelectTag(uint8_t *serNum) { uint8_t i; uint8_t status; uint8_t size; uint16_t recvBits; uint8_t buffer[9]; buffer[0] PICC_SELECTTAG; buffer[1] 0x70; for (i 0; i 5; i) { buffer[i2] serNum[i]; } RC522_CalculateCRC(buffer, 7, buffer[7]); // 计算CRC status RC522_ToCard(PCD_TRANSCEIVE, buffer, 9, buffer, recvBits); if ((status MI_OK) (recvBits 0x18)) { // 成功返回0x18(24)位 size buffer[0]; } else { size 0; } return size; // 返回卡片容量类型0表示失败 }4.2 扇区认证与数据块读写在读写数据块之前必须对目标扇区进行认证。MIFARE Classic卡片使用三轮认证3-Pass Authentication。1. 认证 (Authentication)需要指定认证模式A密钥0x60或B密钥0x61、块地址、扇区密钥6字节和卡片UID4字节。uint8_t RC522_Auth(uint8_t authMode, uint8_t blockAddr, uint8_t *sectorKey, uint8_t *serNum) { uint8_t status; uint16_t recvBits; uint8_t i; uint8_t buff[12]; buff[0] authMode; buff[1] blockAddr; for (i 0; i 6; i) { buff[i2] sectorKey[i]; } for (i 0; i 4; i) { buff[i8] serNum[i]; } status RC522_ToCard(PCD_AUTHENT, buff, 12, buff, recvBits); if ((status ! MI_OK) || (!(RC522_ReadReg(Status2Reg) 0x08))) { // 检查Status2Reg的Auth位 status MI_ERR; } return status; }重要提示块地址blockAddr是绝对地址0-63。例如你要操作第1扇区Sector 1的第0块Block 0其绝对地址是4。因为每个扇区有4个块所以绝对地址 扇区号 * 4 块号。认证时必须使用该扇区内任意一个块的地址通常是你要读写的那个块的地址。2. 读块 (Read)认证成功后就可以读取一个块的数据16字节。uint8_t RC522_ReadBlock(uint8_t blockAddr, uint8_t *recvData) { uint8_t status; uint16_t unLen; recvData[0] PICC_READ; recvData[1] blockAddr; RC522_CalculateCRC(recvData, 2, recvData[2]); status RC522_ToCard(PCD_TRANSCEIVE, recvData, 4, recvData, unLen); if ((status ! MI_OK) || (unLen ! 0x90)) { // 成功返回144位(16字节数据2字节CRC) status MI_ERR; } return status; }3. 写块 (Write)写块操作需要先发送命令和地址然后发送16字节数据。uint8_t RC522_WriteBlock(uint8_t blockAddr, uint8_t *writeData) { uint8_t status; uint16_t recvBits; uint8_t i; uint8_t buff[18]; buff[0] PICC_WRITE; buff[1] blockAddr; RC522_CalculateCRC(buff, 2, buff[2]); status RC522_ToCard(PCD_TRANSCEIVE, buff, 4, buff, recvBits); if ((status ! MI_OK) || (recvBits ! 4) || ((buff[0] 0x0F) ! 0x0A)) { // 应答应为0x0A status MI_ERR; } if (status MI_OK) { // 发送16字节数据 for (i 0; i 16; i) { buff[i] writeData[i]; } RC522_CalculateCRC(buff, 16, buff[16]); status RC522_ToCard(PCD_TRANSCEIVE, buff, 18, buff, recvBits); if ((status ! MI_OK) || (recvBits ! 4) || ((buff[0] 0x0F) ! 0x0A)) { status MI_ERR; } } return status; }警告绝对不要轻易写入扇区的尾块Block 3, 7, 11...即每个扇区的第3块。这个块存放着密钥和访问控制位。如果写错了访问控制位可能导致整个扇区永久锁死无法再读写。在测试阶段建议只读写扇区的数据块Block 0, 1, 2。5. 主程序逻辑与功能整合有了底层驱动和卡片操作函数主程序的逻辑就清晰了。通常我们会将读卡流程封装成一个函数在主循环中周期性地调用它。// main.c 示例 uint8_t g_ucTempbuf[20]; uint8_t g_ucArray[16]; uint8_t g_ucStatus; uint8_t g_ucSN[4]; // 存储卡片UID int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // 初始化串口用于调试输出 MX_USART1_UART_Init(); printf(RC522 Test Start...\r\n); RC522_Init(); printf(RC522 Init OK.\r\n); while (1) { // 1. 寻卡 g_ucStatus RC522_Request(PICC_REQIDL, g_ucTempbuf); if (g_ucStatus MI_OK) { printf(Card Detected.\r\n); // 2. 防冲突获取UID g_ucStatus RC522_Anticoll(g_ucTempbuf); if (g_ucStatus MI_OK) { memcpy(g_ucSN, g_ucTempbuf, 4); // 保存UID printf(UID: %02X %02X %02X %02X\r\n, g_ucSN[0], g_ucSN[1], g_ucSN[2], g_ucSN[3]); // 3. 选卡 if (RC522_SelectTag(g_ucTempbuf) ! 0) { printf(Select Tag OK.\r\n); // 4. 认证以第0扇区A密钥为例默认密钥通常是6个0xFF uint8_t defaultKey[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; g_ucStatus RC522_Auth(PICC_AUTHENT1A, 4, defaultKey, g_ucSN); // 认证第0扇区Block 0 if (g_ucStatus MI_OK) { printf(Auth OK.\r\n); // 5. 读块读第0扇区Block 0 g_ucStatus RC522_ReadBlock(0, g_ucArray); if (g_ucStatus MI_OK) { printf(Block 0 Data: ); for (int i0; i16; i) { printf(%02X , g_ucArray[i]); } printf(\r\n); } else { printf(Read Block Failed.\r\n); } // 6. 写块示例谨慎操作 // uint8_t writeData[16] Hello, STM32!; // g_ucStatus RC522_WriteBlock(1, writeData); // 写Block 1 // if (g_ucStatus MI_OK) printf(Write OK.\r\n); // 7. 休眠卡片 RC522_Halt(); } else { printf(Auth Failed. Check Key.\r\n); } } } } HAL_Delay(200); // 延时避免过于频繁寻卡 } }6. 调试技巧与常见问题排查在实际焊接和调试中你几乎一定会遇到问题。下面是我总结的几个常见坑点和排查思路。6.1 硬件连接与电源问题现象完全无法通信读寄存器返回全是0xFF或0x00。排查电压确认首先用万用表测量RC522模块的VCC引脚确保是稳定的3.3V。有些模块标注5V容忍但STM32的IO是3.3V电平最好统一用3.3V供电和通信。线序检查这是最常出错的地方。务必确认MOSI接MOSIMISO接MISO而不是交叉连接。SPI是主出从入MOSI、主入从出MISO。接触不良杜邦线连接非常容易接触不良。尝试按压连接处或者直接焊接。如果使用面包板检查面包板内部簧片是否老化。上拉电阻RC522的RST引脚内部可能有弱上拉但为了稳定建议在STM32端通过软件设置为上拉输出或者外部接一个4.7k-10k电阻上拉到3.3V。6.2 软件配置与通信问题现象能读到RC522的版本号读VersionReg寄存器应为0x92但无法寻到卡片。排查SPI模式与速率反复检查CubeMX中SPI的CPOL和CPHA设置必须为0和0。尝试降低SPI波特率如降到500kHz或更低。片选时序确保RC522_CS的拉低和拉高时机正确。在RC522_ReadReg和RC522_WriteReg函数中片选信号应包裹整个SPI传输过程。可以在这些函数前后加HAL_Delay(1)来放慢时序观察。天线检查RC522模块上的天线线圈是否完好没有短路或断路。模块应远离大的金属物体。初始化序列对比你的RC522_Init函数和成熟的初始化代码确认关键寄存器如TxControlReg的配置是否正确开启了天线驱动。中断标志清除在RC522_ToCard等函数中发送命令后需要等待RC522的IRqReg寄存器中的相应标志位如TxIRq,RxIRq置位并在处理完成后清除它们。很多驱动代码漏了清除标志位导致后续操作异常。6.3 卡片操作逻辑问题现象能寻到卡读到UID但认证失败或读写失败。排查密钥错误这是最常见的原因。MIFARE Classic卡的出厂默认密钥A是6个0xFF即0xFF,0xFF,0xFF,0xFF,0xFF,0xFF。但有些卡片可能被改过密钥。如果你不知道密钥可以使用mfoc等工具在PC上尝试破解。在代码中确保你传递给RC522_Auth函数的密钥数组是正确的。块地址计算错误再次强调认证和读写函数使用的blockAddr是绝对地址0-63。认证第1扇区第0块时地址是4。如果你误传了0实际上是在尝试认证第0扇区的尾块控制块这几乎肯定会失败。未先选卡确保在执行RC522_Auth之前成功执行了RC522_SelectTag。卡片未休眠/停止一次完整的操作后特别是读写后应调用RC522_Halt()函数让卡片进入休眠状态。否则卡片可能一直处于激活状态影响下一次寻卡或导致状态混乱。缓冲区污染确保你的数据缓冲区在每次使用前都被正确初始化或清空。残留的数据可能干扰CRC计算或命令解析。6.4 稳定性优化建议增加超时机制在RC522_ToCard函数中等待RC522中断标志时应增加超时判断避免因卡片离开或干扰导致程序死等。CRC校验RC522支持硬件CRC初始化时通过ModeReg寄存器开启。确保在发送需要CRC的命令如选卡、读写前正确调用了RC522_CalculateCRC函数。错误重试在主循环读卡流程中对于MI_ERR的返回值可以加入有限次数的重试机制而不是立即放弃以提高抗干扰能力。降低寻卡频率在主循环中每次寻卡之间加入足够的延时如100-500ms。过于频繁的射频信号发射可能干扰卡片响应也增加功耗。7. 项目进阶与扩展思路一个基础的读卡器做完了但这只是开始。基于这个稳定的驱动你可以做很多有意思的扩展1. 多卡片管理实现一个简单的卡片UID白名单或黑名单系统。将授权卡的UID预先存储在STM32的Flash或外部EEPROM中。读卡后将获取的UID与名单比对匹配则点亮绿灯并执行操作如控制继电器模拟开门不匹配则亮红灯报警。2. 数据存储与读写利用MIFARE Classic 1K的1KB空间你可以设计一些简单的数据存储结构。例如在扇区0的块1和块2存储用户姓名、编号的ASCII码。实现一个简单的“电子钱包”逻辑在某个数据块中存储余额数值通过读写操作进行“充值”和“消费”注意这毫无安全性可言仅用于演示逻辑。将卡片作为设备配置卡刷卡后将特定扇区中的数据读出来用于配置设备的参数如Wi-Fi密码、工作模式。3. 与上位机通信通过STM32的UART、USB CDC或以太网接口将读到的卡片UID、数据块内容实时上传到电脑或服务器。你可以用Python写一个简单的上位机程序实现刷卡记录、权限管理等功能构建一个完整的离线或在线门禁系统原型。4. 低功耗设计如果你的设备是电池供电需要优化功耗。可以在没有刷卡需求时让STM32进入Stop模式同时通过RC522的CommandReg让其进入软掉电模式。然后通过外部中断如按键或定时唤醒后再重新初始化RC522并进行寻卡。这能极大降低待机电流。5. 封装为通用库将上述所有函数良好地组织起来提供清晰的API接口例如typedef struct { uint8_t uid[4]; uint8_t type; } RC522_Card_t; RC522_Status_t RC522_PollForCard(RC522_Card_t *card); RC522_Status_t RC522_ReadCardData(RC522_Card_t *card, uint8_t sector, uint8_t block, uint8_t *key, uint8_t *data); RC522_Status_t RC522_WriteCardData(RC522_Card_t *card, uint8_t sector, uint8_t block, uint8_t *key, uint8_t *data);这样在你的其他项目中只需要包含这个库文件调用几个简单的函数就能完成RFID操作大大提高代码复用率。驱动RC522的过程是一个典型的嵌入式外设开发流程理解芯片手册、配置MCU外设、编写底层通信函数、实现高层应用协议、最后调试和优化。把每一步的原理搞懂遇到的问题记录下来你的嵌入式开发功底就会在解决这些具体问题的过程中稳步提升。本文还有配套的精品资源点击获取