STM32F103 RC522驱动实战:规避引脚冲突的SPI读卡完整方案

发布时间:2026/9/2 16:10:06
STM32F103 RC522驱动实战:规避引脚冲突的SPI读卡完整方案 简介这套基于STM32F103RCT6的RC522读卡程序专为正点原子Mini板设计帮助嵌入式入门者在Mini板上快速实现RFID卡号读取与液晶显示。资源包共82个文件包括C源码、头文件、Keil工程文件uvprojx/uvoptx以及编译生成的hex/axf/map等压缩包约1.7MB结构清晰可直接打开工程对照学习或烧录验证。目前已有1778人学习下载。程序核心围绕SPI2接口展开完整演示了SPI初始化、RC522寄存器配置、天线使能、防碰撞、选卡、读卡与数据解析并驱动LCD显示卡号涉及STM32 GPIO、SPI、定时器与显示外设的协同工作。通过阅读和调试该工程能直观理解RFID通信流程积累嵌入式驱动开发经验适合作为门禁、物流等场景的起步模板。 我这次不是要重复贴一份网上一搜一大把的RC522驱动源码。网上RC522程序确实多到泛滥但真正拿到正点原子Mini板STM32F103RCT6上能一次跑通的我体验下来比例并不高。问题多数不是芯片本身而是出在引脚映射、SPI初始化和模块差异上。这篇东西把我实际调通的整套思路写清楚从硬件接线到寄存器操作再到读卡流程给正准备做门禁、考勤、实验管理、快递柜锁控这类项目的朋友提供一个能直接落地参照的版本。1. 先讲讲这个项目到底要解决什么1.1 网上的RC522程序为什么换个板子就跑不动RC522是NXP的MFRC522衍生方案市面上大量模块用的其实是国产兼容芯片FM17520、FM17522之类指令集和寄存器都兼容所以同一份代码在不同板卡之间主要冲突不在芯片而在板级资源。很多店铺附带的例程是给Arduino或51写的换到STM32上首先要重写底层的SPI读写。而给STM32的例程又常常是针对某一款特定开发板比如正点原子战舰版STM32F103ZET6或野火指南者。这些板子引脚多CS、RST随便挑一个空闲引脚就行但到了Mini板上64脚封装的引脚资源很紧照抄过来往往撞上板载外设或者JTAG功能。还有个很容易踩的坑PB3、PB4、PA15这几个引脚在默认状态是JTAG功能。RC522的例程如果恰好把CS或者RST定义在这些脚上而初始化代码没有执行GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)那GPIO输出是完全无效的。这种现象在Mini板用户里出现频率极高表现为硬件接线看着没问题但模块就是没反应。1.2 Mini板STM32F103RCT6的特殊性Mini板上PA9/PA10被CH340串口占用PB5接了蜂鸣器TFTLCD接口占掉一组GPIO能自由分配给RC522的引脚其实有限。我最终选用的组合是SCK - PA5SPI1_SCKMISO - PA6SPI1_MISOMOSI - PA7SPI1_MOSICS - PA4普通GPIO软件控制RST - PA3普通GPIO软件复位这组引脚完全避开JTAG、串口、LED、蜂鸣器和LCD接口在Mini板上非常干净。即使你已经接了LCD只要不是同时用SPI接口的LCD模块这组引脚就不会冲突。2. 硬件接线与引脚选择避开Mini板上的隐形冲突2.1 RC522模块引脚先认清楚RC522模块常见的排针定义是8个引脚SDA、SCK、MOSI、MISO、IRQ、GND、RST、VCC。不少新手第一次看到SDA会下意识以为这是I2C接口实际上在RC522上SDA就是SPI的从机选择信号NSS本质是片选不是数据线。这个认知误区会让接线阶段就出错。供电方面RC522模块必须接3.3V不能接5V。Mini板上有3.3V输出引脚直接从这里取电即可。有些模块板上自带稳压芯片接5V也能跑但不是所有模块都带而且SPI引脚电平可能变成5V倒灌进STM32的GPIO时间长了有烧引脚的风险。统一用3.3V最稳妥。2.2 推荐接线表与实际注意事项RC522模块引脚接Mini板引脚说明VCC3.3V必须3.3V供电GNDGND共地不能漏SDAPA4软件片选低电平有效SCKPA5SPI1时钟MOSIPA7SPI1主机输出MISOPA6SPI1主机输入RSTPA3软件复位低电平复位IRQ不接轮询方式读卡不需要中断引脚我实际测试时用的是10cm左右的杜邦线工作很稳定。如果线长超过20cm或者杜邦线质量比较差SPI时钟频率就要降下来不然会出现偶发读卡失败。另外RC522的天线区域下方不要垫金属金属会吸收能量读卡距离会明显缩短这个在使用中有很直观的体感差异。2.3 为什么CS和RST用普通GPIO控制RC522支持硬件NSS但实际工程项目中绝大多数例程都选择用普通GPIO模拟片选。原因很简单硬件NSS片选时序一旦和SPI外设配置配合不好读写就全乱用GPIO拉低拉高时序肉眼可见配合逻辑分析仪能很快定位问题。RST引脚用GPIO控制的好处是可以在初始化时做一次干净的硬件复位先拉低、保持几十毫秒、再拉高。RC522模块上电瞬间内部状态不固定软件复位虽能处理大部分情况但配合一次硬件复位会让后续寄存器操作更可靠。3. SPI初始化和RC522寄存器配置决定能不能通的细节3.1 SPI时钟、极性和相位怎么选RC522的SPI接口最高支持10MHz时钟但这是理想状态。实际用杜邦线连接时我推荐把SPI时钟压在4.5MHz左右。以下是基于标准外设库的初始化代码直接适配STM32F103RCT6void RC522_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* PA4 - CS推挽输出默认拉高 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PA3 - RST推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PA5/PA6/PA7 - SPI1 SCK/MISO/MOSI复用推挽 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_3); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // SPI Mode 0 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; // 72MHz/164.5MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }关于SPI模式RC522数据手册上写的是支持Mode0和Mode3但实际工程中Mode0在STM32上最稳定。如果你换了一份例程发现读寄存器全是0xFF第一个要怀疑的就是CPOL和CPHA组合。3.2 寄存器读写帧格式RC522的SPI帧格式有点特殊。寄存器地址只有7位有效位单片机发送第一个字节时需要把地址左移一位最低位作为读写标志写寄存器最低位为0读寄存器最低位为1。读写和数据传输共用一个16位的SPI事务写寄存器时第一个字节是命令第二个字节是数据读寄存器时第一个字节是命令第二个字节发送任意值同时MISO上返回寄存器内容。/* 写RC522寄存器 */ void RC522_WriteReg(u8 addr, u8 val) { RC522_CS_LOW(); RC522_SPI_ReadWriteByte((addr 1) 0x7E); // 写命令bit00 RC522_SPI_ReadWriteByte(val); RC522_CS_HIGH(); } /* 读RC522寄存器 */ u8 RC522_ReadReg(u8 addr) { u8 val; RC522_CS_LOW(); RC522_SPI_ReadWriteByte(((addr 1) 0x7E) | 0x80); // 读命令bit01 val RC522_SPI_ReadWriteByte(0x00); RC522_CS_HIGH(); return val; } /* SPI收发单字节 */ u8 RC522_SPI_ReadWriteByte(u8 byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }这段代码是整套RC522驱动的地基。很多初学者会把addr 1这一步忽略直接传原始地址然后发现写进去的值读回来不对。原因就是RC522的内部移位寄存器需要地址左移后对齐。3.3 上电初始化流程与天线控制RC522上电后不能直接寻卡必须先完成复位和关键寄存器配置。我用的初始化函数如下void RC522_Init(void) { GPIO_SetBits(GPIOA, GPIO_Pin_3 | GPIO_Pin_4); // RST、CS拉高 RC522_RST_HIGH(); delay_ms(10); RC522_RST_LOW(); delay_ms(10); RC522_RST_HIGH(); delay_ms(10); RC522_WriteReg(CommandReg, 0x0F); // 软件复位 delay_ms(10); RC522_WriteReg(TModeReg, 0x8D); // 定时器模式 RC522_WriteReg(TPrescalerReg, 0x3E); // 分频系数 RC522_WriteReg(TReloadRegL, 0x1E); // 装载值低字节 RC522_WriteReg(TReloadRegH, 0x00); // 装载值高字节 RC522_WriteReg(TxAutoReg, 0x40); // 发送相关配置 RC522_WriteReg(ModeReg, 0x3D); // 定义收发模式 RC522_WriteReg(TxControlReg, 0x83); // 开启天线 }这段代码里有几个寄存器值得多说一句。TModeReg、TPrescalerReg和TReloadReg共同决定了RC522内部定时器的溢出时间也就是寻卡等待的超时值。如果定时器装错会出现寻卡一直返回超时错误。TxControlReg设置为0x83是打开天线发射很多模块初始化后读卡没反应就是这一步没执行或者寄存器值配错。还有一个很实用的验证手段读VersionReg寄存器地址0x37。正版NXP RC522通常返回0x92国产兼容芯片可能返回0x90或其他值。只要返回值不是0xFF就说明SPI通路和芯片本身是正常的。这个特性在排查问题时非常关键。4. 读卡主流程请求、防冲撞、选卡、验证、读写4.1 寻卡与防冲撞拿到卡号和卡类型RC522读M1卡的流程非常固定寻卡Request- 防冲撞Anticollision- 选卡Select- 验证密钥Authentication- 读或写。寻卡的作用是让卡片回应自己的类型。M1 S50卡回应的是4字节ATQA常见值为00 00 04 00M1 S70为00 00 02 00。判断卡类型时需要把这4字节组合起来看不要只看最后一个字节。防冲撞操作会返回卡的4字节UID卡序列号。这个UID才是我们做门禁、考勤时用来识别身份的标识。注意M1卡的UID有4字节和7字节两种普通S50卡是4字节。我下面给出的代码针对的是最常见的4字节UID场景。/* 寻卡 */ u8 RC522_Request(u8 reqMode, u8 *pTagType) { u8 status; u8 n 0; u8 cmd reqMode; RC522_WriteReg(BitFramingReg, 0x07); pTagType[0] cmd; status RC522_ComMF522(PCD_TRANSCEIVE, pTagType, 1, pTagType, n); if (status MI_OK n 2) { return MI_OK; } return MI_ERR; } /* 防冲撞得到UID */ u8 RC522_Anticoll(u8 *pSnr) { u8 status; u8 i; u8 n 0; u8 buf[2] {0}; RC522_WriteReg(BitFramingReg, 0x00); buf[0] PICC_ANTICOLL; // 0x93 buf[1] 0x00; status RC522_ComMF522(PCD_TRANSCEIVE, buf, 2, pSnr, n); if (status MI_OK n 5) { /* 这里可以校验第5字节的校验位实际项目中我一般直接信任 */ return MI_OK; } return MI_ERR; }需要说明的是真正底层收发函数RC522_ComMF522需要处理清FIFO、发命令、等中断、查错误标志、读FIFO数据这些步骤。这段代码在各类MFRC522驱动中几乎是大同小异的我这里不展开全部代码关键是你要理解它的功能是“把一个命令发给RC522然后等待执行结果把收到的数据从FIFO里取出来”。网上工程里直接找一份匹配标准外设库的RC522_ComMF522实现即可上面的寄存器层代码保证你能把它跑通。4.2 选卡与密钥验证拿到UID之后不能直接读块数据。每次读块之前要先选卡再对目标块进行密钥验证。/* 选卡 */ u8 RC522_Select(u8 *pSnr) { u8 status; u8 n 0; u8 buf[6] {0}; buf[0] PICC_SElECTTAG; // 0x93 buf[1] 0x70; buf[2] pSnr[0]; buf[3] pSnr[1]; buf[4] pSnr[2]; buf[5] pSnr[3]; status RC522_ComMF522(PCD_TRANSCEIVE, buf, 6, buf, n); if (status MI_OK n 1) { return MI_OK; } return MI_ERR; } /* 验证密钥addr为绝对块号 */ u8 RC522_Auth(u8 authMode, u8 addr, u8 *pKey, u8 *pSnr) { u8 status; u8 n 0; u8 buf[12] {0}; buf[0] authMode; // PICC_AUTHENT1A: 0x60, PICC_AUTHENT1B: 0x61 buf[1] addr; for (u8 i 0; i 6; i) { buf[2 i] pKey[i]; } for (u8 i 0; i 4; i) { buf[8 i] pSnr[i]; } status RC522_ComMF522(PCD_AUTHENT, buf, 12, buf, n); if (status MI_OK) { return MI_OK; } return MI_ERR; }新买的白卡出厂默认密钥是6个字节的0xFF。很多读卡失败案例都是因为卡片已经被别人改过密钥用默认密钥验证自然过不了。调试初期建议用一张没改过密钥的白卡。4.3 块读写与M1 S50存储结构M1 S50卡有16个扇区每个扇区4个块每个块16字节一共1024字节。扇区的第3块是尾块存放KeyA、访问控制位和KeyB普通数据不要往里写。0扇区的第0块是厂商块出厂固化了UID和厂商数据只读。所以要测试写卡建议用第1块、第2块这类普通数据块。块地址的换算逻辑绝对块号 扇区号 * 4 块内偏移。比如要写扇区1的第2块绝对块号就是1*426。/* 读一个块 */ u8 RC522_Read(u8 addr, u8 *pData) { u8 status; u8 n 0; u8 buf[2] {0}; buf[0] PICC_READ; // 0x30 buf[1] addr; status RC522_ComMF522(PCD_TRANSCEIVE, buf, 2, pData, n); if (status MI_OK n 16) { return MI_OK; } return MI_ERR; } /* 写一个块 */ u8 RC522_Write(u8 addr, u8 *pData) { u8 status; u8 n 0; u8 buf[18] {0}; buf[0] PICC_WRITE; // 0xA0 buf[1] addr; for (u8 i 0; i 16; i) { buf[2 i] pData[i]; } status RC522_ComMF522(PCD_TRANSCEIVE, buf, 18, buf, n); if (status MI_OK n 1 buf[0] 0x0A) { return MI_OK; } return MI_ERR; }读写操作的时序相对较长RC522写一个块大约需要十几毫秒所以主循环里不要频繁地连续读写否则会让整个系统响应变慢。5. 实测排查那些最难缠的问题到底出在哪这个问题清单是我实际调试中遇到最多的按出现频率排序现象原因处理方式读VersionReg返回0xFFSPI引脚接错、模块没供电、SPI模式错误先用万用表确认3.3V和GND再核对SCK/MOSI/MISO三根线最后检查CPOL/CPHAVersionReg返回0x90/0x91/0x92芯片版本不同属于正常现象不要当错误只要能读出非0xFF即可寻卡一直返回超时天线没打开、卡片距离太远、定时器配置错误检查TxControlReg是否为0x83卡片贴近线圈核对TMode/TPrescaler/TReload三个寄存器能寻卡但读到的UID偶尔错一位SPI时钟过高、杜邦线过长、供电不足把分频降到16分频或32分频换短线3.3V单独供电能读UID但Read块返回MI_ERR块地址越界、密钥验证失败、读取的是厂商块确认绝对块号小于64用默认密钥0xFF重新验证测试普通数据块5.1 初始化失败先打印VersionReg再往下走我调试RC522时永远是先读版本号。这个习惯救过很多次。RC522_ReadReg(0x37)如果打印出来是0x92哪怕寻卡暂时失败也说明芯片和SPI链路是通的问题在上层命令流程。如果打印出来是0xFF就先不要查寻卡逻辑回头查接线、供电和SPI模式。这样分段排查半小时就能定位问题省得在错误方向上浪费时间。5.2 寻卡超时不一定是硬件问题有一回我换了一个新买的RC522模块寻卡一直返回超时。用逻辑分析仪看SPI波形命令发出去了MISO也有响应但就是没有卡回复。最后发现是那个模块的天线默认关闭初始化里写了TxControlReg0x83也不够必须再执行一次AntennaOn也就是额外往TxControlReg写入0x83并且让寄存器先回到0x80再置1。这个操作看起来多此一举但对部分国产芯片是必需的。5.3 数据错乱优先怀疑SPI速率正常情况下SPI在4.5MHz下用10cm杜邦线不会出错。但如果你用的是20cm以上的飞线或者把RC522和电机驱动板放在一起MISO线上的干扰会明显增大。现象是大多数时候读卡正常偶尔读出的UID和面板上印的不一致。把SPI分频从8分频降到16分频大部分问题会立刻消失。如果依旧出错检查电源纹波RFID模块在发射瞬间电流变化明显供电不稳会导致逻辑电平抖动。6. 我自己实测后的几个补充技巧6.1 上电时序与延时问题RC522模块上电后不要立刻初始化。我在Mini板上实测VCC稳定之后至少要等100ms再执行复位和寄存器配置否则偶尔会出现版本号读错或者初始化失败。另外每次寻卡命令之间建议间隔20ms以上连续高速轮询会让模块内部状态来不及恢复反而降低识别率。6.2 从读卡到应用一句经验如果你打算做门禁授权最稳妥的做法是把UID读出来以后直接和预设白名单比对不要依赖块数据。因为块数据涉及密钥管理一旦密钥丢失整张卡数据就废了。UID是出厂固化的不可更改部分UID可改卡除外识别稳定性高得多。我实际项目里的判断逻辑就是连续两次读到的UID一致且在白名单内才执行开锁动作这样能过滤掉绝大部分偶发错误。这套程序配合正点原子Mini板我跑了大概一个月每天上百次刷卡测试没有出现死机或卡数据损坏的情况。如果你用的也是Mini板直接按这个引脚和流程来基本可以少走很多弯路。本文还有配套的精品资源点击获取