简介:面向嵌入式开发者的STM32与RC522 RFID刷卡方案,完整实现非接触式MIFARE卡片识别与唯一ID读取。工程基于STM32标准外设库,通过SPI接口与RC522读卡芯片通信,涵盖SPI时钟极性和相位配置、寄存器初始化、中断响应与CRC校验,并集成USART串口和OLED显示模块,便于调试与状态呈现,适合学习STM32外设驱动、射频协议栈及刷卡应用开发的读者参考。压缩包共148个文件,以C源码、头文件及编译过程生成的中间文件为主,另含可烧录的hex/axf固件和完整工程配置,RAR压缩包仅2.78MB,便于直接移植或二次开发。目前已有5347人学习,除核心驱动外还保留清晰工程目录,可帮助理解MFRC522防冲突算法、数据帧解析与多卡片识别处理,是一份从入门到进阶的实用参考资料。
1. STM32 接 RC522 刷卡模块:从点不亮到稳定读卡,一次讲透
做嵌入式的都知道,RC522 这块 13.56MHz 的 RFID 读卡芯片,几乎是门禁、考勤、消费机和小车识别项目的默认选项。模块本身十几块钱,但真正接进 STM32 工程里,你才会发现一堆文档没写透的细节:SPI 模式选错读回来全是 F;寄存器初始化顺序不对,天线就是不起振;就算卡放上去了,寻卡指令也可能一直超时。这篇笔记就把我拆过的「STM32+RC522 刷卡模块」资源包里的核心流程、寄存器操作和代码骨架完整过一遍。适合正在做刷卡门禁、校园卡读取或嵌入式入门的学生和工程师,照着接线、改两处宏定义,就能在串口里看到卡号。
2. 硬件连接与 SPI 配置:接线顺序、引脚映射和初始化时序
RC522 模块的接口非常简单,但我见过太多人把 SDA、SCK、MOSI、MISO 接反,或者忽略了 IRQ 引脚导致中断模式死活不触发。这一章先解决硬件层,把接线和 SPI 初始化的关键参数敲定。
2.1 引脚映射:模块上的 8 个引脚分别接哪里
RC522 模块对外引出的引脚一般是 SDA(片选)、SCK(时钟)、MOSI、MISO、IRQ、GND、RST、VCC。这里的 SDA 不是 I2C 数据线,而是 SPI 的 NSS 片选,很多第一次用的人会被这个命名带偏。VCC 有 3.3V 和 5V 两种版本,强烈建议用 3.3V 供电,毕竟 STM32 的 GPIO 容忍度并不统一,5V 供电容易把某些引脚的输入电平抬到危险区。
我一般接 PA4-PA7 加 PB0 和 PB1,这样可以避开默认的 SWD 调试引脚。片选单独用一个 GPIO 控制,而不是用 SPI 的 NSS 硬件模式,后面你会在初始化里看到原因。
// rc522_hw.h #define RC522_SPI SPI1 #define RC522_SPI_CLK_ENABLE() __HAL_RCC_SPI1_CLK_ENABLE() #define RC522_NSS_GPIO_PORT GPIOA #define RC522_NSS_PIN GPIO_PIN_4 #define RC522_NSS_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() #define RC522_SCK_GPIO_PORT GPIOA #define RC522_SCK_PIN GPIO_PIN_5 #define RC522_MOSI_GPIO_PORT GPIOA #define RC522_MOSI_PIN GPIO_PIN_7 #define RC522_MISO_GPIO_PORT GPIOA #define RC522_MISO_PIN GPIO_PIN_6 // 片选拉低表示选中模块 #define RC522_NSS_LOW() HAL_GPIO_WritePin(RC522_NSS_GPIO_PORT, RC522_NSS_PIN, GPIO_PIN_RESET) #define RC522_NSS_HIGH() HAL_GPIO_WritePin(RC522_NSS_GPIO_PORT, RC522_NSS_PIN, GPIO_PIN_SET)这里片选一定要用普通 GPIO 手动控制。原因是 RC522 对 NSS 的时序要求是:每条指令传输期间片选全程拉低,指令结束后拉高并等待一段时间。如果用硬件 SPI 的自动片选,芯片在收发完最后一个字节的瞬间就会释放总线,但此时 RC522 还在内部处理数据,状态寄存器的读取时机就错了。手动控制片选,才能精确掐住「发完指令-等处理-读状态-回数据」的节奏。
2.2 SPI 模式选择:为什么必须是 Mode 0,位长为什么是 8 位
RC522 的 SPI 接口兼容 Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),但绝大多数模块的天线电路和固件都按 Mode 0 优化,所以我一律用 Mode 0。这个要说清楚:CPOL=0 表示时钟空闲为低,CPHA=0 表示数据在第一个时钟沿采样。如果你在 STM32 的 CubeMX 里配置 SPI 时选错了相位,表现不一定是完全不通信,更可能是「读 ID 正确但读写寄存器偶尔出错」,这种玄学问题最难定位。
位长必须是 8 位。RC522 的所有寄存器都是 8 位宽,指令帧也是按字节组织的。即使某些资料里写了 16 位模式,也不要开,否则地址自增模式会错位。
// rc522_spi.c void RC522_SPI_Init(void) { SPI_HandleTypeDef hspi; hspi.Instance = SPI1; hspi.Init.Mode = SPI_MODE_MASTER; hspi.Init.Direction = SPI_DIRECTION_2LINES; hspi.Init.DataSize = SPI_DATASIZE_8BIT; hspi.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi.Init.NSS = SPI_NSS_SOFT; hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi); }这条初始化里,BaudRatePrescaler 用的 16 分频。STM32F103 的 APB1 在 72MHz 主频下是 36MHz,16 分频后 SPI 时钟约 2.25MHz,在 RC522 支持的 10MHz 上限以内,留了充足余量。不要一上来就拉满到 8 分频,RC522 的 PCB 天线和走线质量参差不齐,高速下更容易出现字节错位,我调过的某块模块在 4MHz 下读卡率只有 60%,降到 2MHz 就恢复正常了。
2.3 初始化时序:复位与自检的毫秒级要求
RC522 上电后必须先硬件复位,再软件配置。硬件复位就是给 RST 引脚一个至少 100ns 的低脉冲,然后拉高。很多模块板上已经接了复位电路,但为了可靠性我还是在代码里强制做一次。
复位完成后要检查芯片是否在线,最直接的方法是读VersionReg寄存器(地址 0x37),这个寄存器出厂值是 0x92(也有部分批次是 0x91)。如果读出来是 0x00 或 0xFF,说明 SPI 通信链路有问题,后面折腾寄存器全是白费。
// rc522.c uint8_t RC522_Init(void) { // 硬件复位 RC522_NSS_HIGH(); HAL_GPIO_WritePin(RC522_RST_PORT, RC522_RST_PIN, GPIO_PIN_RESET); HAL_Delay(2); HAL_GPIO_WritePin(RC522_RST_PORT, RC522_RST_PIN, GPIO_PIN_SET); HAL_Delay(2); // 发送软件复位命令 RC522_WriteRegister(MR_FIFO_LEVEL_REG, 0x00); RC522_WriteRegister(COMMAND_REG, 0x0F); // PCD_SOFTRESET HAL_Delay(2); RC522_WriteRegister(COMMAND_REG, 0x00); // 回到 IDLE // 读版本号验证通信 uint8_t version = RC522_ReadRegister(0x37); if (version == 0x92 || version == 0x91) { // 版本正确,继续配置天线增益、定时器、中断 } else { return 1; // 初始化失败 } }软件复位命令要写进CommandReg,全片复位后寄存器恢复默认值,紧接着必须重新配置TxModeReg、RxModeReg、ModWidthReg和TModeReg等关键寄存器。我见过有人在软件复位后只配了三个寄存器就开始寻卡,结果防冲突阶段反复超时,直到他把RFCfgReg的天线增益也补上后才正常。这属于芯片手册里没强调的细节。初始化完成后,可以用RC522_AntennaOn()打开天线,再进入寻卡循环。
3. 核心操作流程:寻卡、防冲突、选卡、验证密钥的完整代码
RC522 读一张 M1 卡的流程,业内叫「三轮握手」,其实不止三次:先发送寻卡指令让卡片响应,再防冲突选出唯一的卡号,然后选中该卡,最后验证扇区密钥才能读写数据。每一步都有对应的命令字节,漏掉任何一步,下一步的寄存器状态都会不对。
3.1 寻卡指令 REQA 与 WUPA:什么时候用哪一个
寻卡命令有两个:PICC_REQA(0x26)和PICC_WUPA(0x52)。前者用于唤醒空闲状态的卡片,后者用于唤醒休眠状态的卡片。对刚上电的卡,两者效果一样,但如果卡之前被HALT指令休眠过,只有WUPA才能再次唤醒它。
我看到很多示例代码不分情境,永远用REQA。这在门禁场景里问题不大,因为每次刷卡前卡片都在场上,没有被发过休眠命令。但在身份识别类项目里,如果你有省电逻辑,读卡失败后发过HALT,第二次就必须用WUPA。这个细节不处理好,就会出现「第一次能刷,后面怎么都读不到」的现象。
// 寻卡函数:返回卡类型,0x4400 是 Mifare Classic 1K uint8_t RC522_Request(uint8_t req_mode, uint8_t *card_type) { uint8_t status = MI_ERR; RC522_ClearBitMask(STATUS2_REG, 0x08); // 清 MFCryptO1 标志 RC522_WriteRegister(BIT_FRAMING_REG, 0x07); // 发 7 位帧格式 // 发送 REQA/WUPA 命令 if (RC522_Transceive(req_mode) == 0) { // 读取卡类型两字节 if (RC522_ReadRegister(FIFO_LEVEL_REG) == 2) { card_type[0] = RC522_ReadRegister(FIFO_DATA_REG); card_type[1] = RC522_ReadRegister(FIFO_DATA_REG); status = MI_OK; } } return status; }这里有个细节,BitFramingReg写入 0x07 是为了让发送的 REQA 命令按照 7 位帧格式发出,这是 RFID 协议规定的短帧。如果你把这个寄存器配置成 0x00,卡片不会响应。初次调这块芯片的人,最容易在这个 7 位帧上卡住。
3.2 防冲突与选卡:卡号 4 字节还是 7 字节
防冲突(AntiCollision)的作用是,如果有两张卡同时进入读写器天线范围,只选出其中一张来处理。RC522 的防冲突指令返回的卡号有两种长度:4 字节的短 UID 和 7 字节的长 UID。Mifare Classic 1K 用 4 字节,Mifare Ultralight 也用 4 字节,而某些门禁卡是 7 字节,处理逻辑不一样。
读卡号的过程中还要做校验,4 字节 UID 后面带一个 BCC 校验字节,算法是对前 4 字节做异或。网上很多代码直接把校验字节也当成卡号存进数组,结果就是打印的卡号长度和实际不符,后续按卡号发选卡指令也会出错。
// 防冲突:读 4 字节 UID + 1 字节 BCC uint8_t RC522_AntiCollision(uint8_t *card_id) { uint8_t status = MI_ERR; uint8_t i; RC522_WriteRegister(BIT_FRAMING_REG, 0x00); RC522_WriteRegister(TRANSCEIVE_REG, 0x00); // 发送防冲突命令 0x93 uint8_t cmd = 0x93; RC522_ClearBitMask(STATUS2_REG, 0x08); RC522_WriteRegister(FIFO_LEVEL_REG, 0x00); RC522_WriteRegister(FIFO_DATA_REG, cmd); RC522_WriteRegister(FIFO_DATA_REG, 0x20); // NVB: 表示 7 字节的卡号 + 完整 BCC RC522_WriteRegister(COMMAND_REG, 0x1E); // PCD_TRANSCEIVE RC522_SetBitMask(BIT_FRAMING_REG, 0x80); // 启动位帧发送 if (RC522_WaitForCardReady() == MI_OK) { uint8_t len = RC522_ReadRegister(FIFO_LEVEL_REG); if (len == 5) { // 4 字节卡号 + 1 字节 BCC for (i = 0; i < 4; i++) { card_id[i] = RC522_ReadRegister(FIFO_DATA_REG); } uint8_t bcc = RC522_ReadRegister(FIFO_DATA_REG); // 校验 BCC 是否等于前 4 字节异或 uint8_t calc = card_id[0] ^ card_id[1] ^ card_id[2] ^ card_id[3]; if (calc == bcc) { status = MI_OK; } } } return status; }NVB(Number of Valid Bits)是防冲突协议里一个很关键的参数。0x20 表示前 4 个字节(32 位)有效,如果改成 0x70 就表示发 7 字节完整 UID。对长 UID 的卡,防冲突需要分两轮:第一轮发 0x93 和 0x20 读前 4 字节,第二轮再用 0x95 和 0x70 读完整 UID。代码里只做 4 字节版本最通用,但你要知道这个边界:遇到 7 字节的卡,这函数会返回错误。
3.3 选卡与密钥验证:三次握手的最后一环
拿到卡号后,要向卡片发送选卡指令,告诉它「接下来我要对你进行操作」。选卡命令是0x93、0x70,后面跟 5 字节(UID + BCC)。选卡成功后,卡片会返回 SAK(Select Acknowledge)字节,用于识别卡类型。Mifare 1K 的 SAK 是 0x08,Mifare 4K 是 0x18,Ultralight 是 0x00。
选卡之后还不能直接读写数据,必须先验证扇区密钥。M1 卡每 16 个字节为一个块,4 个块组成一个扇区,每个扇区有独立的 A 密钥和 B 密钥。出厂默认密钥是 6 个字节的0xFF。验证密钥使用PCD_AUTHENT1指令(0x60 表示验证 A 密钥,0x61 表示验证 B 密钥)。
// 验证扇区密钥:key_type 传 0x60 或 0x61 uint8_t RC522_Authentication(uint8_t auth_mode, uint8_t block_addr, uint8_t *key, uint8_t *uid) { uint8_t status = MI_ERR; // 第一步:把卡号放入 FIFO RC522_WriteRegister(FIFO_LEVEL_REG, 0x00); for (uint8_t i = 0; i < 4; i++) { RC522_WriteRegister(FIFO_DATA_REG, uid[i]); } // 第二步:写入密钥和块地址 for (uint8_t i = 0; i < 6; i++) { RC522_WriteRegister(FIFO_DATA_REG, key[i]); } RC522_WriteRegister(FIFO_DATA_REG, block_addr); // 第三步:执行认证命令 RC522_WriteRegister(COMMAND_REG, auth_mode); RC522_SetBitMask(BIT_FRAMING_REG, 0x80); if (RC522_WaitForCardReady() == MI_OK) { uint8_t val = RC522_ReadRegister(STATUS2_REG) & 0x08; if (val == 0) { status = MI_OK; // 认证成功 } } return status; }这个函数最容易被忽略的是:认证命令发出去后,不能立刻判断成功,得去读Status2Reg的 bit3(MFCryptO1 标志)。这个标志位在认证成功后会被硬件自动置 1,读操作完成后自动清零。我最早调的时候看中断标志位来判断,结果永远成功,后来才发现根本看错了寄存器。
4. 读写数据块:从寄存器到字节数组的完整解析
认证通过后,读写 M1 卡的数据块就简单了,但细节仍然很多。块地址的选择、写块前的数据格式、以及读回来数据的解析方式,每一项都是坑。
4.1 块地址与存储区划分
Mifare Classic 1K 有 16 个扇区,每扇区 4 块,共 64 个块,每块 16 字节。扇区 0 的块 0 存储的是厂商信息(UID 和厂商数据),出厂时被锁定为只读,普通读写命令动不了它。每个扇区的第 4 块(即块号模 4 等于 3)是厂商块,存放 A 密钥、访问控制位和 B 密钥。读写数据一般从块 1、块 2 开始。
块号换算公式:块号 = 扇区号 × 4 + 块索引。比如扇区 2 的数据块就是块 8、9、10。很多人直接把验证函数里的block_addr当绝对地址用,写扇区 2 的数据时传了 6,结果操作的是扇区 1 的厂商块,改坏了卡的访问控制位,卡直接变砖。这是我在调试时踩过的最痛的一个坑:一张卡扇区 1 的访问控制位被我改成 0xFF,读卡器再也没法认证那个扇区。
4.2 读块与写块的代码骨架
读块的过程是:把块地址写入 FIFO,发送PCD_TRANSCEIVE,然后从 FIFO 读 16 字节。写块更复杂,要先发PCD_WRITE命令,等待卡返回 4 位 ACK,再把 16 字节数据分成两帧发出去。这里的关键是写块时卡片的 ACK 不是普通数据帧,而是 4 位长的短帧,必须在BitFramingReg里设置正确的接收位长度。
// 读一个块,数据存入 buf,长度固定 16 字节 uint8_t RC522_ReadBlock(uint8_t block_addr, uint8_t *buf) { uint8_t status = MI_ERR; RC522_WriteRegister(FIFO_LEVEL_REG, 0x00); RC522_WriteRegister(FIFO_DATA_REG, 0x30); // PICC_READ RC522_WriteRegister(FIFO_DATA_REG, block_addr); RC522_WriteRegister(COMMAND_REG, 0x1E); // PCD_TRANSCEIVE RC522_SetBitMask(BIT_FRAMING_REG, 0x80); if (RC522_WaitForCardReady() == MI_OK) { uint8_t len = RC522_ReadRegister(FIFO_LEVEL_REG); if (len == 16) { for (uint8_t i = 0; i < 16; i++) { buf[i] = RC522_ReadRegister(FIFO_DATA_REG); } status = MI_OK; } } return status; }读块命令的0x30是指令码,紧跟其后的block_addr是目标块号。写块命令是0xA0,两者只差一个字节,但执行时序差别很大。写块时,芯片在收到卡片的 4 位 ACK 后要立即把数据写入 FIFO,期间片选不能拉高,否则卡片等待超时。下面的写块代码里我在 ACK 等待后直接连续写 16 字节,没有做任何延时,这是经过实测的——任何超过 1ms 的间隔都会让卡片放弃写入。
// 写一个块(16 字节) uint8_t RC522_WriteBlock(uint8_t block_addr, uint8_t *buf) { uint8_t status = MI_ERR; uint8_t i; RC522_WriteRegister(FIFO_LEVEL_REG, 0x00); RC522_WriteRegister(FIFO_DATA_REG, 0xA0); // PICC_WRITE RC522_WriteRegister(FIFO_DATA_REG, block_addr); RC522_WriteRegister(COMMAND_REG, 0x1E); RC522_SetBitMask(BIT_FRAMING_REG, 0x80); if (RC522_WaitForCardReady() == MI_OK) { // 收到 4 位 ACK uint8_t len = RC522_ReadRegister(FIFO_LEVEL_REG); if (len == 1) { // 清空 FIFO,写入 16 字节数据 RC522_WriteRegister(FIFO_LEVEL_REG, 0x00); for (i = 0; i < 16; i++) { RC522_WriteRegister(FIFO_DATA_REG, buf[i]); } // 再次发送,注意此时命令寄存器是 0x1E 加 0x80 标志 RC522_WriteRegister(COMMAND_REG, 0x1E); RC522_SetBitMask(BIT_FRAMING_REG, 0x80); if (RC522_WaitForCardReady() == MI_OK) { // 检查写成功标志 uint8_t val = RC522_ReadRegister(STATUS2_REG) & 0x04; if (val == 0) { status = MI_OK; } } } } return status; }写完后检查的0x04是Status2Reg的ModemState相关位,实际含义是 CRC 和调制状态正常。我见过有人在写完后直接延时 5ms 就认为成功,结果数据写进去了,但卡上的访问控制位被破坏,后续读出来全是 0。所以写块之后必须校验:要么读一遍对比,要么检查状态寄存器,二选一,不能省。
4.3 数据解析:大端小端、UID 格式化与容量扩展
从 FIFO 读出来的数据是原始字节流,M1 卡的数据在卡上存储时通常按块内偏移逐个字节存放,不需要字节交换。但 UID 的呈现方式是另一回事。早期很多板载例程直接把 UID 按「小端模式」打印,也就是把读到的第一字节放最后。如果你要做上位机比对,建议统一成「大端字符串」格式,即按读出的顺序作为卡号。这里还有个容易混淆的点:RC522 读出的 UID 顺序和卡片上喷涂的 UID 顺序相反,打印时要决定用哪种,并在代码注释里写清楚,否则两个月后你自己都看不懂日志。
容量扩展方面,M1 1K 只有 1024 字节,实际可用存储去掉厂商块和厂商块,约 752 字节。如果项目要存考勤记录或配置参数,这点空间捉襟见肘。我一般会把数据按「记录头 + 记录体」的格式写入,并在每块末尾留两字节 CRC16 校验。RC522 硬件只负责链路层的 CRC,应用层的数据完整性必须自己做,靠芯片自带的 FIFO 错误标志是抓不住「数据写进去但含义不对」这类问题的。
5. 避坑与排查:RC522 读卡模块的 5 个高发问题
RC522 在调通之后是可靠的,但把它从「能读一次」调到「能读一万次」,中间要趟过的坑不少。这里把我遇到的 5 类高频问题按「现象 → 原因 → 解决」写清楚。
5.1 现象:天线有信号,但寻卡一直超时
现象:卡片放到天线区域,LED 会亮(有些模块带载波指示),但代码在RC522_Request返回错误,FIFO 里始终没有数据。
原因:最常见的两个。一是BitFramingReg没写 0x07,REQA 短帧格式不对,卡片根本不理会;二是天线配置寄存器TxControlReg没正确开启,模块上的 LED 亮只是供电指示,不代表天线驱动在工作。
解决:在初始化里显式调用RC522_AntennaOn(),这个函数做的事是往TxControlReg写入 0x80 并延时 2ms,再写入 0x83。如果还不行,把RFCfgReg的 RxGain 调整一下,默认 0x48,可以改成 0x60 提升接收灵敏度。注意增益不是越高越好,我在某款模块上把增益拉满后,反而频繁读到错误卡号,因为背景噪声也被放大了。
5.2 现象:读到卡号,但验证密钥失败
现象:AntiCollision能返回 4 字节 UID,但Authentication一直返回错误,换了好几张卡都一样。
原因:密钥字节序错了。M1 卡的 6 字节密钥是按出厂顺序存储的,FF FF FF FF FF FF没问题,但自定义密钥时,要注意你写入的数组顺序和卡片扇区密钥里存的顺序是否一致。另一个常见原因是,验证密钥前必须先完成选卡(Select),直接跳过选卡去验证,卡片会直接拒绝。
解决:先确认代码流程是「Request → AntiCollision → Select → Authentication」。Select 这一步有些精简例程会省略,但 RC522 在选卡成功前对认证命令一律不做处理。再确认密钥数组定义,比如uint8_t key[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},数组下标 0 对应卡片密钥第一字节,不要反转。
5.3 现象:换一块模块就完全不通
现象:A 模块上验证过的代码,换到 B 模块上,初始化读版本号失败,串口打印的全是 0xFF 或 0x00。
原因:RC522 模块的板级设计差异很大,特别是 SPI 上拉电阻和天线匹配电路。部分模块在 MISO 引脚上没有做上拉,而 STM32 的 SPI 输入在浮空状态下容易读到噪声。另外有些模块的 SDA 片选是低有效,但个别板子把片选逻辑反了,需要高电平选中。
解决:先量电压,确保 VCC 在 2.5V-3.3V 范围。然后强制把 STM32 的 GPIO 设为推挽输出模式,在 SPI 初始化时把所有相关引脚统一配置。MISO 引脚如果内部上拉不够,就在板级外接一颗 10kΩ 电阻到 3.3V。最后用示波器看 SCK 的波形:低电平必须低于 0.3V,高电平必须高于 0.7VCC。我曾经遇到一块模块的 SCK 输入阈值偏高,3.3V 电平刚好卡在临界点,换 5V 供电后问题消失,但为了安全我还是选择了换模块。
5.4 现象:程序复位后第一次读卡失败,第二次才成功
现象:系统上电或手动复位后,第一次按读卡键超时,再按一次就正常。或者反过来,第一次正常,第二次开始连续超时。
原因:复位后 RC522 内部的定时器、中断标志位和 FIFO 都是随机状态。有些例程在初始化时只写关键寄存器,没有做「软件复位后延时再清空 FIFO」的操作。第一次寻卡时,FIFO 里残留的上一次数据会干扰新指令的解析。
解决:在RC522_Init最后,先写FIFOLevelReg清空 FIFO,再把Status2Reg的 MFCryptO1 清零,然后做一次空读操作(发一个无效命令让硬件状态机归位)。还有一个祖传技巧:初始化完成后延时 20ms 再进主循环,让天线和电源稳定。
5.5 现象:SPI 读回的数据总是0xFF
现象:所有寄存器读出来都是 0xFF,写进去再读也全 F,看起来像 SPI 完全没通信。
原因:MOSI/MISO 接线接反,或者片选永远还是高电平,芯片处于掉线状态。另一个原因是 MISO 引脚复用配置错误,你可能在 CubeMX 里把 MISO 配成了输出模式,导致数据读不回来。
解决:用万用表量片选引脚电平,读卡期间应该有低脉冲。然后把 MISO 改成输入模式(CubeMX 里选 GPIO_Input 或复用功能,取决于你用的库)。最后用最笨的办法:手动拉低片选,往FIFOLevelReg写 0x55,再读回来,如果读回的不是 0x55,硬件链路就还没通。这一步能排除掉 90% 的接线错误。
6. 进阶:把刷卡识别做成高可靠性方案的三个实践
基础读写通了之后,项目真正考验人的是稳定性和用户体验。这里分享三个我反复用过的技巧,特别是最后一个,是让整套系统从「能用」变成「好用」的关键。
6.1 低功耗场景:定时寻卡比外部中断更可靠
不少门禁项目希望设备待机省电。RC522 的 IRQ 引脚支持中断唤醒,但实际做下来,天线场需要持续供电,IRQ 触发条件在卡片快进快出时经常漏掉。我更倾向用定时器做 50ms 间隔的轮询寻卡,卡片停留时间普遍超过 100ms,三次寻卡至少能命中一次,强度低但成功率高。
6.2 一卡一密的简易实现
出厂默认密钥谁都读得动,只要买一块读卡器就能复制卡片。一卡一密的思路是:用卡片的 UID 做种子,跑一遍 CRC16 或 MD5,取前 6 字节作为该卡扇区密钥。关键点是不能在卡上明文存 UID 和密钥的对应关系,密钥由上位机后台按 UID 动态计算下发。这样即使某张卡被复制,复制卡的 UID 变了,密钥就不对,照样读不出数据。
6.3 读卡超时的统一处理
卡片放进天线后,如果用户马上拿走,读卡流程可能卡在某一状态。我在所有收发函数里都加了超时变量:调RC522_WaitForCardReady时,如果 10ms 内CommIrqReg的 RxIRq 位置 1 就继续,否则直接清标志返回超时。这套机制要写成一while循环,而不是依赖硬件看门狗。从那以后我每次给刷卡代码加新功能,都会强制走一遍「进函数先记时间戳、出函数检查超时标志」这个流程,翻车概率明显降了一个量级。希望帮到你。
本文还有配套的精品资源,点击获取