☰
RC522 RFID课程设计:读卡固件、上位机与门禁考勤改造
2026/11/4 20:46:38 网站建设 项目流程

简介:这份中南大学RFID课程设计资料包,面向物联网、自动化及相关专业的学生与授课教师,聚焦射频识别系统的编码实现与实验总结,可用于课程设计参考、答辩准备和自学复现。压缩包共320个文件,约8.72MB,以211张png截图、21个cpp源文件、17个h头文件及5个ui界面文件为主体,另含obj、o等编译中间文件,dll与so动态库,pro工程配置,db数据库和exe可执行程序,完整保留了Qt项目从开发到运行的目录结构。目前已有1317人学习下载。内容围绕阅读器、电子标签与天线的通信原理展开,配套代码涉及数据库操作、用户管理、签到功能与主界面实现,实验报告则覆盖项目概述、硬件选择、系统架构、软件设计、功能实现与结果分析,读者可据此理解RFID门禁、考勤类应用的开发流程,并借鉴其中的排错思路与文档组织方式。

1. 一个上电后读不到卡的真实场景

答辩前一晚,读卡器接上电,串口里刷出来的 UID 一直是00 00 00 00,换一张卡还是四个零——这多半不是玄学,八成是 SPI 模式不对,或者天线根本没起振。这类 RFID 课程设计看上去只是「读一张卡、显示一个号」,真正动手才会发现麻烦集中在三处:13.56MHz 的 ISO14443A 协议时序、M1 卡的扇区密钥与块地址、以及单片机和上位机之间那几帧串口数据。这份资源把三样东西打包在一起:能编译烧录的读卡器固件、一个配套的上位机工具,以及一份按实验步骤写的报告。它适合正在做 RFID 课程设计、手里只有模块和数据手册、不知道从哪下手的人;也适合课设已经跑通、想把它改成门禁或考勤小系统写进简历的人。下面按「先看骨架、再拆代码、然后跑起来、最后改」的顺序讲。

2. 项目骨架与技术选型:SPI 驱动、协议状态机和上位机怎么分工

拿到一个课设包,最忌讳的操作是直接双击工程文件点编译,然后看着一堆报错不知道该删哪一行。正确的顺序是先花十分钟把目录走一遍,搞清楚哪些文件是「必须读懂的」,哪些是「框架自带、当黑匣子用就行」。这个项目的分层做得比较干净:最下面是 RC522 芯片的寄存器读写,中间是 ISO14443A 的寻卡、防碰撞、认证、读写四段状态机,上面是门禁/消费的业务逻辑和串口协议。三层之间靠头文件约束,换主控芯片时只需要重写最下面那几十行 SPI 代码。

2.1 拿到包先看目录:哪几个文件是真正要读的

这类课设包的目录结构基本跑不出同一个形状,我按常见的组织方式给你画一遍。你拿到手之后先对着这份结构对一遍,缺哪个就知道自己拿到的是精简版还是完整版:

rfid_course_design/ ├── doc/ │ ├── 实验报告.docx # 按实验步骤写的正文,含截图和数据表 │ ├── 原理图.pdf # 模块与主控的接线 │ └── 答辩说明.md # 关键代码讲解要点 ├── firmware/ │ ├── Core/ │ │ ├── Inc/ rc522.h mifare.h proto.h app.h │ │ └── Src/ main.c rc522.c mifare.c proto.c app.c │ ├── Drivers/ # 芯片厂商的 HAL / 标准外设库 │ └── MDK-ARM/ # 工程文件,双击就能打开 ├── host/ │ ├── rfid_tool.py # 上位机,串口收发 + 十六进制展示 │ └── requirements.txt └── README.md # 接线表、编译环境、已知问题

真正需要逐行读的只有五个文件:rc522.c(寄存器读写与收发)、mifare.c(认证和块操作)、proto.c(串口帧解析)、app.c(业务逻辑,比如扣款和放行)、main.c(初始化顺序)。Drivers/那一层是芯片厂商提供的,正常情况下一行都不要改,改了就很难再对上别人的例程。doc/实验报告.docx不要当附件看,它是这个包里最值钱的部分之一——里面通常有示波器抓的时序图、每一块的实测数据、以及「为什么密钥要用 FF FF FF FF FF FF」这类必须写进报告的说明。

我一般会先在md里把rc522.h的全部函数签名抄一遍,抄完基本就知道这个包的作者是怎么划分职责的。签名暴露得越少,说明封装得越好,后面改动越不容易连坐。

2.2 三层分工:硬件抽象、协议状态机、业务逻辑

分层不是为了好看,是为了让「换一块板子」这件事变成改一个文件。这个项目的驱动层对外只暴露六个函数,上层完全碰不到寄存器地址:

/* rc522.h —— 驱动层对外接口,上层只认这几个函数 */ #ifndef __RC522_H #define __RC522_H #include <stdint.h> #define RC522_OK 0 #define RC522_ERR_TIMEOUT 1 /* 卡片没应答,多半是没放卡或天线没起振 */ #define RC522_ERR_FRAME 2 /* 帧错误,校验位或位宽对不上 */ #define RC522_ERR_AUTH 3 /* 认证失败,密钥错或块号越界 */ void rc522_init(void); uint8_t rc522_version(void); /* 读版本寄存器,正常返回 0x91 / 0x92 */ uint8_t rc522_request(uint8_t req_code, uint8_t atqa[2]); /* 0x26 寻休眠卡,0x52 寻全部卡 */ uint8_t rc522_anticoll(uint8_t uid[5]); /* 防碰撞,取出 5 字节(含校验) */ uint8_t rc522_select(uint8_t uid[5], uint8_t *sak); /* 选卡,之后才能认证 */ void rc522_halt(void); /* 休眠当前卡,读下一张前必须调用 */ #endif

这六个函数的分工很明确:rc522_version()是上电自检用的,返回0x91或0x92说明 SPI 通了、芯片活了,返回0x00或0xFF说明线接错或者模式设错;request → anticoll → select是标准三步,少一步都无法进入认证;halt()最容易被忽略,如果不调用它,多张卡同时在感应区里会一直重复读到同一张。

协议层在mifare.c,它不管寄存器,只管「发什么命令、期望回几个字节」。业务层在app.c,它连 UID 从哪来都不关心,只接收一个card_t结构体,判断这个人能不能进、这次该扣多少钱。

/* app.c —— 业务层只认这个结构,换读卡芯片不用改这里 */ typedef struct { uint8_t uid[4]; uint8_t balance_ok; uint32_t balance; } card_t; uint8_t door_check(const card_t *card) { /* 白名单可以是写死的,也可以从 Flash 里读,课设阶段写死就够 */ static const uint8_t white_list[][4] = { {0x1A, 0x2B, 0x3C, 0x4D}, {0x5E, 0x6F, 0x70, 0x81}, }; for (uint8_t i = 0; i < sizeof(white_list) / 4; i++) { if (memcmp(card->uid, white_list[i], 4) == 0) return 1; } return 0; }

这样分层带来的直接好处是:如果老师要求换成 125kHz 的 ID 卡模块,你只需要重新实现rc522.h里那六个函数,app.c一行不用动。我见过不少课设代码把 UID 比较、串口打印、继电器控制全塞在main的while(1)里,最后想加一个功能就得重写。

2.3 技术选型上的几个取舍

课设里几个绕不开的选择,选错了会浪费一整晚。下面这张表是我踩过之后的结论:

选择点常见做法我的建议理由
SPI 实现硬件 SPI / 软件模拟优先硬件 SPI软件模拟在 8MHz 以下勉强能用,但和串口打印抢时间时会偶发丢帧
收发方式轮询中断标志 / 外部中断轮询命令是同步的,轮询代码三行搞定;外部中断还要处理引脚复用
卡片认证每次读块前都认证同一扇区内认证一次即可认证状态按扇区保持,重复认证反而拖慢刷卡响应
串口协议自定义二进制帧 / 直接 printf 文本二进制帧 + CRC文本格式调试方便但没法传 16 字节原始块数据,容易截断
密钥存放硬编码在 .c 里单独放keys.h答辩被问到「密钥能不能改」时,能立刻指出来

硬件 SPI 这里再说一句:RC522 只支持 SPI 模式 0(CPOL=0,CPHA=0),时钟空闲低电平、上升沿采样。这个参数在 CubeMX 里对应CLKPolarity = LOW、CLKPhase = 1EDGE,选错了读出来全是0xFF,而且不会报错,只会静默地读不到卡——这也是第 5 章第一条要讲的坑。

3. 核心代码拆解:一张卡从进入感应区到读出数据

把一张 M1 卡完整读出来,芯片实际上发了六条命令:REQA(寻卡)、ANTICOLLISION(防碰撞)、SELECT(选卡)、AUTHENT(认证)、READ(读块)、HALT(休眠)。每一条命令都有固定的字节数、固定的位宽、固定的超时时间,任何一处偏差都会表现为「读不到卡」这一个笼统现象。下面按顺序拆开。

3.1 收发内核:所有命令共用的那一个函数

寻卡、认证、读块走的都是同一套流程:清标志、塞 FIFO、启动命令、等中断、取数据。把这段抽成一个函数,后面三节都能省掉一半代码:

/* rc522.c —— 通用收发:cmd 传 PCD_TRANSCEIVE 或 PCD_AUTHENT */ static uint8_t rc522_transceive(uint8_t cmd, const uint8_t *send, uint8_t send_len, uint8_t *recv, uint16_t *recv_len, uint8_t tx_last_bits) { uint8_t irq; uint16_t i, n; rc522_write_reg(RC522_COM_IRQ, 0x7F); /* 清掉上一次遗留的所有中断标志 */ rc522_write_reg(RC522_FIFO_LEVEL, 0x80); /* FlushFIFO,不清会串数据 */ rc522_write_reg(RC522_COMMAND, PCD_IDLE); /* 取消可能还在跑的旧命令 */ for (i = 0; i < send_len; i++) rc522_write_reg(RC522_FIFO_DATA, send[i]); rc522_write_reg(RC522_COMMAND, cmd); if (cmd == PCD_TRANSCEIVE) rc522_write_reg(RC522_BIT_FRAMING, 0x80 | (tx_last_bits & 0x07)); /* StartSend=1 */ else rc522_write_reg(RC522_BIT_FRAMING, 0x00); /* 认证命令不需要 StartSend */ irq = rc522_wait_irq(0x30, 60); /* 等 RxIRq(0x20) 或 IdleIRq(0x10),60ms 超时 */ rc522_write_reg(RC522_BIT_FRAMING, 0x00); if (irq & 0x01) return RC522_ERR_TIMEOUT; /* TimerIRq:卡片没应答 */ if (!(irq & 0x20)) return RC522_ERR_FRAME; /* 没等到 RxIRq,帧错误 */ n = rc522_read_reg(RC522_FIFO_LEVEL); if (n > *recv_len) n = *recv_len; /* 防止接收缓冲被写穿 */ for (i = 0; i < n; i++) recv[i] = rc522_read_reg(RC522_FIFO_DATA); *recv_len = n; return RC522_OK; }

几个参数值得单独说。tx_last_bits只对寻卡有用:REQA 是 7 位短帧,最后一位之后天线立刻要切到接收,所以必须告诉芯片「最后这个字节只发 7 位」,传0x07;其他命令传0x00表示发完整字节。rc522_wait_irq(0x30, 60)里的60是毫秒超时,卡片不在场时靠这个值退出而不是死等,调大了刷卡响应会发木,调小到 20 以下在卡片刚靠近的瞬间会误判失败。FlushFIFO那行是最容易漏的,漏掉之后现象很诡异:第一张卡能读,换第二张卡读出来的是上一张的尾巴。

3.2 寻卡与防碰撞:UID 是怎么一位一位撞出来的

寻卡用 REQA(0x26),唤醒已休眠的卡用 WUPA(0x52)。正常应答是两个字节的 ATQA,用来告诉读卡器「我支持哪种防碰撞流程」:

/* 寻卡:成功时 atqa 里是 2 字节应答 */ uint8_t rc522_request(uint8_t req_code, uint8_t atqa[2]) { uint8_t buf[2] = {0}; uint16_t len = 2; if (rc522_transceive(PCD_TRANSCEIVE, &req_code, 1, buf, &len, 0x07) != RC522_OK) return RC522_ERR_TIMEOUT; if (len != 2) return RC522_ERR_FRAME; atqa[0] = buf[0]; atqa[1] = buf[1]; return RC522_OK; }

防碰撞这一步是整套协议里最精彩的地方。多张卡同时在感应区,它们会同时回 UID,读卡器收到的是一堆电平叠加的乱码,只能靠逐位比较确定谁先谁后。标准库函数的写法是级联级别0x93加上0x20,让卡片自己报 UID,遇到冲突位就冻结并重新选:

/* 防碰撞:返回 5 字节,前 4 字节是 UID,第 5 字节是 BCC 异或校验 */ uint8_t rc522_anticoll(uint8_t uid[5]) { uint8_t buf[5] = {0}; uint16_t len = 5; uint8_t cmd[2] = {0x93, 0x20}; /* 0x93 = 级联级别1,0x20 = 防碰撞命令 */ if (rc522_transceive(PCD_TRANSCEIVE, cmd, 2, buf, &len, 0x00) != RC522_OK) return RC522_ERR_TIMEOUT; if (len != 5) return RC522_ERR_FRAME; /* BCC = UID0 ^ UID1 ^ UID2 ^ UID3,校验不过说明帧被干扰了 */ if ((buf[0] ^ buf[1] ^ buf[2] ^ buf[3]) != buf[4]) return RC522_ERR_FRAME; memcpy(uid, buf, 5); return RC522_OK; }

len != 5这个判断不能省。感应区里有两张卡时,芯片有时会返回 4 个字节或者 6 个字节的残帧,不做长度检查直接memcpy就是一次越界。BCC 校验也是一道便宜的保险——天线附近有手机或者金属时偶尔会读到只错一位的 UID,加了这个异或校验能过滤掉大部分。

select只是把 UID 和 BCC 再发一遍让卡片确认,成功后卡片进入 ACTIVE 状态,才可以认证。很多同学省掉select直接认证,结果认证返回状态码0x04或者干脆没响应。这不是玄学,是状态机没走完。

3.3 扇区认证与块读写:0x60 / 0x30 / 0xA0

M1 卡 1K 容量分成 16 个扇区,每扇区 4 块,每块 16 字节,最后一块是扇区尾块,存密钥 A、访问位、密钥 B。块 0 是厂商固化区,只读,里面放着 UID 和厂商信息。

认证的命令字是0x60(密钥 A)和0x61(密钥 B)。认证时要一次性把「命令 + 块号 + 6 字节密钥 + 4 字节 UID」塞进 FIFO:

/* 认证:mode 传 PICC_AUTHENT1A(0x60) 或 PICC_AUTHENT1B(0x61) */ uint8_t mifare_auth(uint8_t mode, uint8_t block, const uint8_t key[6], const uint8_t uid[4]) { uint8_t buf[12]; uint8_t recv[4]; uint16_t len = 4; buf[0] = mode; buf[1] = block; /* 认证的是扇区里的任意一块,不是扇区号 */ memcpy(&buf[2], key, 6); memcpy(&buf[8], uid, 4); /* 用前面防碰撞拿到的 4 字节 UID */ if (rc522_transceive(PCD_AUTHENT, buf, 12, recv, &len, 0x00) != RC522_OK) return RC522_ERR_AUTH; if (!(rc522_read_reg(RC522_STATUS2) & 0x08)) return RC522_ERR_AUTH; /* Crypto1On 没置位,说明认证没成功 */ return RC522_OK; }

这里有个细节新手几乎必错:block传的是具体块号(0~63),不是扇区号。同一个扇区里认证一次之后,读这个扇区的另外三块都不用再认证,因为认证状态挂在扇区上。另外检查认证结果不能只看有没有收到数据,要看Status2寄存器的 Crypto1On 位,不看这位的话明明密钥错了也会「成功」返回。

认证通过后读块就很直白了,命令0x30加一个块号,回来 16 字节:

uint8_t mifare_read_block(uint8_t block, uint8_t data[16]) { uint8_t cmd[2] = {0x30, block}; uint16_t len = 16; if (rc522_transceive(PCD_TRANSCEIVE, cmd, 2, data, &len, 0x00) != RC522_OK) return RC522_ERR_FRAME; if (len != 16) return RC522_ERR_FRAME; /* 只回来 4 字节通常是认证掉了 */ return RC522_OK; }

写块用0xA0,先发命令和块号,再发 16 字节数据,两次 FIFO 写入之间不能 flush。写完回调一次读出来比对是最省事的验证手段,比看返回值可靠得多——尤其是天线功率不足的时候,写命令会「成功」但数据其实没落下去。

3.4 钱包扣款与上位机串口帧

余额存在某个数据块里,前 4 字节小端放金额,紧接着 4 字节放取反值当校验。扣款就是读-改-写,但顺序有讲究:先写备份块,再写主块,这样掉电最多丢一次操作,不会出现「备份扣了主块没扣」的尴尬:

/* 扣款:主块和备份块同时更新,返回 0 表示完整成功 */ uint8_t wallet_spend(uint8_t blk_main, uint8_t blk_bak, uint32_t amount) { uint8_t buf[16]; uint32_t balance; if (mifare_auth(PICC_AUTHENT1A, blk_main, KEY_A, g_uid)) return 1; if (mifare_read_block(blk_main, buf)) return 2; balance = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24); if (balance < amount) return 3; /* 余额不足交给业务层提示 */ balance -= amount; memset(buf, 0, 16); buf[0] = (uint8_t)(balance); buf[1] = (uint8_t)(balance >> 8); buf[2] = (uint8_t)(balance >> 16); buf[3] = (uint8_t)(balance >> 24); buf[4] = (uint8_t)~buf[0]; buf[5] = (uint8_t)~buf[1]; buf[6] = (uint8_t)~buf[2]; buf[7] = (uint8_t)~buf[3]; if (mifare_write_block(blk_bak, buf)) return 4; /* 先落备份 */ if (mifare_write_block(blk_main, buf)) return 5; /* 再落主块 */ return 0; }

固件侧最后一道工序是把结果按帧发出去。帧格式定义成AA 55 | LEN | CMD | DATA[LEN] | CRC16 低字节 高字节 | 0D 0A,接收端用状态机消费,不能靠readline之类的做法:

/* proto.c —— 逐字节喂进状态机,收到完整帧返回 1 */ uint8_t proto_feed(uint8_t byte, uint8_t *cmd, uint8_t *data, uint8_t *data_len) { static uint8_t buf[64]; static uint8_t idx = 0; static uint8_t expect = 0; if (idx == 0 && byte != 0xAA) return 0; /* 等帧头第一字节 */ if (idx == 1 && byte != 0x55) { idx = 0; return 0; } /* 帧头第二字节不对就复位 */ buf[idx++] = byte; if (idx == 3) expect = (uint8_t)(4 + buf[2] + 2); /* LEN + CMD + DATA + CRC16 */ if (idx < 3 || idx < expect) return 0; idx = 0; uint16_t crc = (uint16_t)buf[expect - 2] | ((uint16_t)buf[expect - 1] << 8); if (crc != crc16_modbus(&buf[2], (uint16_t)(2 + buf[2]))) return 0; *data_len = buf[2]; *cmd = buf[3]; memcpy(data, &buf[4], buf[2]); return 1; }

上位机那一端用 Python 写几十行就够,重点是把 CRC 算对,0xA001多项式、初值0xFFFF、低字节在前,这三个参数错一个就永远对不上:

# host/rfid_tool.py —— 组帧与校验,和固件端的 crc16_modbus 必须完全一致 import serial, struct def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc def build_frame(cmd: int, payload: bytes = b"") -> bytes: body = bytes([len(payload), cmd]) + payload return b"\xAA\x55" + body + struct.pack("<H", crc16_modbus(body)) + b"\r\n" def read_block(ser: serial.Serial, blk: int) -> bytes: ser.write(build_frame(0x03, bytes([blk]))) head = ser.read(2) if head != b"\xAA\x55": raise IOError("帧头不对,检查波特率和共地") rest = ser.read(2 + ser.read(1)[0] + 2) return rest[1:-2] # 去掉 CMD 和 CRC,剩下 16 字节块数据

4. 把代码跑起来:接线、编译、烧录与串口验证

代码读懂和跑通是两件事。这一章按顺序走一遍上电路径,中间任何一步失败都别往下走,否则后面排查会同时冒出三个可疑点。

4.1 硬件清单与接线表

需要的东西不多:一块主控板、一块 RC522 读卡模块、若干杜邦线、一张 M1 卡(通常随模块附赠一张白卡和一张异形卡)、一个 USB 转串口模块。接线按 SPI1 走,具体引脚如下:

RC522 引脚主控引脚说明
SDA / CSPA4片选,低电平有效,必须手动控制
SCKPA5SPI1 时钟
MOSIPA7主出从入
MISOPA6主入从出
RSTPA3复位,接上更稳,也可以软件拉
3.3V3.3V严禁接 5V
GNDGND必须和主控共地

供电这一栏我单独画了线。RC522 模块的工作电压是 2.5V~3.6V,接到 5V 上大概率当场烧掉,运气好没烧也会出现「能读到卡但距离只有 1 厘米」这种半死不活的状态。有些模块板上带了电平转换和稳压,标着「5V 兼容」,但课设里常见的裸模块没有,接线前先看丝印。

4.2 工程导入、编译选项与烧录

工程用 Keil 打开,第一次编译前确认三件事:编译器版本选对(AC5 和 AC6 对__weak和inline的处理不一样,混用会报一堆重定义)、C 标准选 C99(代码里用了for (uint8_t i = 0; ...)这种写法)、勾上 MicroLIB(printf重定向到串口时省掉半主机模式的麻烦)。

# 命令行编译(装了 Keil 的命令行工具时可用),比点 IDE 更适合快速验证 UV4.exe -b firmware/MDK-ARM/rfid.uvprojx -j0 -o build.log grep -E "Error|Warning" build.log # 用 st-flash 烧录,注意复位方式要选 hardware,否则烧完不自动跑 st-flash --reset write build/rfid.bin 0x08000000

堆栈大小也要看一眼。默认Stack_Size是0x400,收发缓冲加上printf的格式化开销偶尔会踩到边界,现象是刷卡刷到第十几次突然死机。改成0x800基本就没问题了。这类问题最难查,因为它只在跑了一段时间之后出现,看起来像是「卡的问题」。

4.3 上电自检:四条命令验证链路

烧录完不要急着塞卡,先按顺序发四条命令,把链路一层层验通。固件里预留了一组文本命令,用任意串口助手(115200、8N1)就能测:

命令期望返回说明
AT+VERVER=1.0 RFID=0x910x91/0x92说明 SPI 通了,0x00/0xFF说明接线或模式有问题
AT+UIDUID=1A2B3C4D不放卡应返回UID=NONE,放卡后 200ms 内出结果
AT+READ=432 个十六进制字符块 4 是第一个数据块,密钥默认全 F
AT+WALLET=4,-5OK BAL=95扣 5 元后返回余额,再读一次块 4 应能看到变化

如果AT+VER就失败,别往下测了,问题一定在 SPI 物理层。如果AT+VER正常但AT+UID一直NONE,那问题在天线或协议时序。这个二分法能把排查范围砍掉一大半。

4.4 实验报告怎么写才能和代码对得上

报告不是代码的翻译,评审看的是「你有没有真的测过」。我建议按这个顺序组织:需求分析(要做哪几个功能)、方案对比(为什么选 13.56MHz 而不是 125kHz)、硬件设计(接线图和供电说明)、软件流程(三层结构图加关键函数说明)、测试数据(连着刷 50 次卡的成功率、不同距离下的读取情况、余额扣减前后的块数据对比)、问题与改进。

其中测试数据那一节最能加分。比如「距离 0~3cm 读取成功率 100%,3~5cm 降到 82%,超过 5cm 基本失败」,这种带数字的结论比「读取效果良好」有说服力得多。截图也别只截成功的界面,把一次认证失败时串口打印的错误码也截进去,再写清楚你是怎么定位到密钥写错的——这比一路顺风的报告真实。

5. 避坑与排查:五个最容易出问题的地方

下面这五条,按出现频率从高到低排。每条我都标了现象、原因和解决方式,遇到问题时可以直接对号入座。

第一条:读不到卡,AT+VER返回0x00或0xFF。现象是上电后芯片版本号读不出来,串口一片安静。原因九成是 SPI 模式设错——RC522 只认模式 0(CPOL=0,CPHA=0),而 CubeMX 默认给的可能是模式 3。另一种可能是片选引脚没手动控制,被硬件 SPI 的 NSS 功能接管了。解决方式是把CLKPolarity改成LOW、CLKPhase改成1EDGE,同时把 NSS 设成GPIO_Output手动拉。改完再读版本号,能出0x91就说明通了。

第二条:能读到卡,但认证一直返回错误。现象是寻卡和防碰撞都正常,UID 也打印出来了,一到认证就失败。原因通常是三种:密钥写错(默认空白卡是全0xFF六个字节,不是全0x00)、块号传成了扇区号、或者忘了先调select直接认证。还有一种隐蔽情况是 UID 传了 5 个字节进去,实际协议只要前 4 个,多传的那个 BCC 字节会让卡片直接忽略这条命令。解决方式是把mifare_auth的入参打印出来对一遍,尤其是 UID 长度。

第三条:刷卡时好时坏,换个姿势又能读。现象是同一张卡,有时候 100ms 出结果,有时候要刷三四次,放在桌上不动也会间歇失败。原因是天线附近的金属和人体影响了谐振频率,桌面如果是金属材质、或者手指压在卡片天线线圈上,都会让读卡距离骤降。解决方式是给模块背面贴一层绝缘垫片垫高 5mm 以上,测试时把卡平放在非金属表面,天线周围 2cm 内不要有其他走线。另外把rc522_wait_irq的超时从 60ms 调到 100ms 也能缓解,代价是失败时响应会变慢。

第四条:换第二张卡读出来还是第一张的 UID。现象很诡异,明明换了一张卡,串口打印的 UID 和上一张一模一样。原因是读完卡之后没调用halt(),卡片一直处于 ACTIVE 状态,下次寻卡时它第一个应答,把新卡挤掉了。另一种情况是 FIFO 没 flush,残留了上次的字节。解决方式是每次读写结束都补一句rc522_halt(),并在rc522_transceive开头保留FlushFIFO那一行。

第五条:串口打印乱码,或者收到一半就断。现象是串口助手上一堆问号,偶尔能看到几个正确的字符。原因有三个层次:波特率不匹配(外部晶振实际是 8MHz 但工程里配的 12MHz,导致 115200 偏差超过 3%)、没共地(USB 转串口模块和主控板各自供电时尤其常见)、或者帧解析用了readline把二进制数据截断在0x0A上。解决方式是先拿示波器或者逻辑分析仪量一下 TX 波形的位宽,确认波特率;再检查共地;最后把接收端改成状态机逐字节消费,不要用文本行的思路处理二进制帧。

6. 二次开发:把它改成能写进简历的门禁/考勤小系统

课设跑通只是及格线。真正让这个项目变得有区分度的,是在它上面加一层「别人没有」的东西。我建议的方向是把单纯的读卡器升级成一个带记录能力的小系统,改动量不大,但成品完整度高得多。

改造方向改动位置工作量效果
刷卡记录存 Flashapp.c加一个环形缓冲半天断电能查最近 200 条记录
加 OLED 显示余额和姓名新增oled.c,I2C 驱动一天脱离电脑也能独立演示
继电器控制门锁一个 GPIO 加三极管两小时从「读卡」变成「门禁」
上位机加考勤统计rfid_tool.py加导出 CSV半天能生成一张出勤表
密钥动态下发串口新增AT+KEY命令两小时答辩时能讲安全性

其中「刷卡记录存 Flash」这一项性价比最高。实现上用一个固定长度的结构体数组,写满之后从头覆盖,配一个写指针存在最后一页:

/* record.c —— 环形记录,一页 Flash 存 32 条,写满回卷 */ typedef struct { uint8_t uid[4]; uint32_t timestamp; /* 上电后的秒数,够课设用了 */ uint8_t result; /* 1 = 放行,0 = 拒绝 */ } log_item_t; void <p> <a href="https://download.csdn.net/download/weixin_42948624/21941540" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询