STM32F103C8T6+RC522射频读卡器+OLED完整教程(软件SPI模拟驱动,读卡显示UID)
一、项目概述
本项目基于 STM32F103C8T6 单片机,采用GPIO 模拟 SPI驱动 MFRC522 射频读卡模块,搭配 I2C OLED 屏幕,实现 13.56M ISO14443A MIFARE 卡片检测、寻卡、防冲突、读取 4 字节 UID、卡型识别并实时显示功能。
整体工作流程
- STM32 初始化 RC522、OLED 对应 GPIO 端口
- 硬件复位 RC522,配置内部定时器、射频调制参数,开启天线
- 读取 RC522 版本寄存器校验 SPI 通信是否正常
- RC522 天线输出 13.56MHz 交变射频磁场
- 主控发送寻卡指令,卡片上电返回 ATQA 卡片类型标识
- 执行防冲突算法,读取 4 字节 UID+BCC 校验码并校验
- 选中卡片后发送休眠指令,OLED 刷新卡片信息
- 每 100ms 循环扫描,搭配 800ms 移卡消抖,避免屏幕闪烁

三大核心器件分工
| 器件 | 核心作用 |
|---|---|
| STM32F103C8T6 | 运行主逻辑,通过模拟 SPI 下发寄存器指令,处理读卡数据,驱动 OLED 显示 |
| RC522 模块 | 完成 MCU 与射频卡信号转换,生成 13.56MHz 载波,射频调制 / 解调、协议帧处理 |
| MIFARE 射频卡 | 无源感应取电,响应寻卡、选卡、休眠指令,返回 UID 与卡片参数 |
二、硬件接线说明(严格对应工程引脚)
引脚对应表
| RC522 模块引脚 | STM32 引脚 | 功能说明 |
|---|---|---|
| SDA/SS | PB12 | SPI 片选,低电平有效(非 I2C 数据线) |
| SCK | PB13 | SPI 时钟输出 |
| MOSI | PB14 | STM32 → RC522 数据发送 |
| MISO | PB15 | RC522 → STM32 数据接收 |
| RST | PA8 | RC522 硬件复位,高电平正常工作 |
| 3.3V | 3.3V | 模块供电,严禁接 5V |
| GND | GND | 共地(必须共地,否则通信失效) |
| IRQ | 悬空 | 本工程轮询读取,不使用中断 |
| OLED 模块引脚 | STM32 引脚 | 功能 |
|---|---|---|
| SCL | PB8 | 软件 I2C 时钟 |
| SDA | PB9 | 软件 I2C 数据 |
| VCC | 3.3V | 供电 |
| GND | GND | 共地 |

重点接线避坑说明
-
RC522 的 SDA 不是 I2C 数据线 RC522 引脚复用:I2C 模式下为 SDA,SPI 模式下是片选 SS。本项目使用模拟 SPI,因此 PB12 只做片选,不能和 OLED 的 PB9 SDA 短接。
-
为什么不用硬件 SPI2,改用软件 SPI? STM32F103 SPI2 硬件引脚定义:SCK=PB13、MISO=PB14、MOSI=PB15; 工程要求 MOSI 接 PB14、MISO 接 PB15,和硬件 SPI 数据流向相反,因此将 PB13/14 配置为输出、PB15 配置为输入,纯 GPIO 模拟 SPI 时序。 若想改用硬件 SPI,需要调换 RC522 MOSI/MISO 接线并重写 SPI 驱动。
-
供电与布线要求
- MFRC522 芯片额定 3.3V,5V 供电会烧毁模块;
- 单片机、RC522、OLED 必须共地,电平无参考基准会出现通信乱码;
- SCK/MOSI/MISO 信号线尽量短,长线易产生干扰、接触不良。
-
天线读卡注意事项 RC522 方形铜线圈为读卡天线:
- 卡片线圈平面与天线平行,读卡距离最佳;
- 金属、硬币、大面积 PCB 地平面会吸收射频能量,大幅降低读卡距离;
- 多张卡片紧贴会互相耦合,读卡不稳定;
- 普通 RC522 稳定读卡距离仅 1~3cm。
三、RC522 芯片内部架构与通信原理
芯片内部数据流框图
STM32 (模拟 SPI) ↔ SPI 寄存器组 ↔ 64 字节 FIFO 缓存 ↔ 数字控制单元 (CRC / 帧校验 / 防冲突) ↔ 模拟前端 (调制解调) ↔ 13.56MHz 天线 ↔ 射频卡
各模块功能解析
- SPI 寄存器组:接收主控配置命令、读写芯片状态;
- 64 字节 FIFO:缓存收发数据,隔离 MCU 与射频通信速率差;
- 数字单元:自动处理 ISO14443A 帧格式、奇偶校验、CRC 校验、防冲突逻辑;
- 模拟前端:载波调制、卡片返回微弱信号放大解调;
- 内置定时器:通信超时保护,防止程序卡死;
- CRC 协处理器:硬件快速计算 CRC_A 校验码,减轻 MCU 运算压力。
核心关键点:STM32 不产生 13.56MHz 射频载波,射频调制、解调、底层编码全部由 RC522 硬件完成,主控仅操作寄存器即可。
四、13.56MHz RFID 无源卡通信完整原理
基础概念:PCD 与 PICC
- PCD:读卡器设备(本项目 RC522)
- PICC:无源射频卡片(MIFARE 卡、钥匙扣、NTAG 标签)
1. 无源卡片无电池如何供电?
RC522 天线持续输出 13.56MHz 交变磁场,卡片内置线圈电磁耦合产生交流感应电压,卡片内部整流稳压后给芯片供电。 读卡距离越远,磁场耦合越弱,极易出现读卡失败、UID 校验错误。
2. 读卡器下发数据(PCD→PICC)
采用 100% ASK 幅度调制,Miller 编码,通过控制 13.56MHz 载波有无传递数据,卡片检测载波幅度变化解析指令。
3. 卡片回传数据(PICC→PCD)
无源卡片无法主动发射射频,采用负载调制: 卡片内部开关改变线圈负载,读卡器天线端感应微弱幅度变化,RC522 模拟前端放大解调,解析 Manchester 编码数据。
五、GPIO 模拟 SPI 底层实现(SPI 模式 0)
SPI 模式 0 时序规则
- SCK 空闲低电平;高位先行;SCK 高电平时采样数据
- 全双工通信:发 1 字节同时接收 1 字节
RC522 寄存器读写规则
寄存器地址字节格式:bit7 = 读写标志,bit6~bit1=6 位寄存器地址,bit0 固定 0
- 写寄存器:
(addr << 1) & 0x7E - 读寄存器:
((addr << 1) & 0x7E) | 0x80
读写流程:
- 拉低 PB12 片选;
- 发送地址字节;
- 写寄存器:发送数据字节;读寄存器:发送 0x00 时钟,同步读取返回数据;
- 拉高片选结束通信。
位操作工具函数
驱动内置SetBitMask、ClearBitMask,先读取寄存器原值,仅修改目标位后写回,不破坏其他配置位。
六、RC522 初始化流程与核心寄存器
初始化步骤
- 开启 GPIOA、GPIOB 外设时钟;
- 配置 PB12/13/14 输出、PB15 浮空输入、PA8 复位输出;
- PA8 拉低 2ms 硬件复位,拉高等待 50ms 释放复位;
- 发送软件复位指令,配置定时器、调制模式;
- 开启 TX1/TX2 射频天线输出;
- 读取 VersionReg 版本寄存器,0x00/0xFF 判定通信故障。
关键寄存器配置
| 寄存器 | 配置值 | 功能 |
|---|---|---|
| CommandReg | 0x0F | 软件复位 RC522 |
| TxASKReg | 0x40 | 开启 100% ASK 调制 |
| TxControlReg | Bit0/1 置 1 | 打开射频天线 |
| VersionReg | 只读 | 校验 SPI 通信,识别芯片版本 |
版本寄存器故障判断
- 0x91:RC522 V1.0;0x92:RC522 V2.0;0x88:兼容芯片
- 0x00:供电异常、复位未释放、MISO 持续低电平
- 0xFF:MISO 悬空、片选接线错误、SPI 通信断开
七、ISO14443A 读卡完整协议流程(寻卡→防冲突→选卡→休眠)
整体交互时序
STM32 下发 WUPA 寻卡指令 → RC522 射频发送短帧 → 卡片返回 ATQA 卡型 → 执行防冲突读取 UID+BCC → 校验异或 BCC → 选卡确认卡片 → 发送 HALT 休眠卡片
1. 寻卡 PcdRequest
PICC_REQALL(0x52):WUPA,唤醒休眠卡片,全区域扫描;PICC_REQIDL(0x26):REQA,仅扫描未休眠卡片;- 发送 7 位短帧,卡片返回 2 字节 ATQA 标识卡片大类。
2. 防冲突 PcdAnticoll
多张卡片同时感应射频场会造成数据冲突,防冲突指令0x93 0x20分离单张卡片,返回 4 字节 UID+1 字节 BCC 校验码; 校验规则:BCC = UID0 ^ UID1 ^ UID2 ^ UID3,异或匹配才判定 UID 有效。
本驱动仅支持 4 字节短 UID 卡片,7/10 字节长 UID 卡需新增 0x95、0x97 级联防冲突逻辑。
3. 选卡 PcdSelect
携带完整 UID+BCC 下发选卡指令,卡片返回 SAK 标识,卡片进入选中状态,才可执行读写扇区操作。
4. 休眠 PcdHalt
下发0x50 0x00 + CRC休眠指令,协议规定卡片不应答即为执行成功,无返回数据代表正常。
八、MIFARE Classic 1K 卡存储与读写拓展(驱动自带接口)
1K 卡存储结构
总容量 1024 字节,16 个扇区,每扇区 4 块,每块 16 字节:
- 块 0:厂商固化块(包含 UID,禁止写入)
- 块 1、块 2:用户数据块
- 块 3:扇区控制块(6 字节 KeyA+3 字节访问控制 + 1 字节用户位 + 6 字节 KeyB)
读写操作流程
- 选卡成功后,使用对应扇区密钥执行
PcdAuthState认证; - 读块:下发 0x30 读指令,返回 16 字节数据 + CRC;
- 写块:分两步,下发写地址指令收到 ACK 后,再发送 16 字节数据;
- 操作完成调用
PcdStopCrypto1关闭加密。
安全提醒
MIFARE Classic Crypto1 加密算法已被破解,仅适用于教学实验,不可用于门禁、支付等高安全场景;禁止修改真实校园卡、交通卡扇区控制位,极易锁卡报废。
九、OLED 屏幕显示逻辑与消抖处理
三种显示界面
- 初始化界面:打印引脚定义、初始化提示;
- 故障界面:读取 VersionReg 为 00/FF,提示 SPI / 供电接线错误,每秒重试初始化;
- 待机界面:显示芯片版本,提示刷卡;
- 读卡界面:打印卡片类型、4 字节 UID,提示移卡切换卡片。
卡型识别规则(通过 ATQA 匹配)
| ATQA | 屏幕显示 |
|---|---|
| 04 00 | MIFARE 1K |
| 02 00 | MIFARE 4K |
| 44 00 | ULTRALIGHT |
| 其他 | UNKNOWN |
移卡消抖机制
- 扫描周期:100ms / 次;
- 移除判定:连续 8 次无卡片应答才判定卡片移开,消抖总时长 800ms;
- 同卡防抖:缓存上一次 UID,卡片不变时不刷新屏幕,减少闪烁。

十、常见故障排查大全
故障 1:OLED 持续提示 RC522 INIT ERROR
- RC522 接 5V 供电,改为 3.3V;
- STM32、RC522、OLED 未共地;
- MOSI/MISO 引脚接反、片选 PB12 虚接;
- RST 引脚 PA8 持续拉低,复位未释放;
- 误将 RC522 SDA 接到 OLED PB9;
- 排针、杜邦线接触不良。
故障 2:初始化成功,刷卡无任何反应
- 使用 125K 低频 ID 卡,RC522 仅支持 13.56M A 类卡;
- 卡片与天线垂直、距离超过 5cm;
- 天线下方放置金属、铁板;
- 多张卡片堆叠,射频耦合干扰;
- 3.3V 电源带载能力不足。
故障 3:能读卡,但 UID 乱码、读取不稳定
- SPI 信号线过长、接触虚焊;
- MOSI/MISO 引脚颠倒;
- 周边存在其他 13.56M 读卡器干扰;
- 卡片处于读卡临界距离。
故障 4:4 字节 UID 正常,7 字节卡片读取失败
当前驱动仅实现一级防冲突,未支持二级、三级级联,需拓展防冲突函数。
故障 5:UID 正常读取,但扇区认证 / 读块失败
- 卡片非 MIFARE Classic,不支持 Classic 读写指令;
- 使用了错误扇区密钥;
- 扇区访问控制位禁止当前密钥操作;
- 未执行 PcdSelect 选卡直接读写;
- 上一次加密未关闭 Crypto1。
十一、工程文件结构说明
RC522.c:底层 GPIO 初始化、模拟 SPI、寄存器读写、寻卡 / 防冲突 / 认证 / 读写全功能驱动;RC522.h:寄存器、命令、返回值宏定义,函数声明;main.c:主循环,OLED 显示逻辑、读卡扫描、消抖处理;- OLED 驱动文件:软件 I2C 屏幕显示函数。
核心调用流程示例
u8 tag_type[2];
u8 uid[5];
if(RC522_Init() == MI_OK)
{
// 寻卡
if(PcdRequest(PICC_REQALL, tag_type) == MI_OK)
{
// 防冲突读取UID
if(PcdAnticoll(uid) == MI_OK)
{
// 选中卡片
if(PcdSelect(uid) == MI_OK)
{
// 可拓展:密钥认证、读写数据块
PcdHalt(); // 卡片休眠
}
}
}
}
main.c
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "RC522.h"
#define RC522_SCAN_INTERVAL_MS 100U
#define RC522_REMOVE_CONFIRM_COUNT 8U
static void OLED_ShowBoot(void);
static void OLED_ShowReaderReady(u8 version);
static void OLED_ShowReaderError(u8 version);
static void OLED_ShowCard(const u8 *tag_type, const u8 *uid);
static void OLED_ShowHexByte(u8 x, u8 y, u8 value, u8 size);
static const u8 *RC522_GetCardTypeName(const u8 *tag_type);
static u8 RC522_UIDEquals(const u8 *first, const u8 *second);
static void RC522_CopyUID(u8 *destination, const u8 *source);
int main(void)
{
u8 tag_type[2];
u8 uid[5];
u8 last_uid[5];
u8 version;
u8 status;
u8 card_present;
u8 missed_scans;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0);
OLED_DisplayTurn(0);
OLED_ShowBoot();
status = RC522_Init();
version = RC522_GetVersion();
while(status != MI_OK)
{
OLED_ShowReaderError(version);
delay_ms(1000);
status = RC522_Init();
version = RC522_GetVersion();
}
card_present = 0U;
missed_scans = 0U;
OLED_ShowReaderReady(version);
while(1)
{
status = PcdRequest(PICC_REQALL, tag_type);
if((status == MI_OK) && (PcdAnticoll(uid) == MI_OK))
{
missed_scans = 0U;
if((card_present == 0U) ||
(RC522_UIDEquals(uid, last_uid) == 0U))
{
RC522_CopyUID(last_uid, uid);
OLED_ShowCard(tag_type, uid);
}
card_present = 1U;
/*
* Select and halt complete one ISO14443A transaction. REQALL in
* the next scan wakes the card again, so a held card remains
* visible while removal can still be detected.
*/
if(PcdSelect(uid) == MI_OK)
{
PcdHalt();
}
}
else
{
if(card_present != 0U)
{
if(missed_scans < RC522_REMOVE_CONFIRM_COUNT)
{
missed_scans++;
}
if(missed_scans >= RC522_REMOVE_CONFIRM_COUNT)
{
card_present = 0U;
OLED_ShowReaderReady(version);
}
}
}
delay_ms(RC522_SCAN_INTERVAL_MS);
}
}
static void OLED_ShowBoot(void)
{
OLED_Clear();
OLED_ShowString(19, 0, (u8 *)"RC522 + OLED", 16);
OLED_ShowString(13, 22, (u8 *)"Initializing reader", 12);
OLED_ShowString(10, 40, (u8 *)"SPI: PB12-PB15", 12);
OLED_ShowString(28, 52, (u8 *)"RST: PA8", 12);
OLED_Refresh();
}
static void OLED_ShowReaderReady(u8 version)
{
OLED_Clear();
OLED_ShowString(19, 0, (u8 *)"RC522 RFID DEMO", 12);
OLED_ShowString(25, 16, (u8 *)"Ready VER:", 12);
OLED_ShowHexByte(85, 16, version, 12);
OLED_ShowString(12, 33, (u8 *)"Tap a card...", 16);
OLED_ShowString(13, 52, (u8 *)"CS:PB12 RST:PA8", 12);
OLED_Refresh();
}
static void OLED_ShowReaderError(u8 version)
{
OLED_Clear();
OLED_ShowString(10, 0, (u8 *)"RC522 INIT ERROR", 12);
OLED_ShowString(31, 16, (u8 *)"VER: 0x", 12);
OLED_ShowHexByte(73, 16, version, 12);
OLED_ShowString(13, 34, (u8 *)"Check power / SPI", 12);
OLED_ShowString(19, 50, (u8 *)"PB12-15, PA8", 12);
OLED_Refresh();
}
static void OLED_ShowCard(const u8 *tag_type, const u8 *uid)
{
u8 index;
u8 x;
OLED_Clear();
OLED_ShowString(25, 0, (u8 *)"CARD DETECTED", 12);
OLED_ShowString(7, 16, (u8 *)"TYPE:", 12);
OLED_ShowString(37, 16, (u8 *)RC522_GetCardTypeName(tag_type), 12);
OLED_ShowString(0, 33, (u8 *)"UID:", 16);
x = 32U;
for(index = 0U; index < 4U; index++)
{
OLED_ShowHexByte(x, 33, uid[index], 16);
x = (u8)(x + 24U);
}
OLED_ShowString(10, 52, (u8 *)"Remove / next card", 12);
OLED_Refresh();
}
static void OLED_ShowHexByte(u8 x, u8 y, u8 value, u8 size)
{
static const u8 hex_table[] = "0123456789ABCDEF";
u8 width;
width = (u8)(size / 2U);
OLED_ShowChar(x, y, hex_table[(value >> 4) & 0x0FU], size);
OLED_ShowChar((u8)(x + width), y, hex_table[value & 0x0FU], size);
}
static const u8 *RC522_GetCardTypeName(const u8 *tag_type)
{
if((tag_type[0] == 0x04U) && (tag_type[1] == 0x00U))
{
return (const u8 *)"MIFARE 1K";
}
if((tag_type[0] == 0x02U) && (tag_type[1] == 0x00U))
{
return (const u8 *)"MIFARE 4K";
}
if((tag_type[0] == 0x44U) && (tag_type[1] == 0x00U))
{
return (const u8 *)"ULTRALIGHT";
}
if((tag_type[0] == 0x08U) && (tag_type[1] == 0x00U))
{
return (const u8 *)"MIFARE PRO";
}
if((tag_type[0] == 0x44U) && (tag_type[1] == 0x03U))
{
return (const u8 *)"DESFIRE";
}
return (const u8 *)"UNKNOWN";
}
static u8 RC522_UIDEquals(const u8 *first, const u8 *second)
{
u8 index;
for(index = 0U; index < 5U; index++)
{
if(first[index] != second[index])
{
return 0U;
}
}
return 1U;
}
static void RC522_CopyUID(u8 *destination, const u8 *source)
{
u8 index;
for(index = 0U; index < 5U; index++)
{
destination[index] = source[index];
}
}
RC522.c
#include "RC522.h"
#include "delay.h"
/* RC522 与 STM32F103C8T6 的 GPIO 引脚定义。 */
#define RC522_CS_PIN GPIO_Pin_12
#define RC522_SCK_PIN GPIO_Pin_13
#define RC522_MOSI_PIN GPIO_Pin_14
#define RC522_MISO_PIN GPIO_Pin_15
#define RC522_RST_PIN GPIO_Pin_8
/* 软件 SPI 和复位引脚的电平控制宏。 */
#define RC522_CS_LOW() GPIO_ResetBits(GPIOB, RC522_CS_PIN)
#define RC522_CS_HIGH() GPIO_SetBits(GPIOB, RC522_CS_PIN)
#define RC522_SCK_LOW() GPIO_ResetBits(GPIOB, RC522_SCK_PIN)
#define RC522_SCK_HIGH() GPIO_SetBits(GPIOB, RC522_SCK_PIN)
#define RC522_MOSI_LOW() GPIO_ResetBits(GPIOB, RC522_MOSI_PIN)
#define RC522_MOSI_HIGH() GPIO_SetBits(GPIOB, RC522_MOSI_PIN)
#define RC522_MISO_READ() GPIO_ReadInputDataBit(GPIOB, RC522_MISO_PIN)
#define RC522_RST_LOW() GPIO_ResetBits(GPIOA, RC522_RST_PIN)
#define RC522_RST_HIGH() GPIO_SetBits(GPIOA, RC522_RST_PIN)
/* 轮询超时计数,防止模块未连接时程序永久停在等待循环中。 */
#define RC522_COMM_TIMEOUT 2000U
#define RC522_CRC_TIMEOUT 255U
/* 本文件内部使用的底层函数声明。 */
static void RC522_GPIO_Init(void);
static u8 RC522_SPI_Transfer(u8 data);
static u8 PcdComMF522(u8 command, u8 *in_data, u8 in_length,
u8 *out_data, u16 *out_length_bits);
static u8 PcdCalculateCRC(u8 *in_data, u8 length, u8 *out_data);
/*
* 函数名称:RC522_GPIO_Init
* 功能说明:初始化 RC522 使用的五根 GPIO 信号线。
* 参数说明:无。
* 返回值 :无。
*
* SPI 时钟空闲时保持低电平,在 SCK 高电平期间读取数据,对应 SPI 模式 0。
* PB12、PB13、PB14 配置为推挽输出,PB15 配置为浮空输入,PA8 配置为
* 推挽输出。初始化结束后释放片选并让 RC522 保持正常工作状态。
*/
static void RC522_GPIO_Init(void)
{
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
RCC_APB2Periph_GPIOB, ENABLE);
gpio.GPIO_Pin = RC522_CS_PIN | RC522_SCK_PIN | RC522_MOSI_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &gpio);
gpio.GPIO_Pin = RC522_MISO_PIN;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &gpio);
gpio.GPIO_Pin = RC522_RST_PIN;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &gpio);
RC522_CS_HIGH();
RC522_SCK_LOW();
RC522_MOSI_LOW();
RC522_RST_HIGH();
}
/*
* 函数名称:RC522_SPI_Transfer
* 功能说明:使用 GPIO 模拟 SPI,同时发送和接收一个字节。
* 参数说明:data 为需要发送的 8 位数据。
* 返回值 :在 MISO 引脚上接收到的 8 位数据。
*
* 数据从最高位开始传输。由于指定接线中的 PB14/PB15 与 STM32F103
* 硬件 SPI2 的 MOSI/MISO 方向相反,因此这里使用软件 SPI。
*/
static u8 RC522_SPI_Transfer(u8 data)
{
u8 bit_mask;
u8 received;
received = 0U;
for(bit_mask = 0x80U; bit_mask != 0U; bit_mask >>= 1)
{
if((data & bit_mask) != 0U)
{
RC522_MOSI_HIGH();
}
else
{
RC522_MOSI_LOW();
}
RC522_SCK_HIGH();
if(RC522_MISO_READ() == Bit_SET)
{
received |= bit_mask;
}
RC522_SCK_LOW();
}
return received;
}
/*
* 函数名称:WriteRawRC
* 功能说明:通过软件 SPI 向 MFRC522 的一个寄存器写入数据。
* 参数说明:address 为寄存器地址,value 为需要写入的数据。
* 返回值 :无。
*
* 写操作地址格式为“0AAAAAA0”,其中 AAAAAA 为 6 位寄存器地址。
*/
void WriteRawRC(u8 address, u8 value)
{
RC522_CS_LOW();
RC522_SPI_Transfer((u8)((address << 1) & 0x7EU));
RC522_SPI_Transfer(value);
RC522_CS_HIGH();
}
/*
* 函数名称:ReadRawRC
* 功能说明:通过软件 SPI 读取 MFRC522 的一个寄存器。
* 参数说明:address 为寄存器地址。
* 返回值 :寄存器当前的 8 位数据。
*
* 读操作需要将地址字节最高位置 1,地址格式为“1AAAAAA0”。
*/
u8 ReadRawRC(u8 address)
{
u8 value;
RC522_CS_LOW();
RC522_SPI_Transfer((u8)(((address << 1) & 0x7EU) | 0x80U));
value = RC522_SPI_Transfer(0x00U);
RC522_CS_HIGH();
return value;
}
/*
* 函数名称:SetBitMask
* 功能说明:将指定寄存器中掩码对应的位设置为 1,其余位保持不变。
* 参数说明:reg 为寄存器地址,mask 为需要置位的位掩码。
* 返回值 :无。
*/
void SetBitMask(u8 reg, u8 mask)
{
WriteRawRC(reg, (u8)(ReadRawRC(reg) | mask));
}
/*
* 函数名称:ClearBitMask
* 功能说明:将指定寄存器中掩码对应的位清零,其余位保持不变。
* 参数说明:reg 为寄存器地址,mask 为需要清零的位掩码。
* 返回值 :无。
*/
void ClearBitMask(u8 reg, u8 mask)
{
WriteRawRC(reg, (u8)(ReadRawRC(reg) & (u8)(~mask)));
}
/*
* 函数名称:PcdReset
* 功能说明:先对 RC522 进行硬件复位,再执行软件复位和基础参数配置。
* 参数说明:无。
* 返回值 :无。
*/
void PcdReset(void)
{
u16 timeout;
/* 硬件复位可以让刚上电或中途重新连接的模块从确定状态开始工作。 */
RC522_RST_LOW();
delay_ms(2);
RC522_RST_HIGH();
delay_ms(50);
WriteRawRC(CommandReg, PCD_RESETPHASE);
timeout = 1000U;
while(((ReadRawRC(CommandReg) & 0x10U) != 0U) && (timeout != 0U))
{
timeout--;
}
/*
* 设置 MFRC522 内部定时器,通信等待周期约为 15 ms。
* 同时配置 100% ASK 调制以及 ISO14443A 通信所需的 CRC 初始值。
*/
WriteRawRC(TModeReg, 0x8DU);
WriteRawRC(TPrescalerReg, 0x3EU);
WriteRawRC(TReloadRegL, 30U);
WriteRawRC(TReloadRegH, 0U);
WriteRawRC(TxASKReg, 0x40U);
WriteRawRC(ModeReg, 0x3DU);
}
/*
* 函数名称:PcdAntennaOn
* 功能说明:打开 TX1、TX2 射频输出,启动 RC522 天线载波。
* 参数说明:无。
* 返回值 :无。
*/
void PcdAntennaOn(void)
{
u8 value;
value = ReadRawRC(TxControlReg);
if((value & 0x03U) != 0x03U)
{
SetBitMask(TxControlReg, 0x03U);
}
}
/*
* 函数名称:PcdAntennaOff
* 功能说明:关闭 TX1、TX2 射频输出,停止 RC522 天线载波。
* 参数说明:无。
* 返回值 :无。
*/
void PcdAntennaOff(void)
{
ClearBitMask(TxControlReg, 0x03U);
}
/*
* 函数名称:RC522_GetVersion
* 功能说明:读取 VersionReg,检查软件 SPI 是否能够与 RC522 正常通信。
* 参数说明:无。
* 返回值 :RC522 芯片版本号。
*/
u8 RC522_GetVersion(void)
{
return ReadRawRC(VersionReg);
}
/*
* 函数名称:RC522_Init
* 功能说明:初始化 GPIO、复位 RC522、打开天线并检查芯片版本号。
* 参数说明:无。
* 返回值 :MI_OK 表示初始化成功,MI_ERR 表示通信异常。
*/
u8 RC522_Init(void)
{
u8 version;
RC522_GPIO_Init();
PcdReset();
PcdAntennaOn();
version = RC522_GetVersion();
/*
* 常见原厂芯片返回 0x91 或 0x92,部分兼容芯片返回 0x88。
* 这里仅把总线悬空时常见的 0x00、0xFF 判定为错误,以便其他版本的
* 兼容教学模块也能正常使用。
*/
if((version == 0x00U) || (version == 0xFFU))
{
return MI_ERR;
}
return MI_OK;
}
/*
* 函数名称:PcdComMF522
* 功能说明:执行 MFRC522 的认证或收发命令,是寻卡、选卡、读写的公共接口。
* 参数说明:
* command MFRC522 内部命令,仅支持 PCD_AUTHENT 或 PCD_TRANSCEIVE。
* in_data 待发送数据缓冲区。
* in_length 待发送字节数。
* out_data 接收数据缓冲区。
* out_length_bits 实际接收到的数据位数。
* 返回值 :MI_OK、MI_NOTAGERR 或 MI_ERR。
*/
static u8 PcdComMF522(u8 command, u8 *in_data, u8 in_length,
u8 *out_data, u16 *out_length_bits)
{
u8 irq_enable;
u8 wait_irq;
u8 irq_value;
u8 error_value;
u8 fifo_length;
u8 last_bits;
u8 index;
u16 timeout;
u8 status;
if((in_data == 0) || (out_data == 0) || (out_length_bits == 0))
{
return MI_ERR;
}
irq_enable = 0U;
wait_irq = 0U;
if(command == PCD_AUTHENT)
{
irq_enable = 0x12U;
wait_irq = 0x10U;
}
else if(command == PCD_TRANSCEIVE)
{
irq_enable = 0x77U;
wait_irq = 0x30U;
}
else
{
return MI_ERR;
}
WriteRawRC(ComIEnReg, (u8)(irq_enable | 0x80U));
ClearBitMask(ComIrqReg, 0x80U);
SetBitMask(FIFOLevelReg, 0x80U);
WriteRawRC(CommandReg, PCD_IDLE);
/* 清空 FIFO 后,将本次待发送的数据逐字节写入 FIFO。 */
for(index = 0U; index < in_length; index++)
{
WriteRawRC(FIFODataReg, in_data[index]);
}
WriteRawRC(CommandReg, command);
if(command == PCD_TRANSCEIVE)
{
/* StartSend 位置 1 后,MFRC522 开始发送 FIFO 中的数据。 */
SetBitMask(BitFramingReg, 0x80U);
}
/* 轮询中断标志,同时用软件计数限制最长等待时间。 */
timeout = RC522_COMM_TIMEOUT;
do
{
irq_value = ReadRawRC(ComIrqReg);
timeout--;
}
while((timeout != 0U) &&
((irq_value & 0x01U) == 0U) &&
((irq_value & wait_irq) == 0U));
if(command == PCD_TRANSCEIVE)
{
ClearBitMask(BitFramingReg, 0x80U);
}
if(timeout == 0U)
{
return MI_ERR;
}
error_value = ReadRawRC(ErrorReg);
if((error_value & 0x1BU) != 0U)
{
return MI_ERR;
}
status = MI_OK;
if((irq_value & irq_enable & 0x01U) != 0U)
{
status = MI_NOTAGERR;
}
if((status == MI_OK) && (command == PCD_TRANSCEIVE))
{
fifo_length = ReadRawRC(FIFOLevelReg);
last_bits = (u8)(ReadRawRC(ControlReg) & 0x07U);
/* 根据最后一个字节的有效位数计算本次应答的总位数。 */
if(last_bits != 0U)
{
*out_length_bits =
(u16)(((u16)(fifo_length - 1U) * 8U) + last_bits);
}
else
{
*out_length_bits = (u16)((u16)fifo_length * 8U);
}
if(fifo_length == 0U)
{
fifo_length = 1U;
}
if(fifo_length > RC522_MAX_LEN)
{
fifo_length = RC522_MAX_LEN;
}
for(index = 0U; index < fifo_length; index++)
{
out_data[index] = ReadRawRC(FIFODataReg);
}
}
return status;
}
/*
* 函数名称:PcdCalculateCRC
* 功能说明:调用 MFRC522 内部 CRC 协处理器计算 ISO14443A CRC_A。
* 参数说明:
* in_data 参与 CRC 计算的数据。
* length 输入数据字节数。
* out_data 保存 2 字节 CRC,低字节在前、高字节在后。
* 返回值 :MI_OK 表示计算完成,MI_ERR 表示参数错误或计算超时。
*/
static u8 PcdCalculateCRC(u8 *in_data, u8 length, u8 *out_data)
{
u8 index;
u8 irq_value;
u16 timeout;
if((in_data == 0) || (out_data == 0))
{
return MI_ERR;
}
ClearBitMask(DivIrqReg, 0x04U);
SetBitMask(FIFOLevelReg, 0x80U);
for(index = 0U; index < length; index++)
{
WriteRawRC(FIFODataReg, in_data[index]);
}
WriteRawRC(CommandReg, PCD_CALCCRC);
timeout = RC522_CRC_TIMEOUT;
do
{
irq_value = ReadRawRC(DivIrqReg);
timeout--;
}
while((timeout != 0U) && ((irq_value & 0x04U) == 0U));
if(timeout == 0U)
{
return MI_ERR;
}
out_data[0] = ReadRawRC(CRCResultRegL);
out_data[1] = ReadRawRC(CRCResultRegH);
return MI_OK;
}
/*
* 函数名称:PcdRequest
* 功能说明:发送 REQA 或 WUPA 寻卡命令,检测天线范围内是否存在卡片。
* 参数说明:
* req_code PICC_REQIDL 表示只寻找未休眠卡,PICC_REQALL 表示寻找全部卡。
* tag_type 用于保存卡片返回的 2 字节 ATQA 类型信息。
* 返回值 :MI_OK 表示寻卡成功,MI_NOTAGERR 表示未发现卡片。
*/
u8 PcdRequest(u8 req_code, u8 *tag_type)
{
u8 buffer[RC522_MAX_LEN];
u16 received_bits;
u8 status;
if(tag_type == 0)
{
return MI_ERR;
}
/* ISO14443A 的 REQA/WUPA 命令使用 7 位短帧发送。 */
ClearBitMask(Status2Reg, 0x08U);
WriteRawRC(BitFramingReg, 0x07U);
SetBitMask(TxControlReg, 0x03U);
buffer[0] = req_code;
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 1U,
buffer, &received_bits);
if((status != MI_OK) || (received_bits != 16U))
{
return (status == MI_NOTAGERR) ? MI_NOTAGERR : MI_ERR;
}
tag_type[0] = buffer[0];
tag_type[1] = buffer[1];
return MI_OK;
}
/*
* 函数名称:PcdAnticoll
* 功能说明:执行第一级防冲突,取得卡片的 4 字节 UID。
* 参数说明:uid 至少提供 5 字节空间,前 4 字节为 UID,第 5 字节为 BCC。
* 返回值 :MI_OK 表示 UID 及异或校验正确,否则返回 MI_ERR。
*/
u8 PcdAnticoll(u8 *uid)
{
u8 buffer[RC522_MAX_LEN];
u8 checksum;
u8 index;
u16 received_bits;
u8 status;
if(uid == 0)
{
return MI_ERR;
}
WriteRawRC(BitFramingReg, 0x00U);
ClearBitMask(CollReg, 0x80U);
buffer[0] = PICC_ANTICOLL1;
buffer[1] = 0x20U;
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 2U,
buffer, &received_bits);
if((status != MI_OK) || (received_bits != 40U))
{
return MI_ERR;
}
checksum = 0U;
for(index = 0U; index < 4U; index++)
{
uid[index] = buffer[index];
checksum ^= buffer[index];
}
uid[4] = buffer[4];
if(checksum != uid[4])
{
return MI_ERR;
}
return MI_OK;
}
/*
* 函数名称:PcdSelect
* 功能说明:使用防冲突阶段取得的 UID 选择目标卡片。
* 参数说明:uid 为 4 字节 UID 加 1 字节 BCC 校验值。
* 返回值 :收到 24 位 SAK 应答时返回 MI_OK,否则返回 MI_ERR。
*/
u8 PcdSelect(u8 *uid)
{
u8 buffer[9];
u8 index;
u16 received_bits;
u8 status;
if(uid == 0)
{
return MI_ERR;
}
buffer[0] = PICC_ANTICOLL1;
buffer[1] = 0x70U;
for(index = 0U; index < 5U; index++)
{
buffer[index + 2U] = uid[index];
}
if(PcdCalculateCRC(buffer, 7U, &buffer[7]) != MI_OK)
{
return MI_ERR;
}
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 9U,
buffer, &received_bits);
if((status == MI_OK) && (received_bits == 24U))
{
return MI_OK;
}
return MI_ERR;
}
/*
* 函数名称:PcdAuthState
* 功能说明:对 MIFARE Classic 指定数据块执行 Crypto1 密钥认证。
* 参数说明:
* auth_mode PICC_AUTHENT1A 使用 Key A,PICC_AUTHENT1B 使用 Key B。
* block_addr 需要认证的数据块地址。
* key 6 字节扇区密钥。
* uid 当前卡片 UID,认证过程使用前 4 字节。
* 返回值 :认证成功返回 MI_OK,否则返回 MI_ERR。
*/
u8 PcdAuthState(u8 auth_mode, u8 block_addr, u8 *key, u8 *uid)
{
u8 buffer[12];
u8 index;
u16 received_bits;
u8 status;
if((key == 0) || (uid == 0))
{
return MI_ERR;
}
buffer[0] = auth_mode;
buffer[1] = block_addr;
for(index = 0U; index < 6U; index++)
{
buffer[index + 2U] = key[index];
}
for(index = 0U; index < 4U; index++)
{
buffer[index + 8U] = uid[index];
}
status = PcdComMF522(PCD_AUTHENT, buffer, 12U,
buffer, &received_bits);
if((status != MI_OK) || ((ReadRawRC(Status2Reg) & 0x08U) == 0U))
{
return MI_ERR;
}
return MI_OK;
}
/*
* 函数名称:PcdRead
* 功能说明:读取 MIFARE Classic 的一个数据块。
* 参数说明:
* block_addr 数据块地址,调用前需要先完成选卡和密码认证。
* data 保存读取结果的缓冲区,长度不得小于 16 字节。
* 返回值 :成功返回 MI_OK,通信或帧长度异常返回 MI_ERR。
*/
u8 PcdRead(u8 block_addr, u8 *data)
{
u8 buffer[RC522_MAX_LEN];
u8 index;
u16 received_bits;
u8 status;
if(data == 0)
{
return MI_ERR;
}
buffer[0] = PICC_READ;
buffer[1] = block_addr;
if(PcdCalculateCRC(buffer, 2U, &buffer[2]) != MI_OK)
{
return MI_ERR;
}
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 4U,
buffer, &received_bits);
if((status != MI_OK) || (received_bits != 144U))
{
return MI_ERR;
}
for(index = 0U; index < 16U; index++)
{
data[index] = buffer[index];
}
return MI_OK;
}
/*
* 函数名称:PcdWrite
* 功能说明:向 MIFARE Classic 的一个数据块写入 16 字节数据。
* 参数说明:
* block_addr 数据块地址,调用前需要先完成选卡和密码认证。
* data 待写入的 16 字节数据。
* 返回值 :两个写入阶段均收到 4 位 ACK 时返回 MI_OK。
*
* 注意:不要随意写入扇区尾块,否则可能修改密钥和访问控制位。
*/
u8 PcdWrite(u8 block_addr, u8 *data)
{
u8 buffer[RC522_MAX_LEN];
u8 index;
u16 received_bits;
u8 status;
if(data == 0)
{
return MI_ERR;
}
buffer[0] = PICC_WRITE;
buffer[1] = block_addr;
if(PcdCalculateCRC(buffer, 2U, &buffer[2]) != MI_OK)
{
return MI_ERR;
}
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 4U,
buffer, &received_bits);
if((status != MI_OK) || (received_bits != 4U) ||
((buffer[0] & 0x0FU) != 0x0AU))
{
return MI_ERR;
}
for(index = 0U; index < 16U; index++)
{
buffer[index] = data[index];
}
if(PcdCalculateCRC(buffer, 16U, &buffer[16]) != MI_OK)
{
return MI_ERR;
}
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 18U,
buffer, &received_bits);
if((status != MI_OK) || (received_bits != 4U) ||
((buffer[0] & 0x0FU) != 0x0AU))
{
return MI_ERR;
}
return MI_OK;
}
/*
* 函数名称:PcdStopCrypto1
* 功能说明:清除 MFCrypto1On 位,结束当前卡片的 Crypto1 加密通信。
* 参数说明:无。
* 返回值 :无。
*/
void PcdStopCrypto1(void)
{
ClearBitMask(Status2Reg, 0x08U);
}
/*
* 函数名称:PcdHalt
* 功能说明:向当前卡片发送 HALT 命令,使其进入休眠状态。
* 参数说明:无。
* 返回值 :卡片不返回数据即表示 HALT 成功,此时返回 MI_OK。
*/
u8 PcdHalt(void)
{
u8 buffer[4];
u16 received_bits;
u8 status;
buffer[0] = PICC_HALT;
buffer[1] = 0x00U;
if(PcdCalculateCRC(buffer, 2U, &buffer[2]) != MI_OK)
{
return MI_ERR;
}
status = PcdComMF522(PCD_TRANSCEIVE, buffer, 4U,
buffer, &received_bits);
/* ISO14443A 规定卡片通过“不应答”的方式确认 HALT 命令。 */
if(status == MI_NOTAGERR)
{
return MI_OK;
}
return status;
}
RC522.h
#ifndef __RC522_H
#define __RC522_H
#include "sys.h"
/*
* 文件名称:RC522.h
* 功能说明:MFRC522 射频读卡模块驱动头文件
* 适用芯片:STM32F103C8T6
* 通信方式:GPIO 模拟 SPI(SPI 模式 0,数据高位先发送)
*
* 硬件连接:
* RC522 SDA/SS -> STM32 PB12(片选信号,低电平有效)
* RC522 SCK -> STM32 PB13(SPI 时钟)
* RC522 MOSI -> STM32 PB14(STM32 向 RC522 发送数据)
* RC522 MISO -> STM32 PB15(STM32 接收 RC522 数据)
* RC522 RST -> STM32 PA8 (硬件复位,高电平正常工作)
*
* 注意:
* 本工程严格按照上述接线使用 PB14 作为 MOSI、PB15 作为 MISO。
* 该方向与 STM32F103 的硬件 SPI2 复用功能相反,因此不能直接使用
* SPI2 外设,驱动中采用 GPIO 模拟 SPI 完成通信。
*/
#define RC522_MAX_LEN 18U
/* 驱动函数返回值,与参考例程中的定义保持兼容。 */
#define MI_OK 0x00U
#define MI_NOTAGERR 0xFFU
#define MI_ERR 0xFEU
/* MFRC522 内部命令字。 */
#define PCD_IDLE 0x00U /* 取消当前命令,进入空闲状态 */
#define PCD_AUTHENT 0x0EU /* 执行 MIFARE 密钥认证 */
#define PCD_RECEIVE 0x08U /* 启动接收电路 */
#define PCD_TRANSMIT 0x04U /* 启动发送电路 */
#define PCD_TRANSCEIVE 0x0CU /* 发送数据并自动接收应答 */
#define PCD_RESETPHASE 0x0FU /* 软件复位 MFRC522 */
#define PCD_CALCCRC 0x03U /* 使用内部协处理器计算 CRC */
/* ISO14443A / MIFARE 卡片命令字。 */
#define PICC_REQIDL 0x26U /* 寻找未进入休眠状态的卡 */
#define PICC_REQALL 0x52U /* 寻找天线范围内的全部卡 */
#define PICC_ANTICOLL1 0x93U /* UID 第一级防冲突/选卡命令 */
#define PICC_AUTHENT1A 0x60U /* 使用 Key A 验证扇区密码 */
#define PICC_AUTHENT1B 0x61U /* 使用 Key B 验证扇区密码 */
#define PICC_READ 0x30U /* 读取 MIFARE 数据块 */
#define PICC_WRITE 0xA0U /* 写入 MIFARE 数据块 */
#define PICC_HALT 0x50U /* 让卡片进入休眠状态 */
/* MFRC522 寄存器地址:第 0 页,命令与通信状态寄存器。 */
#define CommandReg 0x01U
#define ComIEnReg 0x02U
#define DivIEnReg 0x03U
#define ComIrqReg 0x04U
#define DivIrqReg 0x05U
#define ErrorReg 0x06U
#define Status1Reg 0x07U
#define Status2Reg 0x08U
#define FIFODataReg 0x09U
#define FIFOLevelReg 0x0AU
#define WaterLevelReg 0x0BU
#define ControlReg 0x0CU
#define BitFramingReg 0x0DU
#define CollReg 0x0EU
/* MFRC522 寄存器地址:第 1 页,发送、接收和调制配置寄存器。 */
#define ModeReg 0x11U
#define TxModeReg 0x12U
#define RxModeReg 0x13U
#define TxControlReg 0x14U
#define TxASKReg 0x15U
#define TxSelReg 0x16U
#define RxSelReg 0x17U
#define RxThresholdReg 0x18U
#define DemodReg 0x19U
#define MifareReg 0x1CU
#define SerialSpeedReg 0x1FU
/* MFRC522 寄存器地址:第 2 页,CRC、射频增益和定时器寄存器。 */
#define CRCResultRegH 0x21U
#define CRCResultRegL 0x22U
#define ModWidthReg 0x24U
#define RFCfgReg 0x26U
#define GsNReg 0x27U
#define CWGsCfgReg 0x28U
#define ModGsCfgReg 0x29U
#define TModeReg 0x2AU
#define TPrescalerReg 0x2BU
#define TReloadRegH 0x2CU
#define TReloadRegL 0x2DU
/* MFRC522 芯片版本寄存器。 */
#define VersionReg 0x37U
/*
* 初始化与诊断接口:
* RC522_Init 初始化 GPIO、复位芯片、配置射频参数并开启天线。
* RC522_GetVersion 读取芯片版本号,常见值为 0x91、0x92 或 0x88。
*/
u8 RC522_Init(void);
u8 RC522_GetVersion(void);
void PcdReset(void);
void PcdAntennaOn(void);
void PcdAntennaOff(void);
/*
* 卡片操作接口:
* PcdRequest 寻卡并读取 2 字节 ATQA 卡片类型。
* PcdAnticoll 执行防冲突,读取 4 字节 UID 和 1 字节校验值。
* PcdSelect 根据 UID 选择一张卡片。
* PcdAuthState 使用 Key A 或 Key B 对指定数据块进行认证。
* PcdRead 从指定块读取 16 字节数据。
* PcdWrite 向指定块写入 16 字节数据。
* PcdHalt 命令当前卡片进入休眠状态。
* PcdStopCrypto1 停止芯片内部 Crypto1 加密单元。
*/
u8 PcdRequest(u8 req_code, u8 *tag_type);
u8 PcdAnticoll(u8 *uid);
u8 PcdSelect(u8 *uid);
u8 PcdAuthState(u8 auth_mode, u8 block_addr, u8 *key, u8 *uid);
u8 PcdRead(u8 block_addr, u8 *data);
u8 PcdWrite(u8 block_addr, u8 *data);
u8 PcdHalt(void);
void PcdStopCrypto1(void);
/*
* 底层寄存器接口:
* 对教学实验开放,可用于观察 MFRC522 寄存器的读、写和位操作过程。
*/
void WriteRawRC(u8 address, u8 value);
u8 ReadRawRC(u8 address);
void SetBitMask(u8 reg, u8 mask);
void ClearBitMask(u8 reg, u8 mask);
#endif
十二、总结
本项目使用 STM32 模拟 SPI 驱动 RC522,完整复现 ISO14443A 射频卡通信全流程,从硬件接线、芯片底层原理、射频无源卡取电通信、协议分层、MIFARE 存储结构到故障排查全覆盖,适合单片机 RFID 入门教学。 RC522 作为射频前端,隔离复杂射频信号处理,主控仅通过简单寄存器指令即可完成读卡,搭配 OLED 直观展示 UID 与卡片类型,代码模块化、易拓展,可二次开发门禁、刷卡记录、身份识别等小型项目。
更多推荐



所有评论(0)