STM32+RC522读取MIFARE 1K卡:从UID到加密读写的"排坑"实录
前言
最近在做一个基于STM32F103C8T6和RC522的RFID门禁系统项目。由于使用现成的封装库,本以为读取UID、完成扇区密码认证和读写是分分钟的事,结果却在"认证失败"和"写块只写进一个字节"的泥潭里挣扎了好久。
本文将复盘整个调试过程,重点解析 ISO14443A 协议的状态机流转 、硬件CRC的陷阱 以及 底层收发时序截断 这三个经典大坑。如果你也在用 RC522 调 MIFARE 1K (M1) 卡,希望这篇避坑指南能帮你节省几个小时。
实验现象与硬件环境
- 硬件:STM32F103C8T6 最小系统板、RC522 模块(3.3V供电)、MIFARE Classic 1K 白卡。
- 软件:STM32CubeMX + Keil HAL库,串口波特率 115200。
- 目标 :读取卡号 UID -> 使用默认密钥
FFFFFFFFFFFF认证 -> 读块 -> 写块 -> 读回验证。
坑点一:UID能读出,但死活认证不过(Auth Failed)
现象 :寻卡、防冲突获取 UID 一切正常,但调用 MIFARE_Auth 时始终返回失败。起初怀疑是卡被加密了,用手机 NFC 软件(MCT)一测,发现卡确实出厂默认密钥 FFFFFFFFFFFF,根本没加密!
原因与解决: 根据 ISO14443A 协议,卡片的状态机是严格递进的:
REQA(寻卡) -> 卡片进入 READY 态。Anticoll(防冲突) -> 获取 4 字节 UID,卡片依然停留在 READY 态。SELECT(选卡) -> 发送 UID 给卡片校验,卡片进入 ACTIVE 态。AUTHENT(认证) -> 只有进入 ACTIVE 态后,卡片才接受密码认证!
很多简陋的开源库省去了 SELECT 步骤,导致卡片一直处于 READY 态,你发认证指令它当然不理你。我们需要补上选卡逻辑:
c
// 选卡函数
uint8_t RC522_Select(uint8_t *serNum)
{
uint8_t status;
uint16_t unLen;
uint8_t buffer[7];
uint8_t i;
buffer[0] = 0x93; /* Select 指令 */
buffer[1] = 0x70; /* 选卡 */
for (i = 0; i < 4; i++) {
buffer[2 + i] = serNum[i];
}
/* 计算 BCC(UID 4字节异或) */
buffer[6] = serNum[0] ^ serNum[1] ^ serNum[2] ^ serNum[3];
RC522_WriteReg(BitFramingReg, 0x00);
status = RC522_ToCard(PCD_TRANSCEIVE, buffer, 7, buffer, &unLen);
return status;
}
坑点二:加上选卡后,程序彻底卡死或通信超时
现象 :加上 Select 指令后,不仅认证没过,连串口都不打印信息了,或者打印 Select failed。
原因与解决 : 这涉及两个致命细节:CRC校验的开启时机 与 SPI通信速率。
- 硬件 CRC 陷阱 :在 ISO14443A 中,寻卡(0x52)和防冲突(0x9320)是不带 CRC 的;但是选卡(0x9370)、认证(0x0E)、读写(0x30/0xA0)必须带 CRC。如果你的代码全局开启或关闭 CRC,都会导致通信失败。
- SPI 速率过高:用面包板和杜邦线连接时,如果 SPI 速率跑到 9MHz,在带加密和 CRC 的复杂数据交换中极易发生时序错位(丢帧)。
解决思路:
- 降低 SPI 速率 :在 CubeMX 中,将 SPI1 的 Baud Rate Prescaler 从 8 改为 64 或 128(降至 1MHz 左右,非常稳定)。
- 动态控制 CRC :在底层发送函数
RC522_ToCard中,根据指令动态修改TxModeReg和RxModeReg。
c
// RC522_ToCard 函数中的 CRC 动态判断
if (command == PCD_AUTHENT) {
RC522_WriteReg(TxModeReg, 0x80); // 认证开启发送 CRC
RC522_WriteReg(RxModeReg, 0x80); // 认证开启接收 CRC
} else if (command == PCD_TRANSCEIVE) {
// 寻卡和防冲突不需要 CRC
if ((sendData[0] == 0x93 && sendData[1] == 0x20) || sendData[0] == 0x52 || sendData[0] == 0x26) {
RC522_WriteReg(TxModeReg, 0x00);
RC522_WriteReg(RxModeReg, 0x00);
} else {
// 选卡和读写需要 CRC
RC522_WriteReg(TxModeReg, 0x80);
RC522_WriteReg(RxModeReg, 0x80);
}
}
坑点三:认证和读块成功,但写块只写进了第一个字节
现象 :认证通过,读块数据全 00 正常;调用写块函数返回成功,但读回的数据却只有首字节变成了 32,后面全是 00(例如写入 32 30 32 33...,读回 32 00 00 00...)。
原因与解决: 这是 RC522 硬件底层的一个"时序截断"Bug。 MIFARE 卡的写块协议是半双工的:
- 主机发
0xA0+块地址(2字节)。 - 卡片回复
ACK。 - 主机紧接着发
16字节数据。 - 卡片回复
ACK。
当我们用底层函数一次性把 18 字节全部塞进 FIFO 时,RC522 在发完前 2 个字节后收到了卡片的 ACK,此时中断标志位 RxIRq 置位。代码误以为发送已经结束,提前打断了发送过程,导致后面的 15 个数据字节全被丢弃。
解决思路 : 在底层等待中断的逻辑中"打个补丁":如果是写块指令,不等待接收中断,而是强等待发送完成中断(IdleIRq = 0x10),确保 18 字节全部吐完。或者,最稳妥的做法是将写块拆分为两次发送:
c
// 最稳妥的写块函数实现(拆分法)
uint8_t MIFARE_WriteBlock(uint8_t blockAddr, uint8_t *data)
{
uint8_t status;
uint16_t unLen;
uint8_t buffer[2];
// 1. 发送写块指令和地址 (0xA0 + Addr)
buffer[0] = 0xA0;
buffer[1] = blockAddr;
RC522_WriteReg(BitFramingReg, 0x00);
status = RC522_ToCard(PCD_TRANSCEIVE, buffer, 2, NULL, &unLen);
if (status != MI_OK) return status;
// 2. 发送 16 字节数据
status = RC522_ToCard(PCD_TRANSCEIVE, data, 16, NULL, &unLen);
return status;
}
最终的完美输出
经过上述三道"手术",重新编译烧录。刷卡后,串口终于打出了令人极其舒适的完美回显:
text
UART Direct Test OK
RC522 Version: 0x92
MFRC522 Version 2.0 detected.
--- New Card Detected ---
Card UID: 02 5B 0E 9E
--- 1. Read Test ---
Auth(default key FFFFFFFFFFFF) OK
Block 4 raw data: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
--- 2. Write Test ---
Write block 4 OK
Read back block 4: 32 30 32 33 30 30 30 31 5A 68 61 6E 67 00 00 00
--- 3. Wrong Key Test ---
Wrong key rejected, auth failed as expected
总结与思考
在使用 RC522 等 RFID 模块时,往往"能读出UID"只是个开始,真正的挑战在于:
- 吃透协议规范:ISO14443A 的状态机流转(READY -> ACTIVE)是死规定,不能省步骤。
- 硬件调试需降速:面包板长线连接时,SPI 降速往往能解决 90% 的玄学问题。
- 注意芯片特性:RC522 在收发非对称长度数据(如写块)时的中断打断问题,需要仔细查阅芯片手册(FIFO 与 中断控制)。