简介:本资源是一套基于STM32F103RB微控制器与MFRC522 RFID读卡模块的嵌入式开发实践项目,面向嵌入式初学者及STM32课程设计者,解决非接触式IC卡ID读取与串口显示的核心功能实现问题,适用于门禁模拟、考勤原型等教学与验证场景。压缩包共155个文件,以34个C源文件和35个头文件(.c/.h)构成主程序与外设驱动主体,辅以22个依赖文件(.d)、21个编译中间文件(.crf)及固件库(STM32F10x_FWLib)、硬件抽象层(HARDWARE)、用户逻辑(USER)等结构化目录,完整呈现KEIL5工程组织规范;包体大小为572KB,轻量易导入。已有1755人学习下载。读者可直接获取可编译运行的KEIL5工程(含uvprojx工程文件)、SPI通信与RC522底层驱动代码、串口调试输出逻辑、keilkilll.bat一键清理脚本,以及README.TXT说明文档,无需从零配置即可快速上手RFID基础应用开发。
1. RC522 模块在 STM32F103RB 上跑通:不是接上线就能读卡,SPI 时序、电源噪声和寄存器初始化顺序才是真门槛
你手头有一块常见的 MFRC522 RFID 模块,芯片是 NXP 的 RC522,主控选了 STM32F103RB(LQFP64 封装,64KB Flash,20KB RAM),开发环境是 Keil MDK-ARM v5.x —— 这套组合在嵌入式教学、门禁原型、学生毕设和小型工控终端里太常见了。但很多人卡在「能编译、能烧录、串口有输出,就是永远读不到卡片」这一步。根本原因不是代码写错了,而是 RC522 对 SPI 通信的容忍度极低:它不接受高位先发(MSB-first)以外的模式,不能容忍 SCK 空闲电平错配,对 MISO 建立时间(tSU:MISO)和采样边沿极其敏感;更隐蔽的是,模块供电若混入 DC-DC 开关噪声,哪怕电压纹波只有 30mVpp,也会让内部 PLL 失锁,导致PICC_REQA命令超时返回STATUS_TIMEOUT。这不是玄学,是数据手册第 12.3 节明确标注的电气特性约束。本文面向已能点亮 LED、会配置 GPIO 和基本 SPI 的开发者,目标很实在:用最简硬件连接 + 最小可运行固件,在 Keil5 下实测通过 RC522 读取 MIFARE Classic 1K 卡 UID,并把每一步背后的“为什么必须这样”讲透——包括你查不到的寄存器初始化顺序陷阱、Keil 工程里容易被忽略的启动文件配置项、以及示波器上怎么一眼看出 SPI 时序是否合规。
2. 从原理到引脚:为什么 RC522 必须走硬件 SPI,且 STM32F103RB 的 SPI1 是最优解
2.1 RC522 的通信本质:不是普通外设,是带状态机的射频前端
RC522 不是简单的 SPI 存储器。它的内部结构包含三大部分:
- 射频模拟前端(RF Analog Front-End):负责 13.56MHz 载波生成、ASK 调制/解调、天线匹配网络控制;
- 数字基带处理器(Digital Baseband):执行 ISO/IEC 14443-A 协议栈(防冲突、Select、Auth、Read/Write);
- SPI 接口逻辑(SPI Interface Logic):仅提供寄存器访问通道,所有射频操作均由 CPU 写入特定寄存器触发,无自动 DMA 或中断响应机制。
这意味着:CPU 必须严格按手册 Table 22(Register Map)逐字节写入控制寄存器(如CommandReg、ComIrqReg),再轮询状态位(如IRqbit),才能推进协议流程。任何 SPI 传输错误(如丢字节、多字节、时钟相位错)都会导致状态机卡死。这也是为什么软件模拟 SPI(bit-banging)在 F103 上几乎不可行:即使主频 72MHz,GPIO 翻转延迟 + 循环开销也难以稳定满足 RC522 要求的最小 SCK 周期(典型值 100ns,即最高支持 10MHz SCK)。而硬件 SPI 可将时序完全交由外设控制器保障,释放 CPU 去处理协议逻辑。
2.2 STM32F103RB 的 SPI 资源选型:为什么锁定 SPI1,而非 SPI2 或 SPI3
F103RB 共有 2 个全功能 SPI(SPI1、SPI2),SPI3 仅在高密度型号存在(RB 不含)。关键差异在时钟源与引脚复用:
| SPI 外设 | 时钟源 | 最高波特率(理论) | 推荐引脚(AFIO) | 是否支持 NSS 硬件管理 |
|---|---|---|---|---|
| SPI1 | APB2(72MHz) | 18MHz | PA4(NSS), PA5(SCK), PA6(MISO), PA7(MOSI) | ✅(NSS 由 SPI1_NSS 引脚驱动) |
| SPI2 | APB1(36MHz) | 9MHz | PB12(NSS), PB13(SCK), PB14(MISO), PB15(MOSI) | ✅(需额外配置 GPIO) |
RC522 数据手册明确要求 SCK 频率 ≤ 10MHz(Section 10.1.2),但留有余量。SPI1 的 18MHz 理论上限意味着我们可用分频系数BR=000(2分频 → 36MHz)或BR=001(4分频 → 18MHz)轻松覆盖需求,且 APB2 总线更少受其他外设干扰。更重要的是:PA4 作为 SPI1_NSS 引脚,可配置为硬件 NSS 输出模式(SSM=0, SSI=1),由 SPI 外设自动拉低/拉高,彻底规避软件控制 NSS 时因中断延迟导致的帧同步失败——这是大量初学者“读卡失败”的根源。而 SPI2 的 PB12 需手动 GPIO 控制,一旦在HAL_SPI_TransmitReceive()中断中被抢占,NSS 电平维持时间不足,RC522 直接复位。
提示:F103RB 的 PA4 默认是 JTAG_TMS 功能,若未禁用 SWD/JTAG,PA4 会被强拉为输入态,导致 NSS 无法拉低。必须在
SystemInit()后立即执行__HAL_AFIO_REMAP_SWJ_NOJTAG();关闭 JTAG,仅保留 SWD。
2.3 硬件连接表:RC522 模块引脚与 STM32F103RB 的精确映射
| RC522 引脚 | 功能说明 | STM32F103RB 引脚 | 连接要点 |
|---|---|---|---|
| SDA | SPI 片选(低有效) | PA4 | 必须接 PA4!不可用任意 GPIO 模拟,否则 NSS 时序失控 |
| SCK | SPI 时钟 | PA5 | 若使用杜邦线,长度 ≤ 10cm,避免时钟反射 |
| MOSI | 主机输出从机输入 | PA7 | 串联 33Ω 电阻(靠近 STM32 端),抑制高频振铃 |
| MISO | 主机输入从机输出 | PA6 | 串联 33Ω 电阻(靠近 RC522 端),并联 100pF 电容至 GND(滤除 RF 耦合噪声) |
| GND | 信号地 | PGND | 必须与 RC522 模块 GND 直连,禁止经 PCB 铜皮长距离走线 |
| 3.3V | 电源(2.5~3.6V) | VDD_3V3 | 严禁从 USB 5V 经 AMS1117-3.3 降压直供!必须加 10μF 钽电容 + 100nF 陶瓷电容滤波 |
| RST | 复位(低有效) | PB0 | 上拉 10kΩ 至 3.3V,复位脉冲宽度 ≥ 100ns(由HAL_GPIO_WritePin()保证) |
注意:RC522 模块自带天线,但 PCB 天线效率受周围金属影响极大。测试时确保模块远离 USB 线、金属外壳、手机,最佳距离为 3~5cm 空气间隙。
3. Keil5 工程搭建:从新建工程到跑通第一个PCD_ReadRegister
3.1 创建最小化 Keil5 工程:禁用所有非必要组件
- 打开 Keil uVision5 → Project → New µVision Project → 选择
STM32F103RB(CMSIS Device Family Pack 已安装); - 在
Manage Run-Time Environment中,仅勾选:CMSIS::Core(必需)Device::Startup(必需)Device::StdPeriph Drivers(必需,用于 RCC/GPIO/SPI 初始化)- 取消勾选
CMSIS::RTOS、Middleware::USB、Device::DSP等所有无关项;
- 添加启动文件:
startup_stm32f10x_md.s(对应 Medium-density,F103RB 属此类); - 新建
main.c,并添加以下头文件:
#include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" #include "stm32f10x_spi.h" #include <string.h>3.2 RCC 与 GPIO 初始化:时钟使能顺序与推挽速度的硬性要求
RC522 对信号边沿陡峭度敏感,GPIO 必须配置为推挽输出(PP)、高速(50MHz),否则 SCK 上升/下降时间过长,导致 RC522 采样错误。初始化代码如下:
void RCC_Configuration(void) { // 使能 APB2 总线(SPI1, GPIOA, AFIO) RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // 使能 APB1 总线(仅需 SysTick,此处省略) } void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; // 配置 PA4(NSS), PA5(SCK), PA6(MISO), PA7(MOSI) 为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 必须 50MHz! GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置 PB0(RST) 为通用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 关闭 JTAG,释放 PA4/PA5/PA6/PA7 __HAL_AFIO_REMAP_SWJ_NOJTAG(); }参数说明:
GPIO_Speed_50MHz是关键!若设为2MHz,SCK 边沿缓慢,RC522 内部采样点偏移,CommandReg写入失败率 > 90%;__HAL_AFIO_REMAP_SWJ_NOJTAG()必须在GPIO_Init()之后调用,否则 PA4 复用功能不生效;RCC_APB2Periph_SPI1必须显式使能,否则 SPI1 外设时钟关闭,寄存器读写返回 0。
3.3 SPI1 初始化:4 个必须死守的参数
RC522 仅支持Mode 0(CPOL=0, CPHA=0),即空闲时 SCK 为低,数据在 SCK 第一个上升沿采样。SPI 初始化代码:
void SPI1_Configuration(void) { SPI_InitTypeDef SPI_InitStructure; // 复位 SPI1 外设 SPI_I2S_DeInit(SPI1); // 配置 SPI1 参数 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第一上升沿采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Hard; // 硬件 NSS(PA4) SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // SCK = 72MHz / 4 = 18MHz → 实际用 10MHz 分频 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB-first(强制!) SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); // 使能 SPI1 }关键参数解析:
SPI_NSS_Hard:启用硬件 NSS,PA4 由 SPI1 自动控制;SPI_BaudRatePrescaler_4:分频后 SCK=18MHz,但 RC522 实际工作在 10MHz,需在后续SPI_SetBaudRatePrescaler()动态调整(见 3.4 节);SPI_FirstBit_MSB:RC522 寄存器地址和数据均为 MSB-first,若设为 LSB-first,地址解析错误,0x01变成0x80,直接访问越界寄存器;SPI_CPHA_1Edge:必须为第一边沿(上升沿),若误设为SPI_CPHA_2Edge(第二边沿),MISO 数据在 SCK 下降沿才稳定,但 RC522 已开始采样,读出全 0xFF。
3.4 编写底层 SPI 读写函数:带超时的单字节收发
RC522 的 SPI 读写是地址+数据的两阶段操作:
- 写寄存器:发送
0x80 | addr(写标志位 0x80 置位) +data; - 读寄存器:发送
addr(无 0x80) + 读回data。
因此需封装两个原子函数,且必须加入超时保护(防止 SPI 挂死):
#define SPI_TIMEOUT 0x1000 uint8_t SPI1_ReadWriteByte(uint8_t tx_data) { uint16_t timeout = SPI_TIMEOUT; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { if (--timeout == 0) return 0xFF; // 超时返回错误码 } SPI_I2S_SendData(SPI1, tx_data); timeout = SPI_TIMEOUT; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) { if (--timeout == 0) return 0xFF; } return SPI_I2S_ReceiveData(SPI1); } // 写单字节寄存器 void PCD_WriteRegister(uint8_t reg, uint8_t value) { uint8_t addr = reg & 0x7F; // 清除最高位(写标志) SPI1_ReadWriteByte(0x80 | addr); // 发送写地址 SPI1_ReadWriteByte(value); // 发送数据 } // 读单字节寄存器 uint8_t PCD_ReadRegister(uint8_t reg) { uint8_t addr = reg & 0x7F; SPI1_ReadWriteByte(addr); // 发送读地址 return SPI1_ReadWriteByte(0x00); // 发送 dummy byte 并读回 }逻辑说明:
PCD_WriteRegister()中reg & 0x7F确保地址不带写标志,再0x80 | addr显式置位,避免地址计算错误;PCD_ReadRegister()的SPI1_ReadWriteByte(0x00)是关键:RC522 在收到地址后,需主机发送一个 dummy 字节才能触发 MISO 输出数据,此为标准 SPI 从设备行为;- 超时值
0x1000经实测,在 72MHz 主频下足够覆盖 10MHz SCK 的 8 位传输(约 0.8μs),过大则掩盖硬件故障,过小则误判。
4. RC522 初始化避坑指南:5 个让 90% 开发者翻车的寄存器陷阱
4.1 现象:PCD_ReadRegister(0x00)返回0x00,而非手册标称的0x11(RC522 版本号)
原因:SPI1 未使能,或SPI_Cmd(SPI1, ENABLE)被注释掉;更隐蔽的是RCC_APB2PeriphClockCmd()中漏掉了RCC_APB2Periph_SPI1。
解决:用万用表测 PA4 电压,正常初始化后应为 3.3V(NSS 高电平),执行PCD_WriteRegister()时瞬时拉低至 0V。若始终为 3.3V,检查时钟使能和SPI_Cmd()。
4.2 现象:PCD_ReadRegister(0x04)(CommandReg)返回0x00,但写入后读回仍是0x00
原因:RC522 复位未完成。手册规定 RST 引脚需保持低电平 ≥ 100ns,再拉高 ≥ 5ms 才进入就绪态。若HAL_GPIO_WritePin(GPIOB, GPIO_Pin_0, GPIO_PIN_SET)后立即读寄存器,RC522 内部 PLL 未锁定。
解决:在PCD_Reset()函数中插入硬延时:
void PCD_Reset(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 拉低 RST for(volatile uint32_t i=0; i<1000; i++); // ~10us @72MHz HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 拉高 RST HAL_Delay(5); // 确保 ≥5ms }4.3 现象:PCD_Request()返回STATUS_TIMEOUT,示波器显示 SCK 有波形但 MISO 恒为高电平
原因:RC522 的TModeReg(0x2A)和TPrescalerReg(0x2B)未正确配置,导致内部定时器无法启动载波。这是最隐蔽的坑——很多开源库直接忽略这两个寄存器。
解决:在初始化末尾强制写入:
PCD_WriteRegister(0x2A, 0x80); // TModeReg: 启用定时器,TReloadValH = 0x80 PCD_WriteRegister(0x2B, 0x3E); // TPrescalerReg: 分频系数 62 → 载波周期 ≈ 13.56MHz PCD_WriteRegister(0x2D, 0x00); // TReloadValL: 低字节清零 PCD_WriteRegister(0x2C, 0x00); // TReloadValH: 高字节清零(实际用 0x2A 设置)血泪经验:不配
TModeReg,RC522 根本不发射载波,卡片无法上电,自然REQA超时。此步骤必须在PCD_AntennaOn()之前执行。
4.4 现象:串口打印UID: 00 00 00 00,但卡片真实 UID 是A1 B2 C3 D4
原因:PCD_AntennaOn()中TxControlReg(0x14)未设置正确的天线增益。默认值0x00使 TX 电流过小,卡片无法获得足够能量唤醒。
解决:写入推荐值0x03(增强 TX 电流):
PCD_WriteRegister(0x14, 0x03); // TxControlReg: 设置 TX 电流增益4.5 现象:同一张卡,第一次读取成功,第二次PCD_Request()返回STATUS_COLLISION
原因:未执行PCD_StopCrypto1()(写0x00到CommandReg)清除上一次认证状态。RC522 的 Crypto1 协处理器状态残留,导致防冲突算法误判。
解决:每次读卡流程结束前,强制停止加密:
PCD_WriteRegister(0x01, 0x00); // CommandReg = Idle5. 完整读卡流程实现:从上电到 UID 解析的 7 步闭环
5.1 主循环框架:状态机驱动,拒绝阻塞式延时
为避免HAL_Delay()阻塞导致错过卡片响应,采用轮询状态机:
typedef enum { STATE_IDLE, STATE_RESET, STATE_INIT, STATE_REQUEST, STATE_ANTICOLL, STATE_SELECT, STATE_READ_UID } PCD_StateTypeDef; PCD_StateTypeDef g_PCD_State = STATE_IDLE; uint8_t g_UID[4] = {0}; void PCD_MainLoop(void) { static uint32_t tick = 0; switch(g_PCD_State) { case STATE_IDLE: if (tick++ > 10000) { // 约 10ms g_PCD_State = STATE_RESET; tick = 0; } break; case STATE_RESET: PCD_Reset(); g_PCD_State = STATE_INIT; break; case STATE_INIT: PCD_Init(); g_PCD_State = STATE_REQUEST; break; case STATE_REQUEST: if (PCD_Request(PICC_REQA, &g_UID[0]) == STATUS_OK) { g_PCD_State = STATE_ANTICOLL; } break; case STATE_ANTICOLL: if (PCD_Anticoll(&g_UID[0]) == STATUS_OK) { g_PCD_State = STATE_SELECT; } break; case STATE_SELECT: if (PCD_Select(&g_UID[0]) == STATUS_OK) { g_PCD_State = STATE_READ_UID; } break; case STATE_READ_UID: // UID 已在 Anticoll 阶段获取,此处可打印 printf("UID: %02X %02X %02X %02X\r\n", g_UID[0], g_UID[1], g_UID[2], g_UID[3]); g_PCD_State = STATE_IDLE; break; } }5.2PCD_Request()实现:ISO14443-A 的 REQ/ATQA 流程
#define PICC_REQA 0x26 // Request command, 7 bits #define STATUS_OK 0 #define STATUS_ERROR 1 #define STATUS_TIMEOUT 2 uint8_t PCD_Request(uint8_t req_code, uint8_t *atqa) { uint8_t status; uint8_t irq_en = 0x04; // Allow IRQ on RxIRq (bit 2) uint8_t wait_for = 0x04; // Wait for RxIRq // Step 1: 清空 FIFO PCD_WriteRegister(0x02, 0x80); // Clear FIFO buffer // Step 2: 设置 IRQ 使能 PCD_WriteRegister(0x04, irq_en); // Step 3: 设置命令为 REQ PCD_WriteRegister(0x01, 0x0C); // Transceive command, no CRC // Step 4: 写入请求码(7 bits) PCD_WriteRegister(0x09, req_code); // BitFramingReg: 7 bits // Step 5: 启动命令 PCD_WriteRegister(0x01, 0x0C); // CommandReg = Transceive // Step 6: 等待 RxIRq uint16_t i = 5000; while ((PCD_ReadRegister(0x04) & wait_for) == 0) { if (--i == 0) return STATUS_TIMEOUT; } // Step 7: 读取 ATQA (2 bytes) atqa[0] = PCD_ReadRegister(0x0A); // FIFO data register atqa[1] = PCD_ReadRegister(0x0A); return STATUS_OK; }参数说明:
req_code = 0x26是 ISO14443-A 的REQA命令,用于唤醒所有 Type A 卡;PCD_WriteRegister(0x09, req_code)写入BitFramingReg,指定发送 7 位,因REQA无 CRC;PCD_ReadRegister(0x0A)从 FIFO 读取数据,RC522 自动将接收到的 ATQA(0x04 0x00)存入 FIFO。
5.3PCD_Anticoll():获取 UID 的核心,4 字节防冲突
uint8_t PCD_Anticoll(uint8_t *uid) { uint8_t status; uint8_t i, ser_num_check = 0; uint8_t serial_number[5]; // 4 bytes UID + 1 byte BCC // Step 1: 清空 FIFO PCD_WriteRegister(0x02, 0x80); // Step 2: 设置 BitFramingReg 为 7 bits PCD_WriteRegister(0x09, 0x20); // 7 bits, no CRC // Step 3: 发送 Anticollision command (0x93, 0x20) PCD_WriteRegister(0x01, 0x00); // Idle first PCD_WriteRegister(0x01, 0x0C); // Transceive // Send 0x93 (Anticollision) + 0x20 (slot 0) PCD_WriteRegister(0x09, 0x93); PCD_WriteRegister(0x09, 0x20); // Step 4: 等待 RxIRq uint16_t i_timeout = 5000; while ((PCD_ReadRegister(0x04) & 0x04) == 0) { if (--i_timeout == 0) return STATUS_TIMEOUT; } // Step 5: 读取 5 字节(UID 4B + BCC 1B) for (i = 0; i < 5; i++) { serial_number[i] = PCD_ReadRegister(0x0A); } // Step 6: 校验 BCC (XOR of first 4 bytes) for (i = 0; i < 4; i++) { ser_num_check ^= serial_number[i]; } if (ser_num_check != serial_number[4]) return STATUS_ERROR; // Step 7: 复制 UID for (i = 0; i < 4; i++) { uid[i] = serial_number[i]; } return STATUS_OK; }关键细节:
PCD_WriteRegister(0x09, 0x20)设置BitFramingReg为 7 位,因ANTICOLL命令后跟 slot 号(1 bit),共 8 位,但 RC522 要求BitFramingReg指定总位数;- BCC(Block Check Character)校验是强制步骤,若失败说明 UID 传输错误,必须重试;
serial_number[4]是 BCC,serial_number[0..3]是 UID,顺序与卡片物理 UID 一致。
5.4PCD_Select():确认 UID 并激活卡片
uint8_t PCD_Select(uint8_t *uid) { uint8_t i; uint8_t cmd_buffer[9]; // 1 byte cmd + 4 byte UID + 1 byte BCC + 2 byte CRC uint8_t result[2]; // Build command: 0x93 + UID[0..3] + BCC cmd_buffer[0] = 0x93; for (i = 0; i < 4; i++) { cmd_buffer[1+i] = uid[i]; } // Calculate BCC cmd_buffer[5] = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]; // Send command PCD_WriteRegister(0x02, 0x80); // Clear FIFO for (i = 0; i < 6; i++) { PCD_WriteRegister(0x09, cmd_buffer[i]); // Write to FIFO } // Set BitFramingReg for 6 bytes (48 bits) PCD_WriteRegister(0x09, 0x70); // 48 bits, no CRC PCD_WriteRegister(0x01, 0x0C); // Transceive // Wait for response uint16_t i_timeout = 5000; while ((PCD_ReadRegister(0x04) & 0x04) == 0) { if (--i_timeout == 0) return STATUS_TIMEOUT; } // Read 2-byte SAK result[0] = PCD_ReadRegister(0x0A); result[1] = PCD_ReadRegister(0x0A); return STATUS_OK; }注意:PCD_Select()返回 SAK(Select Acknowledge),result[0]的 bit0 表示是否为 MIFARE Classic,但本例仅需确认卡片被选中,故返回STATUS_OK即可。
6. 实战验证与进阶技巧:用逻辑分析仪抓包定位时序问题,以及低功耗唤醒方案
6.1 用 Saleae Logic 抓取 SPI 波形:3 个必看信号眼
当读卡失败时,不要盲目改代码,先抓波形。连接 Saleae 逻辑分析仪:
- Channel 0: PA4 (NSS)
- Channel 1: PA5 (SCK)
- Channel 2: PA7 (MOSI)
- Channel 3: PA6 (MISO)
关键观察点:
- NSS 时序:查看
PCD_WriteRegister()执行时,PA4 是否在 SCK 第一个脉冲前 ≥ 100ns 拉低,并在最后一个 SCK 脉冲后 ≥ 100ns 拉高。若 NSS 过早释放,RC522 会终止当前事务; - SCK 稳定性:测量 SCK 周期抖动,若 > 5%(即 18MHz 下抖动 > 2.7ns),检查电源滤波电容是否虚焊;
- MISO 数据有效性:在 SCK 上升沿时刻,MISO 电平是否已稳定?若在上升沿跳变,说明 RC522 的 tSU:MISO(MISO 建立时间)不足,需降低 SCK 频率(改
SPI_BaudRatePrescaler_8→ 9MHz)。
6.2 降低功耗:RC522 的休眠与唤醒策略
F103RB 常用于电池供电设备,RC522 的典型工作电流为 13mA,休眠电流仅 10μA。实现方法:
// 进入休眠 void PCD_Sleep(void) { PCD_WriteRegister(0x01, 0x05); // CommandReg = SoftReset + PowerDown } // 唤醒(需先拉低 RST 100ns 再拉高) void PCD_WakeUp(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); for(volatile uint32_t i=0; i<100; i++); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_Delay(1); PCD_Init(); // 重新初始化 }技巧:在STATE_IDLE状态停留 5 秒后调用PCD_Sleep(),检测到外部中断(如按键)再唤醒,整机待机电流可降至 20μA 以下。
6.3 抗金属干扰:天线匹配电容的实测调整法
RC522 模块在靠近金属表面时,天线谐振频率偏移,导致读卡距离骤减。解决方案:
- 模块背面贴铜箔屏蔽层(仅覆盖 RF IC,避开天线区域);
- 动态调整匹配电容:原模块 C1/C2 为 27pF,实测发现改为 22pF + 10pF 并联(总 32pF)后,在铝板上读卡距离从
本文还有配套的精品资源,点击获取