STM32F103RCT6(HAL库)驱动RC522:从零构建RFID门禁系统核心模块
1. 项目背景与硬件准备最近在做一个智能门禁系统的原型开发选择了STM32F103RCT6作为主控芯片搭配RC522 RFID读写模块。这种组合在门禁考勤系统中非常常见成本低且性能稳定。先说说为什么选这两个器件STM32F103RCT6是ST的经典款72MHz主频够用自带SPI接口正好驱动RC522。RC522模块更是个好东西13.56MHz工作频率支持ISO14443A标准的M1卡淘宝上十块钱就能买到还带天线板。我用的这块开发板是正点原子的MiniSTM32引脚排布规整调试起来特别方便。硬件连接其实很简单七根杜邦线就能搞定PA4(SPI1_NSS) - SDAPA5(SPI1_SCK) - SCKPA7(SPI1_MOSI) - MOSIPA6(SPI1_MISO) - MISOPA3 - RST3.3V - 3.3VGND - GND这里有个坑要注意RC522的IQ引脚不用接悬空就行。第一次做的时候我傻乎乎地把所有引脚都接了结果模块死活不工作查了半天手册才发现这个细节。2. CubeMX配置要点用STM32CubeMX配置SPI接口时建议这样设置SPI模式选全双工主模式时钟分频设到PCLK2的8分频9MHz时钟极性低电平相位第一个边沿数据宽度8bitMSB先行NSS信号选软件控制GPIO配置要注意NSS引脚PA4要手动设为GPIO输出RST引脚PA3同样设为GPIO输出SPI的SCK/MOSI/MISO记得配置复用功能我遇到过SPI时钟速率过高导致通信失败的情况。RC522官方手册说最高支持10MHz但实测发现当STM32超频到128MHz时即使分频后SPI时钟在规格内也会出问题。后来把系统时钟调回72MHz就稳定了。3. RC522驱动开发详解3.1 寄存器操作基础RC522的所有功能都是通过寄存器控制的底层驱动要封装好读写函数。这里分享我的实现// 写寄存器 void MFRC_WriteReg(uint8_t addr, uint8_t data) { uint8_t AddrByte (addr 1) 0x7E; // 地址格式转换 RS522_NSS(0); // 片选使能 HAL_SPI_TransmitReceive(hspi1, AddrByte, ret, 1, 10); HAL_SPI_TransmitReceive(hspi1, data, ret, 1, 10); RS522_NSS(1); // 片选禁用 } // 读寄存器 uint8_t MFRC_ReadReg(uint8_t addr) { uint8_t AddrByte ((addr 1) 0x7E) | 0x80; // 读操作位 uint8_t data 0; RS522_NSS(0); HAL_SPI_TransmitReceive(hspi1, AddrByte, ret, 1, 10); HAL_SPI_TransmitReceive(hspi1, dummy, data, 1, 10); RS522_NSS(1); return data; }调试时发现个有趣现象如果连续快速读写寄存器必须保证NSS信号有足够的高低电平保持时间。我有次在for循环里连续写配置结果只有第一个字节生效了后来在每次操作后加了1us延时就正常了。3.2 模块初始化流程完整的初始化应该包括硬件复位拉低RST引脚至少2us软复位写CommandReg配置定时器参数开启天线void PCD_Reset(void) { // 硬复位 RS522_RST(0); HAL_Delay(2); RS522_RST(1); HAL_Delay(2); // 软复位 MFRC_WriteReg(MFRC_CommandReg, MFRC_RESETPHASE); // 定时器配置 MFRC_WriteReg(MFRC_ModeReg, 0x3D); MFRC_WriteReg(MFRC_TReloadRegL, 30); MFRC_WriteReg(MFRC_TModeReg, 0x8D); MFRC_WriteReg(MFRC_TPrescalerReg, 0x3E); }天线控制也有讲究开关间隔至少要1msvoid PCD_AntennaOn(void) { uint8_t temp MFRC_ReadReg(MFRC_TxControlReg); if (!(temp 0x03)) { MFRC_SetBitMask(MFRC_TxControlReg, 0x03); } }4. RFID卡片操作实战4.1 寻卡与防冲突寻卡使用PCD_Request命令有两种模式PICC_REQIDL只寻找未进入休眠的卡PICC_REQALL寻找所有卡char PCD_Request(uint8_t RequestMode, uint8_t *pCardType) { uint8_t CmdFrameBuf[MFRC_MAXRLEN]; CmdFrameBuf[0] RequestMode; // 发送寻卡命令 char status MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 1, CmdFrameBuf, unLen); if(status PCD_OK unLen 0x10) { pCardType[0] CmdFrameBuf[0]; pCardType[1] CmdFrameBuf[1]; } return status; }防冲突算法是重点RC522内置了防冲突机制我们只需要调用PCD_Anticoll函数就能获得卡片UIDchar PCD_Anticoll(uint8_t *pSnr) { uint8_t CmdFrameBuf[MFRC_MAXRLEN] {PICC_ANTICOLL1, 0x20}; char status MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 2, CmdFrameBuf, unLen); if(status PCD_OK) { for(int i0; i4; i) { pSnr[i] CmdFrameBuf[i]; } } return status; }4.2 密钥认证流程M1卡的每个扇区都有独立的密钥进行读写前必须先认证。典型流程如下char PCD_AuthState(uint8_t AuthMode, uint8_t BlockAddr, uint8_t *pKey, uint8_t *pSnr) { uint8_t CmdFrameBuf[12]; CmdFrameBuf[0] AuthMode; CmdFrameBuf[1] BlockAddr; memcpy(CmdFrameBuf[2], pKey, 6); // 密钥 memcpy(CmdFrameBuf[8], pSnr, 4); // UID char status MFRC_CmdFrame(MFRC_AUTHENT, CmdFrameBuf, 12, CmdFrameBuf, unLen); return status; }注意BlockAddr参数虽然认证是以扇区为单位但参数可以是扇区内的任意块地址。比如要操作第2扇区块8-11传入8、9、10或11都可以。5. 数据读写实现5.1 读块数据读操作使用PICC_READ命令每次读取16字节char PCD_ReadBlock(uint8_t BlockAddr, uint8_t *pData) { uint8_t CmdFrameBuf[4] {PICC_READ, BlockAddr}; MFRC_CalulateCRC(CmdFrameBuf, 2, CmdFrameBuf[2]); char status MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, unLen); if(status PCD_OK unLen 0x90) { memcpy(pData, CmdFrameBuf, 16); } return status; }有个易错点块地址范围是0-63但每个扇区的最后一块如块3、7、11...是控制块存储密钥和访问权限块地址不能越界否则会返回错误5.2 写块数据写操作相对复杂需要先发送写命令再发送数据char PCD_WriteBlock(uint8_t BlockAddr, uint8_t *pData) { uint8_t CmdFrameBuf[18]; CmdFrameBuf[0] PICC_WRITE; CmdFrameBuf[1] BlockAddr; MFRC_CalulateCRC(CmdFrameBuf, 2, CmdFrameBuf[2]); // 发送写命令 char status MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 4, CmdFrameBuf, unLen); if(status PCD_OK) { memcpy(CmdFrameBuf, pData, 16); MFRC_CalulateCRC(CmdFrameBuf, 16, CmdFrameBuf[16]); // 发送数据 status MFRC_CmdFrame(MFRC_TRANSCEIVE, CmdFrameBuf, 18, CmdFrameBuf, unLen); } return status; }重要安全提示写操作前一定要先验证密钥否则会触发芯片的保护机制。我有次忘记验证直接写卡导致一张M1卡被锁死只能报废处理。6. 门禁系统逻辑整合6.1 串口指令控制通过串口可以动态修改写入卡的数据我设计了简单协议指令格式Set_CardData:数据内容;最大支持16字节数据void USART_Handl(void) { if(memcmp(UartData, Set_CardData:, 13) 0) { if(UartRxLen-14 16) { RFID_WritFlag 1; memcpy(RFID_WriteData, UartData[13], UartRxLen-14); printf(准备写入数据:%s\n, RFID_WriteData); } } }6.2 LED状态指示增加两个LED作为状态指示LED0系统运行指示灯1Hz闪烁LED1刷卡状态指示灯刷卡时亮200ms// 在main循环中添加 HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin); HAL_Delay(500); if(cardDetected) { HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); HAL_Delay(200); HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); cardDetected 0; }7. 常见问题排查寻卡失败检查天线连接是否正常测量3.3V电源是否稳定用逻辑分析仪抓SPI波形看时钟和数据是否正常认证总是失败确认使用的密钥与卡片匹配检查UID读取是否正确尝试用默认密钥0xFF 0xFF 0xFF 0xFF 0xFF 0xFF写操作不生效确保认证通过检查块地址是否有效不能是控制块验证写保护位是否被设置通信不稳定缩短SPI线缆长度在3.3V电源加100uF电容降低SPI时钟速率有个特别隐蔽的bug我花了三天才解决当开发板通过USB供电时如果插拔USB线RC522会进入奇怪的状态。后来发现是电源扰动导致的在VCC和GND之间加了10uF钽电容后问题消失。8. 项目优化方向增加多卡管理在Flash中存储授权卡UID列表实现添加/删除卡功能引入EEPROM存储记录刷卡日志保存系统配置参数添加无线功能通过ESP8266联网上报数据支持远程授权管理低功耗优化使用STM32的停机模式间歇性唤醒RC522检测卡片这个项目最让我惊喜的是RC522的稳定性。连续运行一个月处理了上千次刷卡操作没有出现一次误读或漏读。现在这套系统已经用在我工作室的门禁上配合电磁锁工作得非常可靠。