STM32+RC522实现RFID校园一卡通:M1卡块规划与SPI驱动详解
2026/9/13 6:16:35 网站建设 项目流程

简介:这是一份针对基于RFID的校园一卡通系统的完整课程设计资源,以STM32为平台,覆盖门禁、消费充值和借书还书三类典型应用场景,适合物联网、嵌入式方向的在校生以及需要快速搭建RFID项目的开发者参考。资源包共168个文件、约5.57MB,涵盖C语言源码、头文件、Keil工程配置、hex/axf编译产物、链接映射文件及DOCX课程设计文档,便于从源码到实物全流程学习。目前已有2765人学习或下载。通过该项目,读者可以掌握IC卡基础信息的读取方式、余额读写与扣费流程、门禁权限判断逻辑以及图书借阅信息管理,理解一卡通系统中数据存储与多模块协同的设计思路。整套资料代码结构清晰、注释完整,既可作为射频识别程序设计课程的结课作业,也能为后续的嵌入式综合项目提供可复用的参考模板。

1. 从刷卡到后台:基于RFID的校园一卡通课程设计到底在做什么

这套课程设计把射频识别(RFID)、单片机控制器和业务逻辑串在了一条线上:STM32F103 通过 SPI 协议挂载 RC522 读卡头,读取 13.56MHz 的 M1 卡,卡片里预先写好学号、姓名、余额和借书状态。刷卡时,读卡器先做寻卡、防碰撞、选卡和密钥认证,再把 16 字节一块的数据读回来,用位运算拆成业务字段。真正的设计难点不是“读卡号”,而是 M1 卡块地址怎么规划、Key A/Key B 怎么配、充值时掉电怎么处理,以及如何把 stm32f10x 标准库里的 Flash、定时器、ADC 资源用起来。这里不会带你一行行抄代码,而是按一套可复现的流程拆透:先想清楚协议,再动手写驱动,最后补上排错经验。

2. STM32F10x 与 RC522 的 SPI 读写链路,先把 M1 卡存储模型立住

2.1 为什么选用 13.56MHz 的 RC522 和 M1 卡

校园一卡通课程设计里最常见的是 RC522 读卡芯片加 M1/Mifare Classic S50 卡。RC522 支持 ISO14443A 协议,寻卡、防碰撞、密钥认证、块读写这些底层工作时序都在芯片内部完成,STM32 只需要通过 SPI 接口向 RC522 寄存器写命令字和参数。相比低频 125kHz 的 EM4100,这种方案的安全边界更完整:M1 卡有独立扇区、独立密钥,能模拟“学生信息区、余额区、借书区”三个维度的应用,比单纯读一个 ID 卡号要有说服力。

M1 S50 容量为 1KB EEPROM,分为 16 个扇区,每个扇区 4 个块,每块 16 字节。前三个块是数据块,第四个块是控制块。控制块里有 Key A、访问位、Key B,决定该扇区内的三个块能不能读写。也就是说课程设计里“读卡”天然分两层:先选卡,再认证,认证通过后才能操作块数据。很多新手只做到了 PcdRequest,卡号读出来了就以为做完,这离真正的一卡通还隔着一层密钥体系。

2.2 引脚分配与 SPI 初始化

RC522 支持 SPI、I2C、UART 三种接口,在 STM32 工程里最常用 SPI,因为它简单:SCK、MISO、MOSI、NSS 四根线,加上 RST 复位。我一般把 RC522 挂在 SPI1 上,引脚映射为 PA5-PA7,NSS 用普通 GPIO 做软件片选,这样 Pin 控制更直接,不用让硬件 NSS 自动翻转引起误触发。

功能STM32 引脚方向说明
SDA/NSSPA4输出软件片选,低有效
SCKPA5输出SPI 时钟
MISOPA6输入从 RC522 读数据
MOSIPA7输出写命令和数据
RSTPA3输出复位,拉高进入工作状态

下面这段初始化来自项目里比较典型的标准库配置。注意 SPI 工作模式必须和 RC522 数据手册的时序对应,RC522 要求空闲时钟为低,数据在上升沿被采样,也就是 SPI Mode 0:CPOL=Low、CPHA=1Edge。

void RC522_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* PA5/PA6/PA7 作为 SPI1 复用推挽输出 */ gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* PA3 是 RST,PA4 是软 NSS,都做成普通推挽输出 */ gpio.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode = SPI_Mode_Master; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_CPOL = SPI_CPOL_Low; spi.SPI_CPHA = SPI_CPHA_1Edge; spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; spi.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spi); SPI_Cmd(SPI1, ENABLE); RC522_RST_HIGH(); }

这段代码里有几个点值得展开。首先,SPI_CPOL_Low 表示空闲时 SCK 电平为低,SPI_CPHA_1Edge 表示数据在第一个边沿采样,这是 RC522 默认推荐的时序。其次,分频系数选 32,系统时钟 72MHz 时 SPI 时钟为 2.25MHz,对 RC522 模块加杜邦线的场景足够稳定。最后,PA3/PA4 配成普通推挽输出而不是复用,是为了让复位和片选完全由软件掌控。很多读者把 NSS 配置成 SPI_NSS_Hard,结果每次 SPI 通信时片选由硬件自动拉低,RC522 反而收不到完整命令。

2.3 M1 卡块地址:不规划清楚后面全是坑

RFID 一卡通最常见的“业务事故”是把学生信息写在扇区 0 块 3,或者把控制块当成数据块写,导致整张卡锁死。M1 卡每个扇区的第 4 个块是控制块,其余 3 个块是可读可写的数据块。扇区 0 的块 0 比较特殊,里面是出厂 UID 和厂商数据,一般只读。默认控制块内容为 FF FF FF FF FF FF FF 07 80 69 FF FF FF FF FF FF,也就是 Key A 和 Key B 都是六个 FF,访问位控制在默认状态:Key A 可以读,数据块可以被 Key A 或 Key B 认证后读写。

扇区块号建议用途说明
Sector 0Block 0UID/厂商数据生产时写入,不要覆盖
Sector 0Block 1学生信息学号、姓名
Sector 0Block 2余额数据余额 + 校验
Sector 0Block 3控制块不要当数据区写
Sector 1Block 0门禁权限门位图、生效日期
Sector 1Block 1借书信息图书编号、借书时间
Sector 1Block 2流水备份最近一次消费流水号

这样分块的好处是:门禁、消费、借书三个业务都有独立扇区,后续只要换 Key 就能独立授权。如果全部堆在一个块里,改余额要连带读写姓名和权限,每次写卡之前还要把整块数据读出来重新拼装,逻辑会很混乱。

2.4 用结构体规划一块 16 字节的学生信息

M1 卡最小操作单位是块,一次 PcdRead/PcdWrite 固定 16 字节。因此学生信息在设计阶段就应该凑成 16 字节的整数倍。常见课程设计会把学号存成 8 字节 ASCII,姓名存成 7 字节,再加 1 字节保留位:

typedef struct { uint8_t userId[8]; /* 学号,ASCII 码,例如 "20230001" */ uint8_t name[7]; /* 姓名,UTF-8 编码 */ uint8_t reserved; /* 保留,填 0 对齐到 16 字节 */ } StudentInfo;

写卡前要对 struct 做清零,避免残留脏数据。读卡后可以用 memcpy 把块数据拷贝到 StudentInfo 变量,再按需要当字符串打印。要注意 M1 卡写块时如果剩余字节不是预期的 0,读回来会看到上一次写入的随机值,所以每次写整块数据前尽量先清零目标 buffer,再填业务字段。这个习惯能省掉很多莫名其妙的乱码问题。

3. 寻卡、防碰撞、密钥认证:RC522 内核代码的执行顺序

3.1 一条完整的读块调用链

RFID 读卡器和邮箱很像:你不能直接打开别人的信箱,要先识别是哪栋楼、哪一户,再拿钥匙开。RC522 的命令链也是这样,寻卡、防碰撞、选卡、认证、读/写,每一步都有明确作用。下面这套封装是我在一个基于 STM32F10x 的课程设计里常用的读块接口:

uint8_t M1_ReadBlock(uint8_t block, uint8_t *buf) { uint8_t uid[4]; uint8_t atqa[2]; /* 1. 寻卡:向场中发送 REQA,等待卡片应答 */ if (PcdRequest(PICC_REQALL, atqa) != MI_OK) { return 1; } /* 2. 防碰撞:获取 4 字节 UID */ if (PcdAnticoll(uid) != MI_OK) { return 2; } /* 3. 选卡:确认这张卡被选中,进入 READY 状态 */ if (PcdSelect(uid) != MI_OK) { return 3; } /* 4. 密钥认证:认证目标块 */ if (PcdAuthState(PICC_AUTHENT1A, block, g_defaultKey, uid) != MI_OK) { return 4; } /* 5. 读块数据 */ return PcdRead(block, buf) == MI_OK ? 0 : 5; }

PcdRequest(PICC_REQALL, atqa) 的第一参数是请求命令类型,PICC_REQALL 会让场中所有可响应的卡都应答,适用于刷卡触发场景;PICC_REQIDL 只请求处于 idle 状态的卡,适合做连续轮询。第二个参数 atqa 是 2 字节应答数据,它由物理层调制解调结果填充,不需要手动解析。

PcdAnticoll 是防碰撞的关键。多张卡同时入场时,卡的 UID 位流会冲突,RC522 寄存器通过位冲突检测得到一张卡的 UID。课程设计里如果出现“明明只有一张卡,却总是返回防碰撞失败”,先把卡片拿远一点,确认场中真的只有一张卡,再检查接线。

PcdSelect 执行后卡片进入 READY 状态,从这一刻起才能对它做密钥认证。PcdAuthState 的参数 block 不是扇区而是具体块号,认证时会校验当前卡的块访问控制位和 Key A/Key B。后续 PcdRead 才能读到真实数据,否则读回来全是被置位的伪数据。

3.2 写块接口和权限选择

写卡接口结构几乎一致,只是把 PcdRead 换成了 PcdWrite。课程设计里通常会预留 Key B 认证,这是因为有些场景希望“读卡用 Key A,写卡用 Key B”,两种密钥权限不同。比如发卡时只给学生卡 Key A 的读权限,后台管理机用 Key B 更新余额,能防止持卡人用读到的数据反推写权限。

uint8_t M1_WriteBlock(uint8_t block, const uint8_t *buf) { uint8_t uid[4]; uint8_t atqa[2]; if (PcdRequest(PICC_REQALL, atqa) != MI_OK) return 1; if (PcdAnticoll(uid) != MI_OK) return 2; if (PcdSelect(uid) != MI_OK) return 3; if (PcdAuthState(PICC_AUTHENT1B, block, g_defaultKey, uid) != MI_OK) return 4; return PcdWrite(block, buf) == MI_OK ? 0 : 5; }

这里有个容易被忽略的细节:M1 卡的写操作必须是完整 16 字节。你不能写“前 8 字节学号”,然后把剩余 8 字节丢到一边。因此写数据之前必须先读回旧数据,把要改动的字段替换进去,再整体写回。否则上一次写入的其他字段会被全部清零。

注意:不要对控制块执行普通数据写操作。控制块的访问位一旦被误写,这个扇区可能永久无法认证。

3.3 用状态码判断卡在哪个环节失败

调试时只看“读卡失败”是不够的,要把协议层面的状态码打印出来,才能定位是物理层没卡、认证失败还是读数据超时。下表是常见返回值的含义:

返回值出错环节
0MI_OK成功
1MI_NOTAGERR场中无卡或卡片离开
2MI_ERR通信 CRC、奇偶校验错误
3MI_AUTHERR密钥错误、访问位限制
4MI_READERR读块操作失败
5MI_WRITEERR写块操作失败

我在调试时一般会在每条函数调用后串口打印状态码,比如执行到 PcdAuthState 返回 3,说明前三级都过了,问题集中在密钥或访问控制位。直接用串口输出状态码,比对着逻辑分析仪猜更高效。

4. 一卡通业务落地:消费充值、门禁、借书还书的状态机设计

4.1 块地址规划先于代码

课程设计文档里写清了门禁、消费充值、借书还书三块功能,但这三块不能都在一个扇区里堆数据。我建议把卡内存储规划为“身份区 + 业务区 + 备份区”。身份区放学号和姓名,业务区放余额、权限和借书信息,备份区放流水号或余额镜像,用来应对充值掉电。

实际应用中这样规划:

数据块内容长度
块 1学号 + 姓名16 字节
块 2余额 + 校验码16 字节
块 4门禁权限位图 + 有效期16 字节
块 5当前借书信息16 字节
块 6最近流水号 + 余额备份16 字节

这里的块 4、5、6 都属于扇区 1,和扇区 0 的密钥可以分别配置。门禁控制员只拿到块 4 的读权限,图书馆终端只拿块 5 的读写权限,不会互相越权。实际写代码时可以定义成宏,方便换地址。

4.2 余额读写:校验码防止“读卡一半断电”

余额是低频次高价值数据,哪怕课程设计,也要考虑读改写窗口。刷卡消费时,第一步读余额块,第二步在内存中扣减,第三步写回。如果第二步和第三步之间掉电,卡上余额还是旧值,商品已经拿走,账目对不上。

我常用的做法是在余额块里放一个校验字节。校验值用余额本身的高 16 位异或低 16 位生成,启动时先校验,校验失败就默认余额为 0 或进入人工对账流程。示例:

int UpdateBalance(int delta) { uint8_t data[16] = {0}; uint32_t balance; int status; status = M1_ReadBlock(BLOCK_BALANCE, data); if (status != 0) return -1; /* 低 4 字节存余额,第 1 字节存校验值 */ balance = data[1] | (data[2] << 8) | ((uint32_t)data[3] << 16) | ((uint32_t)data[4] << 24); if ((uint8_t)(balance ^ (balance >> 8)) != data[0]) { return -2; /* 校验失败,拒绝交易 */ } balance += delta; data[0] = (uint8_t)(balance ^ (balance >> 8)); data[1] = balance & 0xFF; data[2] = (balance >> 8) & 0xFF; data[3] = (balance >> 16) & 0xFF; data[4] = (balance >> 24) & 0xFF; status = M1_WriteBlock(BLOCK_BALANCE, data); if (status != 0) return -3; return 0; }

函数逻辑分四步:先从卡片读余额块;再用预置校验值验证数据完整性;然后把 delta 加到余额上,正数代表充值,负数代表消费;最后把新余额和校验值写回卡内。注意 delta 是 int,正负号在业务层处理好,这样函数本身不看“消费”还是“充值”,只负责余额变更。

提示:余额块不要定义成 32 位整数就完事,至少保留 16 字节块内的其余空间。一期只用到 5 字节,二期还能加冻结余额或者累计消费次数。

4.3 门禁和借书还书的卡片状态判断

门禁本质是“身份识别 + 权限位图”。把门号和权限编码到卡片的一块数据里,读卡时检查这个位图,而不只是判断 UID 在不在白名单。这样管理员不用提前把每张卡写进后台,发卡时直接写权限区即可。

如果项目里用到了 stm32f10x_flash.c,常见用途是把卡片注销信息存进 STM32 内部 Flash,掉电不丢失。门禁机上电时把 Flash 里的黑名单读出来,刷卡后先比对黑白名单,再查卡内权限位。这样即便持卡人卡片被复制,黑名单仍然生效,安全性比单纯看卡内权限高一层。

借书还书的字段可以用类似结构体:

typedef struct { uint8_t bookId[12]; /* 图书条形码,ASCII */ uint8_t borrowDate; /* 以 2025 年 1 月 1 日为 0 的偏移 */ uint8_t returned; /* 0 表示未还,1 表示已归还 */ uint8_t reserve[2]; /* 对齐 16 字节 */ } BorrowInfo;

刷卡借书时,先读 BorrowInfo。如果 returned 为 0,说明卡上还有未还图书,直接提示“请先还书”。如果 returned 为 1,就写入 bookId 和 borrowDate,再把 returned 置 0。还书时反向操作,只读不写图书管理系统也能完成流程:管理员扫描图书条形码,把卡上的 returned 置 1 即可。

消费、门禁、借书三条链路共用同一套 RC522 协议层,业务层各自写卡片字段。这就把复杂系统拆成了“读卡驱动、数据区、状态机”三层,也是课程设计文档需要呈现的架构层次。

5. 调试一卡通:SPI 时序、状态码映射与防克隆验证

读卡不稳定时,优先级最高的是检查 SPI 时序。先把示波器或逻辑分析仪接在 SCK 和 MISO 上,观察 SCK 空闲电平是否接近 0V。RC522 的 SPI 是 Mode 0,CPOL=0、CPHA=0,也就是 SCK 低电平空闲,数据在上升沿采样。如果初始化里误配成 Mode 3,读出来的数据会错位或全是前一次命令的残留值。杜邦线较长时,可把 SPI 分频从 32 降到 64,用 1.125MHz 再试,优先保证通信稳,再追求速度。

状态码能帮助快速分类故障。场中无卡会卡在 PcdRequest,PcdAnticoll 报错往往因为有杂物或同时出现多张卡,认证报错则要检查密钥和访问位。我曾经遇到整张卡所有块都读不出,最后发现是控制块 Access Bits 被上位机软件误改,扇区永久锁死。因此课程设计中建议准备两张普通 M1 卡,一张作为实验卡,一张作为备份卡。

防克隆层面,不要只拿 4 字节 UID 判断身份。UID 可以被同类型卡模拟,一卡通系统应该在卡内写入随机数种子,后端保存对应的挑战应答结果。课程设计不用真正实现国密算法,至少要在权限区写一个不可变随机 ID,然后在 STM32 Flash 里存一份卡号白名单,双重校验。项目里的 stm32f10x_flash.c 刚好可以做这个白名单存储。

验证时我习惯在 main 函数里加一个命令循环,手动输入数字:1 寻卡、2 读学生信息、3 读余额、4 写余额,每次操作完串口打印状态码。这样能把协议层稳定性和业务层逻辑分开测试。调通后再把所有 RC522 原语函数封装成rc522_card_io.c,上层只调用ReadBlock/WriteBlock/AuthCard,后续换 NFC 读卡器或者移植到蓝牙读卡器时,只需要替换驱动层,一卡通业务代码基本不用动。

本文还有配套的精品资源,点击获取

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

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

立即咨询