简介:这套RC522读卡程序专为正点原子Mini开发板设计,主控芯片是STM32F103RCT6,代码改写自野火Mini工程。程序通过SPI2接口与MFRC522模块通信,利用串口1把读卡结果和调试信息发送到终端,适合正在学习STM32、SPI总线、串口通信以及RFID应用的开发者参考,稍作修改也可适配其他SPI接口的RFID读卡芯片。压缩包共153个文件,约2.91MB,包含完整工程源码、编译中间文件、Keil工程配置、可烧录的HEX文件以及链接映射文件等,目录结构清晰,整个工程采用标准外设库编写,代码结构整齐,可以直接编译下载。目前已有1782人学习。借助这套程序,读者可以快速搭建RC522读写环境,理解SPI时序和串口输出方法,还能参考其中定时器、LCD、Flash等外设驱动,方便把RC522模块移植到其他STM32型号上,为门禁、电子支付等非接触场景提供实用基础,同时适合入门学习与二次开发。 做嵌入式这些年,开发板换了不少,但正点原子Mini(STM32F103RCT6)这块小板子一直留在我工作台角落。原因很简单:芯片资源够用,例程齐全,跑个RC522这种13.56MHz射频读卡模块非常合适。项目标题说的“正点原子Mini(STM32F103RCT6)Rc522程序”,本质上就是一套让MCU通过SPI总线驱动RC522,实现IC卡读卡、防冲突、选卡、读写扇区数据的裸机程序。这篇文章我把硬件接线、底层驱动、核心代码、调试心得全部拆开讲,适合正在学STM32外设驱动、或者准备做门禁/考勤/校园卡读卡项目的朋友直接抄作业。
1. 这个项目的本质:一块板子加一个射频模块的通路搭建
1.1 STM32F103RCT6和RC522分别在干什么
很多新手拿到“RC522程序”第一反应是:能不能直接复制到自己的板子上?这就得先搞明白这套系统里两个主角的分工。
STM32F103RCT6是意法半导体旗下的Cortex-M3内核芯片,主频72MHz,片上Flash 256KB,SRAM 48KB。在正点原子Mini板上,它负责运行逻辑:初始化SPI外设、控制GPIO拉高拉低、解析RC522返回的数据、把卡号打印到串口或者显示在屏幕上。RC522则是恩智浦的MFRC522射频读写芯片,工作频率13.56MHz,支持ISO14443A协议,常见的Mifare S50、S70卡都能读。它负责“无线电”这一层:发射射频场、调制解调卡片返回的信号、完成CRC校验等底层活儿。
简单说,MCU是大脑,RC522是耳朵和嘴巴。程序里最核心的工程问题,就是让这两个芯片用同一种语言(SPI协议)说话。
1.2 这套程序能做什么,谁适合参考
完成这个项目后,你可以实现以下功能:
- 读出一张Mifare卡的4字节序列号(UID),通过串口打印出来
- 选卡后对指定扇区进行密钥验证,验证通过后读写块数据
- 把卡号显示在OLED屏或者LCD屏上,扩展成简易门禁系统
- 把读到的卡号与上位机交互,做考勤记录、会员管理等应用
适合参考的人群很明确:正在学STM32 SPI通信的初学者,做过基础流水灯想进阶的新手,以及需要快速在主控里集成RFID读卡功能的开发者。RC522模块价格便宜,几块钱一个,比买整套RFID开发板划算得多。
2. 硬件准备与接线:动手前先答对三道题
2.1 物料清单和模块选型建议
我的建议配置如下:
- 正点原子Mini开发板(STM32F103RCT6核心板)
- RC522模块(带天线一体板,最常见那种蓝色PCB)
- 一张Mifare S50卡,或者普通IC卡、门禁卡
- 杜邦线若干,母对母、母对公都备一点
- USB转TTL串口模块(如果板载没有USB串口)
选RC522模块时注意看天线大小,我试过几种,那种矩形三圈天线的模块读卡距离大概能到3~5cm,圆形小天线模块只有2cm左右,差别不小。如果要做门禁项目,建议选大面积天线的模块,读卡体验会好很多。
2.2 RC522与正点原子Mini的引脚对照表
正点原子Mini板上的SPI2接口在PB13、PB14、PB15这三个引脚,这个是硬件固定的,不能随便改。RC522的SDA是SPI片选信号,我用软件方式控制,接到PB12上。RST复位引脚接到PB0。IRQ中断引脚不接,程序里使用轮询方式读取数据,这样省一个GPIO。
| RC522引脚 | 正点原子Mini引脚 | 说明 |
|---|---|---|
| SDA | PB12 | SPI片选,软件控制,低电平有效 |
| SCK | PB13 | SPI2时钟线 |
| MOSI | PB15 | SPI2主机发送数据线 |
| MISO | PB14 | SPI2主机接收数据线 |
| IRQ | 不接 | 中断引脚,轮询模式无需连接 |
| RST | PB0 | 复位引脚,低电平复位 |
| GND | GND | 共地 |
| VCC | 3.3V | 模块供电,注意不能接5V |
2.3 供电、电平、线序的三个坑
三个坑每一个都可能让你白折腾两小时。
第一,供电电压。RC522模块标注是3.3V供电,但很多模块板上已经集成稳压电路,接5V也能工作。问题是正点原子Mini板上的IO电平是3.3V,如果模块用5V供电,SPI总线上的高电平就会变成5V,MCU引脚虽然一般有容忍能力,但长期使用不推荐。稳妥做法是统一用3.3V给模块供电。
第二,共地。很多人只接VCC、SCK、MOSI、MISO、SDA,不接GND,导致SPI通信电平没有参考点,时好时坏。这个问题排查起来非常隐蔽,串口偶尔能打印出卡号,偶尔又全零,十有八九就是没共地。
第三,SDA的片选极性。RC522的SDA低电平有效,但某些资料图里画的是高电平选中,照抄就完蛋。在初始化流程里必须确保片选在没有数据通信时保持高电平。
3. RC522的读卡原理与SPI的调参逻辑
3.1 MFRC522芯片在RFID链路里的位置
不用把射频规则背得滚瓜烂熟,你只需要理解这层关系:MFRC522负责和物理世界打交道,它内部有发送器、接收器、模拟前端、数字基带、寄存器组。MCU通过SPI往MFRC522的寄存器里写命令和参数,MFRC522再把命令调制到13.56MHz载波上发送给无源IC卡。
卡本身没有电池,它靠线圈感应取电,所以读卡距离近是物理特性决定的,一般不超过10cm。卡返回数据时通过负载调制方式改变天线端阻抗,MFRC522检测到这些微小变化后解码,把数据放到FIFO缓冲区里,MCU再读取出来。
一句话总结:MCU发的不是射频命令,而是寄存器操作;MFRC522才是真正和卡对话的人。
3.2 SPI四参数怎么定:模式、速率、位序、片选
SPI通信有四件事必须确认,缺一不可:
- 时钟极性和相位(CPOL、CPHA),决定数据在时钟上升沿还是下降沿采样
- 数据位宽,一般8位
- 时钟速率,决定SPI总线频率
- 片选信号,决定从机何时被激活
MFRC522默认支持SPI模式0(CPOL=0,CPHA=0),数据在时钟上升沿采样,下降沿改变。标准外设库初始化SPI2时,把SPI_CPOL_Low、SPI_CPHA_1Edge填进去,这就是模式0。位序必须是MSB先行,即最高位先发。
速率这块要特别说明一下。STM32F103的SPI2挂在APB1总线上,频率36MHz。RC522最高支持10MHz的SPI时钟,理论上可以配到9MHz(分频4)。但我实测下来,用杜邦线连接时,超过4MHz很容易出现偶发读取错误,特别是线长超过10cm后。程序里我使用8分频,SPI时钟大概4.5MHz,实际操作稳定可靠。
3.3 寻卡到读写扇区的完整时序
完整读一段扇区数据的时序大概是这样的:
- PcdRequest寻卡:发送
PICC_REQALL命令寻找视野内的卡片,卡返回ATQA类型码 - PcdAnticoll防冲突:如果多张卡同时在场,读出其中一张的4字节序列号(UID)
- PcdSelect选卡:用UID选中这张卡
- PcdAuthState验证密钥:使用A/B密钥验证目标扇区
- PcdRead/PcdWrite读写:对指定块进行数据读写
每步都要检查返回值是否为MI_OK,任何一个环节失败都不能继续往下走。这个顺序是MFRC522固件规定的,不能跳步,比如你不选卡直接读扇区,返回值必然是错误。
4. 程序框架与关键代码解读
4.1 工程结构:在正点原子库函数模板里加文件
正点原子Mini的例程模板默认带了sys.c、delay.c、usart.c这些基础文件,我建议直接基于“库函数版本”的工程模板来做,不要从零写启动文件。
我的文件组织方式是:
- 根目录下新建
rc522文件夹,放入rc522.c和rc522.h - 头文件路径加入工程,确保能够
#include "rc522.h" - 在
usart.c里初始化好串口1,波特率115200,方便printf输出卡号
rc522.c里的函数分成两层:底层函数负责SPI读写寄存器和GPIO控制;上层命令函数负责按照协议向MFRC522发送命令并解析结果。
4.2 SPI2和GPIO初始化:引脚映射别抄错
void SPI2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15); 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_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI2, &SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }这段代码有几个容易踩的细节。
PB13和PB15配置为复用推挽输出,PB14配置为浮空输入。PB12是软件片选,必须配置为通用推挽输出,并且默认输出高电平。我见过有人把PB12也配成复用功能,结果SPI通信时片选被SPI硬件控制,时序完全乱套。
时钟使能上,SPI2是APB1外设,GPIOB是APB2外设,两者要分别使能。
4.3 RC522底层函数:复位、写寄存器、读寄存器
RC522的通信协议很简单:先拉低SDA,然后发送一个字节的地址加命令组合,再读写数据。
写寄存器函数:
void RC522_WriteReg(unsigned char addr, unsigned char val) { RC522_CS_LOW(); SPI2_ReadWriteByte(((addr << 1) & 0x7E)); SPI2_ReadWriteByte(val); RC522_CS_HIGH(); }读寄存器函数:
unsigned char RC522_ReadReg(unsigned char addr) { unsigned char val; RC522_CS_LOW(); SPI2_ReadWriteByte((((addr << 1) & 0x7E) | 0x80)); val = SPI2_ReadWriteByte(0x00); RC522_CS_HIGH(); return val; }注意地址移位和位7置位的细节。写命令时地址左移1位,最高位为0;读命令时最高位为1。这个约定看数据手册才能确定,很多人直接抄别人代码,没注意这一层。
复位函数PcdReset固定三步:先把RST引脚拉低至少10微秒,然后拉高;复位后延时10ms左右让芯片启动稳定;再调用软件复位命令写寄存器。千万别省略复位后的延时,否则可能出现第一次寻卡失败、第二次就正常的诡异现象。
4.4 上层命令:寻卡、防冲突、选卡、验证、读写
上层函数直接调RC522的寄存器,完成协议交互。
寻卡函数核心逻辑:
char PcdRequest(unsigned char req_code, unsigned char *pTagType) { unsigned char status; unsigned char TagType[2]; unsigned char buf[2]; RC522_WriteReg(RC522_REG_BIT_FRAMING, 0x07); buf[0] = req_code; status = RC522_Command(PCD_TRANSCEIVE, buf, 1, TagType, 2); if (status == MI_OK) { pTagType[0] = TagType[0]; pTagType[1] = TagType[1]; } return status; }防冲突函数返回卡的4字节UID:
char PcdAnticoll(unsigned char *pSnr) { unsigned char status; unsigned char buf[5]; unsigned char i; RC522_WriteReg(RC522_REG_BIT_FRAMING, 0x00); buf[0] = PICC_ANTICOLL; buf[1] = 0x20; status = RC522_Command(PCD_TRANSCEIVE, buf, 2, buf, 5); if (status == MI_OK) { for (i = 0; i < 4; i++) { pSnr[i] = buf[i]; } } return status; }这里有个经典坑:PcdAnticoll执行完后,bit framing寄存器里的值不是0,是0x80(因为有尾字节需要发送)。如果后续要做Mifare密钥验证,必须先把RC522_REG_BIT_FRAMING写回0x00,否则验证永远失败。正点原子老版例程里在PcdAuthState开头写了这行。
主循环里读卡号的逻辑:
while (1) { status = PcdRequest(PICC_REQALL, tagType); if (status == MI_OK) { status = PcdAnticoll(snr); if (status == MI_OK) { printf("Card UID: %02X %02X %02X %02X\r\n", snr[0], snr[1], snr[2], snr[3]); } delay_ms(500); } delay_ms(200); }如果只想读卡号,到这一步就已经完成。需要读写扇区数据的,继续调PcdSelect和PcdAuthState就行。
5. 把程序跑起来:实际烧录、调试和验证
5.1 首次编译烧录要改哪几个地方
拿到工程后我一般按下面的顺序检查:
- 确认芯片型号选择的是STM32F103RC,正点原子Mini的HSE晶振是8MHz,不要刻意改
- 宏定义里检查是否有
STM32F10X_HD,RCT6属于高密度器件,漏了这个宏可能编译初始化代码选错 - 确认串口1波特率,我习惯用115200,8位数据,1位停止位
- 用ST-Link或者FlyMcu烧录,正点原子Mini板上预留了SWD接口,建议直接用ST-Link,烧录速度稳定
首次上电后如果串口不断打印乱码,先查波特率和串口接线是否交叉,不用先怀疑SPI。
5.2 串口打印卡号的实测记录
我实际跑起来的效果是这样:把IC卡贴近天线时,串口立刻输出一行Card UID: 0B 92 84 15,移开后程序继续循环寻卡,再次放卡又能打印。整个过程没有初始化失败的消息,没有乱码。
第一次调通这个程序的时候,我一度怀疑是不是假的读取,因为手机NFC功能读出来是同一个UID,对比完全一致。后来做按键开门实验时,我拿这张卡去刷宿舍的门禁,居然也能开,说明RC522读出来的卡号和真实门禁系统是一致的,协议层没有走偏。
5.3 顺带扩展:加一个OLED显示卡号
串口打印只能接电脑看,稍微扩展一下,把卡号显示到0.96寸OLED上,整个项目就完整多了。
OLED用I2C接口接PB6、PB7(正点原子Mini的I2C1复用),引入OLED驱动文件,把卡号格式化后显示:
char buf[20]; sprintf(buf, "UID: %02X%02X%02X%02X", snr[0], snr[1], snr[2], snr[3]); OLED_ShowString(0, 2, buf, 12);加上OLED显示后,就可以脱离电脑独立演示,适合拿去给别人展示项目成果。这部分代码不复杂,但要注意OLED初始化功耗较低,和RC522一起用3.3V供电没问题。
6. 读卡不成功的排查顺序与避坑清单
6.1 按优先级排查:硬件→电源→时序→程序
遇到读不到卡,别急着改代码,按这个顺序排查:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 全无反应,模块不发热 | 供电没接对 | 用万用表量VCC和GND之间电压是否为3.3V |
| SDA拉低后电流异常 | 供电电压过高 | 检查是否误接5V,确认模块引脚顺序 |
| 单片机能跑但读卡失败 | SPI引脚映射错误 | 重新核对PB13/14/15与RC522的SCK/MOSI/MISO对应关系 |
| 偶尔成功偶尔失败 | 杜邦线接触不良 | 换短杜邦线,或将模块直接插面包板 |
| 寻卡返回MI_NOTAGERR | 卡片类型不支持 | 确认是ISO14443A的Mifare卡,别用身份证 |
我用表格整理出来后,基本上排查速度能快一倍。尤其是“模块没反应”这种,八成是供电没接好或者模块本身是坏的,没必要在代码层面深挖。
6.2 让我印象最深的三个问题
第一个问题是换了一张卡后读到的UID全是FF FF FF FF。排查发现那张卡是UID可重写卡,出厂UID区域全0xFF,需要先写入新的UID才能正常使用。这不是程序问题。
第二个问题是RC522读卡距离短到只有1cm,怎么调都不行。后来发现是天线区域粘了一层双面胶,胶贴边缘正好覆盖了天线线圈的一部分,导致射频能量被干扰。撕掉之后读卡距离立刻恢复到3cm以上。这个细节在小批量打样时很容易忽略。
第三个问题比较隐蔽。我用库函数版本例程时,加了delay_ms(500)后寻卡偶尔会卡死。查了汇编发现是SysTick中断优先级设置问题,RC522命令执行期间被打断,导致SPI时序超时。解决办法是把滴答定时器优先级调为最低,或者在寻卡期间关闭中断,任务完成后重新开启。
6.3 其他移植场景的提醒
如果把程序从正点原子Mini板移植到自己的自制板子上,有几个位置容易出错:
- SPI外设编号:STM32F103C8T6也有SPI1和SPI2,但引脚位置不同,SPI1在PA5/PA6/PA7,移植时如果改了SPI编号,必须同步改GPIO配置
- 片选引脚:RC522的SDA可以用任意GPIO控制,只要在宏定义里改对应的引脚号即可
- 时钟频率:如果板子的外部晶振不是8MHz,HSE_VALUE宏要改,否则串口波特率会偏移
- 天线匹配:自制PCB时天线的线圈参数直接决定读卡距离,这个没有捷径,只能按数据手册推荐值画板
身边有朋友在F103C8T6上直接复刻这套程序,改完SPI1后一切正常。核心逻辑没变,引脚映射改明白就行。
最后再分享一点个人经验:RC522程序调通确实不算难,但真正把它做成一个稳定的产品级功能,比想象中琐碎得多。建议在项目初期就把每次读卡的成功失败次数用串口打出来,挂机跑几个小时观察是否出现偶发失败,再配合一个简单的看门狗就能大幅度提高稳定性。如果后续要接多个RC522模块,可以考虑用单片机自带的SPI硬件NSS配合软件切换,不过目前这个方案对于大多数应用场景已经完全够用。
本文还有配套的精品资源,点击获取