简介:「中南大学 RFID 课程设计 代码+实验报告」面向高校物联网、电子信息类专业学生及 RFID 入门开发者,提供一套可运行的射频识别课程设计范例,用于理解阅读器、电子标签与天线之间的通信原理,并完成签到、用户管理等典型应用开发。压缩包共 320 个文件,约 8.72MB,以 211 个 png 运行截图、21 个 cpp 源文件、17 个 h 头文件、5 个 ui 界面文件为主,另含 Qt 工程配置、数据库文件、动态库与编译产物,覆盖界面交互、数据库读写和签到逻辑等模块。已有 1317 人学习下载,具备较好的参考基础。读者可据此对照硬件选型、系统架构与软件设计思路,复用串口通信、数据存储和界面管理的实现方式,并结合实验报告中的功能实现、问题解决与结果分析,梳理从需求分析、编码到测试的完整流程,也可作为课程设计选题与答辩准备的对照材料。
1. 从一块读不出卡的模块说起:这份RFID课程设计要交付什么
答辩前一晚读卡器突然读不出卡,排查了两小时才发现是复位后少等了几毫秒——这类问题在RFID课程设计里十分常见。一份能过关的交付物,通常是代码、实验报告、测试数据三项内容相互对应:代码里每个函数都能在报告里找到对应的设计说明和实测截图,报告里的每条结论都有数据支撑。这套方案面向要交RFID课程设计的学生,也面向需要快速搭一套13.56MHz读写原型的从业者,从需求拆解、器件选型、模块划分、关键实现一直走到验收答辩,中间那些容易出问题的细节都会点到。
2. 需求拆解与器件选型:13.56MHz、MFRC522与STM32怎么定下来
选型这一章很多人会直接跳过,拿到什么模块就用什么,结果报告写到“方案论证”一节时无从下手。其实课程设计的评分点里,“为什么这么选”占的分不比“做出来了”少。把需求先固定下来,选型就有了依据,后面调试时也容易判断问题出在硬件还是软件。
2.1 从验收指标反推需求清单
先把课程设计通常要求的验收项列出来,再倒推技术指标。常见的验收场景是:刷卡能在半秒内响应、能正确识别至少两张不同的卡、能读出卡内指定扇区的数据、能把刷卡记录留存在上位机上、断电重启后记录不丢。这几条听起来简单,但它们分别对应了寻卡时序、防冲突算法、扇区认证、串口通信和持久化存储五个完全不同的技术点。
我一般会把需求整理成这样一份表,直接放进报告第二章,评审看一眼就知道工作量在哪:
| 需求编号 | 功能描述 | 技术指标 | 对应模块 |
|---|---|---|---|
| R1 | 识别进入天线的卡片 | 识别距离 ≥ 30mm,响应 ≤ 500ms | 射频前端 + 寻卡 |
| R2 | 多卡同时在场时正确选卡 | 2 张卡随机切换,识别成功率 ≥ 95% | 防冲突 |
| R3 | 读指定扇区数据 | 16 字节块读,CRC 校验通过 | 认证与读块 |
| R4 | 写指定扇区数据 | 写入后回读一致率 100% | 认证与写块 |
| R5 | 刷卡记录上传 | 115200bps,连续 100 帧不丢 | 串口协议 |
| R6 | 记录持久化 | 断电重启后记录完整 | SQLite |
| R7 | 本地状态提示 | 成功/失败两种声光提示 | 人机交互 |
指标不一定要定得高,但一定要可测。R2 写成“支持多卡”就没法验收,写成“2 张卡随机切换成功率 ≥95%”就能在报告里画出一张对比柱状图,这就是分数差别。
2.2 频段与读卡芯片的取舍:为什么不是125kHz
课程设计里能拿到的模块基本是两类:125kHz 的低频只读卡(如 EM4100 系列)和 13.56MHz 的高频卡(ISO/IEC 14443A,典型是 Mifare 系列)。低频方案的好处是电路简单、读卡距离能到 5~10cm,缺点是卡内没有可写存储区,UID 也是固化的,做不出“写数据—回读校验”这种能体现工作量的功能。
13.56MHz 方案选 MFRC522 这类读写芯片,虽然读卡距离通常只有 3~5cm,但它支持完整的 ISO14443A 三层协议:寻卡、防冲突、选卡、认证、读写块、钱包加减,每一步都能单独做实验、单独出数据。对课程设计来说,能拆出六个实验点的方案远比“读得远”有价值。
这里有个容易被忽略的点:MFRC522 是 3.3V 器件,I/O 电平不能直接接 5V 单片机的推挽输出。用 STM32F103 这类 3.3V 主控省事,用 5V 主控就要加电平转换,否则 SPI 线上长期过压,短期能跑,长期会出各种难以复现的问题。
2.3 主控与上位机的分工边界
分工定不清楚,最后会出现两边都在做同一件事、代码互相打架的局面。我的划分原则是:实时性要求高的活放主控,记录和展示的活放上位机。
主控负责 SPI 时序、寻卡防冲突、认证读写、本地声光提示,以及把结果打包成帧通过串口发出去。它不做任何数据存储,RAM 只有 20KB,存不了多少东西。上位机负责帧解析、CRC 校验、写入数据库、列表展示和导出 CSV。
这样划分还有一个好处:答辩时可以把两块分开演示。拔掉串口,主控照样能刷卡亮灯,证明底层是通的;接上串口,数据一条条滚出来,证明通信是通的。比“一坨代码跑起来看结果”更容易讲清楚。
2.4 硬件连接与引脚分配表
接线表必须进报告,而且要标清楚每个引脚的作用,不能只画一张连线的示意图。MFRC522 走 SPI1,注意 NSS 用软件控制而不是硬件片选,因为一次寄存器读写需要“拉低—收发—拉高”三段连续动作,硬件 NSS 在字节间隙会自动抬高,容易把一帧拆断。
| MFRC522 引脚 | STM32 引脚 | 说明 |
|---|---|---|
| SDA(NSS) | PA4 | 软件片选,GPIO 推挽输出 |
| SCK | PA5 | SPI1_SCK,主机模式,≤10MHz |
| MOSI | PA7 | SPI1_MOSI |
| MISO | PA6 | SPI1_MISO |
| RST | PB0 | 复位,低有效,上电后拉高 |
| 3.3V | 3.3V | 独立走线,就近加 100nF + 10uF |
| GND | GND | 与主控共地 |
串口用 USART1(PA9/PA10),波特率 115200。蜂鸣器接 PB5,用三极管驱动而不是直接灌电流;指示灯两路,绿成功红失败。电源这块要单独说一句:MFRC522 发射瞬间电流能到 100mA 左右,如果和主控共用一条细的 3.3V 走线,发射时电压会被拉低,表现就是“隔几秒才能读到一次卡”。就近加去耦电容,或者干脆用独立 LDO 供电,这个问题基本就消失了。
3. 总体架构设计:两层通信、五个模块的职责划分
架构图画不清楚,报告第三章就没法写;代码分层不清楚,调试时就没法定位。这两件事其实是一件事。这一章把系统切成可以独立测试的几块,每块都给出明确的输入输出,做到“哪一块坏了能单独验”。
3.1 分层结构与数据流
整个系统分四层,从上到下依次是:应用层(上位机界面与数据库)、通信层(串口帧协议)、业务层(寻卡—选卡—认证—读写的流程编排)、驱动层(SPI 与 MFRC522 寄存器操作)。
数据流是双向的。上行方向:卡片进入磁场 → 卡片通过负载调制把 ATQA/UID 回传 → MFRC522 解调后写入 FIFO → 主控读 FIFO 拿到数据 → 业务层判断是首次刷卡还是重复刷卡 → 打包成帧 → 串口发出 → 上位机解析 → 落库 → 刷新列表。下行方向主要是上位机的控制命令,比如“下发新密钥”“清空日志”“切换读卡模式”,帧格式和上行一致,只是命令字不同。
分层的好处在于,调试时可以把中间层短路。比如怀疑是射频问题,就让驱动层直接打印 FIFO 原始内容,绕过业务层的判断逻辑;怀疑是串口问题,就在主控里造一条假记录发出去,看看上位机能不能正确显示。一次只怀疑一层,比一次怀疑全部要快得多。
3.2 五个功能模块的边界与接口
模块划分我按“能不能单独写一个测试函数”来定,最后切出五块:
射频驱动模块只做两件事:读写寄存器、执行一次收发。对外接口是rc522_write_reg、rc522_read_reg、rc522_transceive。这一层不认识“卡”这个概念,它只认识字节。
卡片操作模块建立在驱动之上,实现picc_request(寻卡)、picc_select(防冲突+选卡)、picc_auth(认证)、picc_read_block、picc_write_block、picc_halt。它对外暴露的是“给我一张卡的 UID”这种语义清晰的函数。
业务逻辑模块决定“读到卡之后干什么”:判断是不是注册过的卡、决定开锁还是报警、判断是不是重复刷卡需要去抖。
通信模块负责帧的组包和解包,做异或校验,不关心业务内容。
人机交互模块管蜂鸣器和指示灯,包括不同节奏的提示音,比如成功一声长、失败三声短。
模块之间只通过函数调用传递参数,不共享全局变量。这一点在报告里值得单独写一段,因为很多课程设计的代码全部写在 main 里,一个 while 循环三百行,评审一看就知道没有设计过程。
3.3 通信帧格式与数据库表设计
串口上跑的不能是裸的 ASCII 字符串,那样一旦丢一个字节,后面全乱。定一个带帧头、长度和校验的二进制帧:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2B | 固定 0xAA 0x55 |
| 长度 | 1B | 命令字 + 负载的总字节数 |
| 命令字 | 1B | 0x01 刷卡上报,0x02 心跳,0x03 下发密钥 |
| 负载 | nB | 刷卡上报时为 UID(4) + 块数据(16) + 时间戳(4) |
| 校验 | 1B | 从长度到负载末尾逐字节异或 |
上位机侧的数据表设计成两张,一张存卡,一张存记录,用 UID 关联:
-- 卡档案表:一张卡一行,UID 做主键天然去重 CREATE TABLE card ( uid TEXT PRIMARY KEY, -- 8 位十六进制字符串,如 'A1B2C3D4' holder TEXT, -- 持卡人标识,课程设计里可以填学号后四位 balance INTEGER DEFAULT 0, -- 钱包余额,对应 Mifare 的值块 enabled INTEGER DEFAULT 1, -- 是否启用,0 表示挂失 created_at TEXT DEFAULT (datetime('now','localtime')) ); -- 刷卡流水表:只增不改,方便统计和画图 CREATE TABLE swipe_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, block_data TEXT, -- 读到的 16 字节,十六进制串 result INTEGER NOT NULL, -- 1 成功 0 失败,用于算成功率 ts TEXT DEFAULT (datetime('now','localtime')) ); CREATE INDEX idx_log_uid ON swipe_log(uid);注意swipe_log上建了索引,因为报告里大概率要按卡号统计刷卡次数,几十条数据看不出差别,但养成加索引的习惯没坏处,写进报告也是个加分点。
3.4 状态机:一张卡从进场到开锁经历了什么
把流程写成状态机,比画一张有很多分支的流程图更容易讲清楚。正常路径是五个状态:空闲 → 寻卡成功 → 防冲突选卡成功 → 认证成功 → 读块成功 → 回空闲。每一个箭头都可能失败,每条失败路径都要有对应的超时和重试。
超时的设定值很关键。寻卡的超时一般给 20~30ms,认证给 50ms,读块给 100ms。这些值不是拍脑袋定的,ISO14443A 规定了帧保护时间,读写器和卡片之间还有握手延迟,超时给太短会频繁报错,给太长会让整个循环变卡。我的做法是先在报告里给出理论值,再通过实测记录不同超时下的成功率,最后选一个折中点,这个过程本身就是很好的实验内容。
失败路径也要设计。寻卡超时说明场上没卡,静默重试就行,不需要报警;认证失败说明密钥不对或者卡不是这个扇区的,要报警并在日志里记一条失败记录;读块 CRC 错误说明信号质量有问题,可以重试一次再放弃。把这三类失败区分开,答辩时被问到“如果密钥错了会怎么样”就能直接答上来。
4. 关键实现:寻卡、防冲突、扇区读写与上位机入库
代码部分不求长,但求每一步都能在报告里对应上。下面按调用顺序从上往下走,先驱动,再卡片操作,再上位机,最后是报告里该放什么数据。
4.1 SPI驱动层:寄存器读写与复位时序
MFRC522 的 SPI 协议有个规律:地址字节的低 7 位是寄存器地址左移一位,最低位是读写标志,1 表示读。搞错这一位,读回来永远是 0x00,是新手最常见的坑。
/* 寄存器写:地址左移一位,最低位 0 表示写 */ static void rc522_write_reg(uint8_t reg, uint8_t val) { uint8_t tx[2] = { (uint8_t)((reg << 1) & 0x7E), val }; RC522_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx, 2, 10); RC522_CS_HIGH(); } /* 寄存器读:最低位置 1,第二字节是空拍,返回值取 rx[1] */ static uint8_t rc522_read_reg(uint8_t reg) { uint8_t tx[2] = { (uint8_t)(((reg << 1) & 0x7E) | 0x80), 0x00 }; uint8_t rx[2] = { 0 }; RC522_CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); RC522_CS_HIGH(); return rx[1]; } /* 置位寄存器中的某些位,用于启动发送、打开天线等 */ static void rc522_set_bits(uint8_t reg, uint8_t mask) { rc522_write_reg(reg, rc522_read_reg(reg) | mask); }HAL_SPI_Transmit的超时参数给 10ms 足够,SPI 时钟建议配到 4~8MHz,太快容易在杜邦线上振铃。片选必须手动控制,RC522_CS_LOW和RC522_CS_HIGH之间不能被打断,所以这两个宏别写成带延时的函数。
复位时序值得单独写一段代码,因为这里出错会导致“上电后第一次能用、后面全废”:
void rc522_init(void) { RC522_RST_LOW(); /* 拉低复位脚 */ HAL_Delay(10); /* 手册要求至少保持 10us,这里给足 */ RC522_RST_HIGH(); HAL_Delay(50); /* 等待内部振荡器起振,别省这一步 */ rc522_write_reg(TModeReg, 0x8D); /* 定时器自动开始,预分频高位 */ rc522_write_reg(TPrescalerReg, 0x3E); /* 预分频低位,对应 40kHz 左右 */ rc522_write_reg(TReloadRegH, 0x00); rc522_write_reg(TReloadRegL, 0x1E); /* 重装值,决定超时时长 */ rc522_write_reg(TxASKReg, 0x40); /* 强制 100% ASK 调制 */ rc522_write_reg(ModeReg, 0x3D); /* CRC 初值 0x6363 */ rc522_write_reg(TxControlReg, 0x03); /* 打开 TX1、TX2 两路天线驱动 */ }天线使能那一行是重点,TxControlReg的 bit0 和 bit1 分别控制两路驱动,只写 0x01 的话读卡距离会明显变短。上电后建议读一次VersionReg(地址 0x37),正常返回 0x91 或 0x92,读不到说明 SPI 根本没通,先解决硬件再往下走。
4.2 寻卡与防冲突:位碰撞检测的实际写法
寻卡就是发一个 REQA(0x26)出去,场上所有处于空闲态的卡都会回一个 2 字节的 ATQA。Mifare 1K 的 ATQA 通常是 0x04 0x00,代表支持 4 字节 UID 的 ISO14443A 卡。
/* req_mode: 0x26 寻未休眠的卡,0x52 唤醒已休眠的卡 */ uint8_t picc_request(uint8_t req_mode, uint8_t *atqa) { uint8_t tx[1] = { req_mode }; uint16_t rx_len = 2; uint8_t status; rc522_write_reg(BitFramingReg, 0x07); /* 短帧,7 位有效 */ status = rc522_transceive(PCD_TRANSCEIVE, tx, 1, atqa, &rx_len, 7); if (status != MI_OK || rx_len != 2) return MI_ERR; return MI_OK; }BitFramingReg写 0x07 是短帧模式,REQA 只有 7 位,这一点写错了卡片根本不会响应。rc522_transceive内部要做四件事:清 FIFO、清中断标志、灌数据、启动发送,然后等ComIrqReg的 RxIRq 或 TimerIRq 置位,再读 FIFO。这段代码比较长,但每一步都不能省。
防冲突是这套设计里最有技术含量的部分。多张卡同时在场上时,它们会同时回 UID,在天线上叠加成碰撞。ISO14443A 的解法是位碰撞检测:读写器发SEL=0x93, NVB=0x20,卡片回 5 个字节(4 字节 UID + 1 字节 BCC),读写器检查每一位,遇到碰撞位就在CollReg里记下来,然后按位重发带确定前缀的请求,逐位把 UID 确定下来。
/* 简化版:支持 4 字节 UID 的单卡选卡,含 BCC 校验 */ uint8_t picc_select(uint8_t *uid) { uint8_t buf[7]; uint16_t rx_len; uint8_t bcc = 0; /* 第一步:防冲突,NVB=0x20 表示只发 SEL,收全部 UID */ buf[0] = 0x93; buf[1] = 0x20; rc522_write_reg(BitFramingReg, 0x00); /* 整字节帧 */ if (rc522_transceive(PCD_TRANSCEIVE, buf, 2, buf, &rx_len, 0) != MI_OK) return MI_ERR; if (rx_len != 5) return MI_ERR; /* 长度不对,多半是多卡碰撞 */ memcpy(uid, buf, 4); for (uint8_t i = 0; i < 4; i++) bcc ^= uid[i]; if (bcc != buf[4]) return MI_ERR; /* BCC 不符,数据不可信 */ /* 第二步:选卡,NVB=0x70 表示 SEL+40 位全部发出 */ buf[0] = 0x93; buf[1] = 0x70; memcpy(&buf[2], uid, 4); buf[6] = bcc; if (rc522_transceive(PCD_TRANSCEIVE, buf, 7, buf, &rx_len, 0) != MI_OK) return MI_ERR; return (rx_len == 3) ? MI_OK : MI_ERR; /* 卡片回 SAK,3 字节 */ }这段是简化版,rx_len不等于 5 时直接返回失败,也就意味着真遇到两张卡同时在场时会选不出来。想拿高分的话,把CollReg(地址 0x0E)的碰撞位读出来,实现完整的位碰撞循环:记录碰撞位置、把该位固定为 0 重发、不行再固定为 1,最多 32 轮就能唯一定位一张卡。代码量大概多三十行,但报告里能画出一张“防冲突轮次与卡数量关系”的图,这就是实打实的工作量。
选卡成功后一定要发 HALT(0x50 0x00),让这张卡进入休眠,否则它会一直响应后续的寻卡命令,导致同一次刷卡被重复上报。业务层再做一层时间窗口去抖,比如同一 UID 在 1.5 秒内只记一次,双保险。
4.3 扇区认证与块读写:密钥、访问位与钱包操作
Mifare 1K 的存储结构是 16 个扇区、每扇区 4 块、每块 16 字节。每个扇区的第 3 块(块 3、7、11……63)是尾块,存 KeyA(6 字节)、访问位(3 字节)、KeyB(6 字节),不能当普通数据块写。这是最容易犯的错误,写坏尾块后整个扇区就锁死了。
认证前必须已经拿到 UID,认证命令的负载是 12 字节:1 字节密钥类型 + 1 字节块号 + 6 字节密钥 + 4 字节 UID。
/* mode: 0x60 用 KeyA 认证,0x61 用 KeyB 认证 */ uint8_t picc_auth(uint8_t mode, uint8_t block, const uint8_t *key, const uint8_t *uid) { uint8_t buf[12]; buf[0] = mode; buf[1] = block; memcpy(&buf[2], key, 6); memcpy(&buf[8], uid, 4); rc522_write_reg(BitFramingReg, 0x00); return rc522_transceive(PCD_AUTHENT, buf, 12, NULL, NULL, 0); } /* 读块:返回 16 字节数据,末尾 2 字节是 CRC_A,要丢掉 */ uint8_t picc_read_block(uint8_t block, uint8_t *data) { uint8_t cmd[2] = { 0x30, block }; uint8_t buf[18]; uint16_t rx_len = 18; if (rc522_transceive(PCD_TRANSCEIVE, cmd, 2, buf, &rx_len, 0) != MI_OK) return MI_ERR; if (rx_len != 18) return MI_ERR; memcpy(data, buf, 16); return MI_OK; }读命令 0x30 后面跟块号,卡片回 16 字节数据 + 2 字节 CRC。返回值是 18 字节,如果只取 16 字节就完事,代码能跑,但报告里写“CRC 校验通过”就站不住脚了。正确做法是把最后 2 字节和本地算出的 CRC_A 比对,不一致就丢弃这一帧。
写块的命令是 0xA0 加块号加 16 字节数据,卡片回 1 字节 ACK(0x0A)。注意写之前必须已经完成认证,认证是对“扇区”生效的,同一个扇区内认证一次可以连续操作多块,但跨扇区必须重新认证。
钱包操作是加分项。Mifare 的值块有固定格式:4 字节小端数值、4 字节取反、4 字节原值、1 字节块地址、1 字节地址取反、2 字节地址。充值用 0xC1(DECREMENT 的变体)配合 0xC2(RESTORE)和 0xC6(TRANSFER),实际上最常见的写法是“读出来—本地加减—写回去”,简单可靠;严格按规范走 RESTORE/TRANSFER 更安全,但代码复杂度上一个台阶。课程设计里用前者就够了,报告里说明理由即可。
4.4 上位机:串口帧解析与SQLite入库
上位机用 Python 写,pyserial收数据、sqlite3存数据、tkinter做界面,全在标准库加一个第三方包的范围内,不用装一堆依赖。
import serial, sqlite3, struct FRAME_HEAD = b'\xAA\x55' def build_frame(cmd: int, payload: bytes) -> bytes: """组帧:帧头 + 长度 + 命令字 + 负载 + 异或校验""" body = bytes([cmd]) + payload chk = 0 for b in body: chk ^= b # 逐字节异或,实现简单且能发现单字节错误 return FRAME_HEAD + bytes([len(body)]) + body + bytes([chk]) def parse_stream(fifo: bytearray): """从缓冲区里切出完整帧,不完整的留着下次继续""" frames = [] while True: idx = fifo.find(FRAME_HEAD) if idx < 0: fifo.clear() # 没有帧头,全部丢弃,防止缓冲区无限增长 break if idx > 0: del fifo[:idx] # 丢掉帧头前的噪声字节 if len(fifo) < 4: break # 连长度字段都没收全 length = fifo[2] total = 3 + length + 1 # 帧头2 + 长度1 + 内容 + 校验1 if len(fifo) < total: break # 帧还没收完,等下一次 body = bytes(fifo[3:3 + length]) chk = fifo[3 + length] calc = 0 for b in body: calc ^= b if calc == chk: frames.append((body[0], body[1:])) del fifo[:total] # 无论校验成功与否都要前进 return framesparse_stream里那个while True循环是关键。串口一次read可能收到半帧、也可能一次收到三帧,用固定长度去读必然出错。正确做法是把收到的字节全部追加到缓冲区,然后循环切帧,切不出来就退出等下一批。校验失败的帧也要从缓冲区里删掉,否则下次还会重复处理同一段垃圾数据。
入库用INSERT OR IGNORE处理重复 UID,用事务包住批量写入,一百条记录一次性提交比逐条提交快一个数量级。
4.5 实验报告里该放哪些数据和图
代码写完只是做了一半,报告里的数据才是评分的抓手。我一般会固定测这几组:
| 测试项 | 方法 | 记录内容 |
|---|---|---|
| 识别距离 | 卡片从 0 沿法线缓慢移动,记录首次识别和首次丢失的位置 | 三次测量取平均,给出有效范围 |
| 响应时间 | 主控打时间戳,从发 REQA 到收到 ATQA 计时 | 统计 100 次的最大值/平均值 |
| 多卡成功率 | 两张卡同时放在天线上,各刷 50 次 | 统计正确选卡次数比 |
| 读写一致性 | 随机写 16 字节,立即回读比对 | 记录 200 次的失配次数 |
| 通信丢帧率 | 连续发 1000 帧,统计上位机正确解析数 | 给出丢帧率和原因分类 |
| 连续运行稳定性 | 连续刷 2 小时 | 记录异常次数和恢复方式 |
每张表配一段分析,说明为什么这个值偏高或偏低。比如识别距离只有 25mm,就解释天线尺寸和驱动电流的关系;多卡成功率 92% 没到 95%,就说明简化版防冲突在 UID 前缀相同时会退化。承认不足并给出改进方向,比硬说“系统运行良好”要可信得多。
5. 避坑与排查:五类最常见的翻车现场
这一章按“现象—原因—解决”写,都是我和身边人真踩过的。调试顺序建议从下往上:先确认 SPI 通不通,再确认寻卡有没有响应,再看防冲突,最后看认证和读写。跳着查会浪费大量时间。
5.1 读卡距离只有一两厘米,手一遮就掉卡
现象:模块能读卡,但必须把卡贴在板子上,稍微抬起来一点就没反应,手指靠近天线也会掉卡。
原因:三个方向。一是供电,MFRC522 发射瞬间电流接近 100mA,如果和主控共用细走线或共用 USB 供电,电压会被瞬间拉低;二是天线驱动没开全,TxControlReg只写了 0x01 而不是 0x03;三是天线附近有金属或大面积覆铜,13.56MHz 的磁场被涡流吃掉。
解决:先量电压,发射瞬间 3.3V 掉到 3.0V 以下就是供电问题,就近加 10uF 电容或换独立 LDO。再读一次TxControlReg确认是 0x03。最后把模块远离金属,线圈平面不要贴着桌面或机箱。改完重测距离,一般能从 1cm 提到 3~5cm。
5.2 寻卡偶发失败,ErrorReg 报冲突错误
现象:大部分时候能读到,但每隔十几次失败一次,读ErrorReg是 0x08(CollErr)。
原因:卡片的响应帧长度不是 5 字节,说明有碰撞或者收到了噪声帧。常见于两种场景:两张卡同时放上去;上一次刷卡后没有发 HALT,卡还在响应。
解决:一是在picc_request之前先发一次 HALT 广播把场上卡片全部休眠,再重新唤醒;二是在picc_select里对rx_len != 5的情况重试两次,仍失败则返回错误交给业务层去抖;三是检查超时寄存器TReloadReg是否太小,重装值 0x1E 对应大约 25ms,如果改到了 0x0A 以下,卡片还没回完就超时了。
5.3 SPI读回来恒为 0x00 或 0xFF
现象:程序跑起来读VersionReg一直是 0x00 或 0xFF,后面所有操作全部失败。
原因:0x00 通常是 MISO 没接好或者从机根本没被选中;0xFF 通常是 MISO 被拉高、主机采样不到有效电平。还有一种是 SPI 模式配错,MFRC522 要求模式 0(CPOL=0, CPHA=0),配成模式 3 时序就错位。
解决:先用万用表量 MISO 静态电平,悬空约 1.6V 才算正常;再检查 NSS 是否真的是软件控制在拉低,用示波器看片选波形,一次读写应该有明显的低电平窗口;最后核对 SPI 初始化参数,模式 0、MSB 先行、8 位数据、时钟先分频到 1MHz 试,通了再往上提。
5.4 写块返回成功,但数据没变
现象:picc_write_block返回 OK,卡片也回了 ACK,但马上读回来还是旧数据,甚至整张卡再也读不了了。
原因:大概率写到了尾块(块号 3、7、11……63)。尾块存的是密钥和访问位,写进去之后密钥就被覆盖了,如果访问位恰好被写成不可写,这个扇区就永久锁死。次一级的原因是没有先认证就写,或者认证用的是 KeyA 但尾块的访问位只允许 KeyB 写。
解决:写之前加一道断言,block % 4 != 3才允许写,这一行能省掉很多废卡。已经在用的卡如果被锁,换一个扇区继续做实验即可,别在报告里回避,写清楚“因误写尾块导致扇区 2 锁定,改用扇区 4 完成后续实验”,反而是很真实的排错记录。
5.5 上位机串口丢帧、日志缺记录
现象:刷卡很频繁的时候,界面上的记录条数比实际刷卡次数少,偶尔还会出现乱码或整条记录消失。
原因:三种可能。波特率不匹配或者晶振误差累积;接收端用固定长度读,一帧被拆成两次收到的部分被丢弃;USB 转串口芯片在高速率下缓冲不够,驱动层丢字节。
解决:先在协议层解决,用 4.4 节那个带缓冲的切帧函数,任何情况下都不丢半帧;再在应用层加一层序号,主控每发一帧序号加一,上位机发现序号跳变就记一条“丢帧”日志,这样至少知道丢了多少;最后降波特率到 57600 试试,如果丢帧消失,说明是转串口硬件的问题,换一个芯片方案更稳。
6. 验收与答辩:用一套自测脚本和指标表把演示变成可复现
答辩现场最怕的不是功能少,而是“刚才还好好的”。我的做法是提前把演示拆成四个可以独立证明的环节,每个环节配一条命令或一个动作,按顺序走,任何一环出问题都能当场定位而不是手忙脚乱。
第一环,硬件自检。上电后串口打印VersionReg的读数、当前TxControlReg的值、以及一次空场寻卡的返回码。这三行输出能在一秒内证明 SPI 通了、天线开了、射频前端在工作。很多同学上来就演示刷开锁,一旦失败就卡住,其实问题往往就在这三行里。
第二环,单卡全流程。放一张卡,串口依次打印 UID、认证结果、读到的 16 字节、写入后回读的比对结果。这一串输出要能截图进报告。
第三环,多卡与异常。两张卡同时放上去,看是否正确选出其中一张;拿一张没注册的卡刷,看是否走的是失败分支而不是崩溃;把卡拿远一点再靠近,看是否能恢复。
第四环,上位机数据。打开数据库文件,用一条 SQL 现场统计。
-- 现场演示用:按卡号统计刷卡次数与成功率 SELECT uid, COUNT(*) AS total, SUM(result) AS ok_cnt, ROUND(1.0 * SUM(result) / COUNT(*), 3) AS ok_rate FROM swipe_log GROUP BY uid ORDER BY total DESC;这条查询一敲出来,表格里每一行都是可验证的数字,比口头说“系统很稳定”有说服力得多。
验收指标建议提前做成一张表,答辩时直接投出来:
| 指标 | 目标值 | 实测值 | 判定 |
|---|---|---|---|
| 识别距离 | ≥30mm | 38mm | 通过 |
| 单次识别耗时 | ≤500ms | 约 120ms | 通过 |
| 双卡识别成功率 | ≥95% | 96% | 通过 |
| 读写一致性 | 100% | 100%(200 次) | 通过 |
| 连续 1000 帧丢帧率 | ≤1% | 0.3% | 通过 |
| 连续运行 2 小时异常 | 0 次 | 1 次(重新上电恢复) | 记录说明 |
那张表里最后一行我特意保留了一次异常。评审问起来,能说清楚“第 78 分钟出现一次寻卡超时,怀疑是电源纹波,重新上电后恢复,后续复现未成功”,比全是满分更可信,也说明确实长时间跑过。
从那以后我每次带这类课程设计,都强制走一遍「上电自检三行输出 + 四环演示脚本」,不管前一天调得多顺,答辩当天早上一定重跑一次,因为线松了、卡被写坏了、串口号变了这种问题,永远在你想不到的时候出现。这套代码结构、报告章节模板和测试表格都整理在一起了,需要的可以拿去对着改,改成自己的题目也完全够用。希望帮到你。
本文还有配套的精品资源,点击获取