
简介这是一份针对基于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-PA7NSS 用普通 GPIO 做软件片选这样 Pin 控制更直接不用让硬件 NSS 自动翻转引起误触发。功能STM32 引脚方向说明SDA/NSSPA4输出软件片选低有效SCKPA5输出SPI 时钟MISOPA6输入从 RC522 读数据MOSIPA7输出写命令和数据RSTPA3输出复位拉高进入工作状态下面这段初始化来自项目里比较典型的标准库配置。注意 SPI 工作模式必须和 RC522 数据手册的时序对应RC522 要求空闲时钟为低数据在上升沿被采样也就是 SPI Mode 0CPOLLow、CPHA1Edge。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 是 RSTPA4 是软 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 0CPOL0、CPHA0也就是 SCK 低电平空闲数据在上升沿采样。如果初始化里误配成 Mode 3读出来的数据会错位或全是前一次命令的残留值。杜邦线较长时可把 SPI 分频从 32 降到 64用 1.125MHz 再试优先保证通信稳再追求速度。状态码能帮助快速分类故障。场中无卡会卡在 PcdRequestPcdAnticoll 报错往往因为有杂物或同时出现多张卡认证报错则要检查密钥和访问位。我曾经遇到整张卡所有块都读不出最后发现是控制块 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 读卡器或者移植到蓝牙读卡器时只需要替换驱动层一卡通业务代码基本不用动。本文还有配套的精品资源点击获取