简介:基于STM32的RFID停车场项目源码与配套数据包,定位为嵌入式方向毕业设计、期末大作业和课程设计的高分参考方案,适合具备基础C语言与STM32开发经验、希望快速搭建完整项目的学习者。代码内含清晰注释,从RFID刷卡识别、车位状态管理到LCD显示与串口交互均有对应实现,系统功能完善、部署简单;资源压缩包约8.33MB,共240个文件,以h头文件、c源程序、o目标文件及uvprojx工程文件为主,另有crf中间文件、hex烧录文件和相关配置,覆盖编译、调试与下载全流程,便于边阅读边实验。已有171人学习使用,项目经过严格调试确保可运行,适合作为毕业设计或期末大作业模板,在理解代码结构后按需修改即可。
1. 为什么“高分项目”到你手里经常跑不起来
做课设或毕设时下载的“基于STM32的RFID停车场项目源码+全部数据(高分项目)”,最常见的结局是:代码在Keil里能编译,烧到板子上LCD白屏、读卡没反应,最后你对着原理图和PDF报告干瞪眼。这个标题的背后其实是一套很标准的嵌入式课程设计组合——STM32做主控、RC522(或MFRC522)做13.56MHz高频读卡、M1卡做车辆身份凭证,再加上LCD显示和电机模拟道闸。它能拿高分,不是因为代码有多玄学,而是完整覆盖了“传感器读取→主控逻辑→执行机构→数据留痕”这条完整链路。这篇文章就是帮你把这套东西从“能编译”推进到“真能跑”,并且把卡号、余额、进出记录这些“全部数据”真正落到自己能看懂的地方。适合正在做嵌入式课设、考研复试项目,或者想快速搭一个门禁/停车demo的从业者。
2. 硬件选型和框架:RC522为什么是首选,引脚怎么接
2.1 高频RFID在停车场景的选型逻辑
停车场道闸场景下,车辆身份识别有两条常见路线:一条是5V供电的低频EM4100/EM4305,读取距离只有5到10厘米,适合室内门禁不适合车窗摇下来刷卡;另一条是13.56MHz高频ISO14443A协议,代表芯片就是RC522和PN532,读卡距离能做到3到5厘米,配合大尺寸天线线圈还能更远。RC522在淘宝单价不到5块,模块自带天线,直接能读M1卡(S50),协议栈和驱动代码在网上极其成熟,所以几乎所有“停车场/图书馆/门禁”课设项目都用它。PN532虽然性能更好,但模块价格高一截,做课设没有意义。
注意一个命名坑:项目标题写“RFID停车场”,源码里大概率用的是MFRC522库,而不是RC522原厂库。MFRC522兼容RC522寄存器,但有些模块上电默认处于硬复位状态,必须软件复位一次才能进Idle模式。如果你用现成的MFRC522库读不到卡,先检查复位时序和SPI模式。
2.2 STM32芯片选型和引脚分配
这套系统用STM32F103C8T6(最小系统板)就够了,64KB Flash对RC522驱动、LCD驱动、状态机逻辑来说绰绰有余。但注意,F103C8T6只有48个引脚,如果你还要同时接LCD、舵机、按键、蜂鸣器,引脚规划要提前做。我一般会这样分配:
| 功能 | 引脚 | 说明 |
|---|---|---|
| SPI1_SCK | PA5 | 接RC522 SCK |
| SPI1_MISO | PA6 | 接RC522 MISO(千万不能接错) |
| SPI1_MOSI | PA7 | 接RC522 MOSI |
| RC522_CS | PA4 | 软件控制片选,不要用硬件NSS |
| RC522_RST | PA3 | 复位引脚 |
| LCD_RS | PB0 | 4线/8线LCD的寄存器选择 |
| LCD_EN | PB1 | LCD使能 |
| LCD_D4-D7 | PB2-PB5 | 4位数据线 |
| SG90舵机 | PA1 | 模拟道闸开合,PWM输出 |
用GPIO模拟SPI也能读卡,但RC522读写SD卡文件时需要比较高的速率,GPIO模拟在32MHz主频下容易时序抖动。常见做法是直接用硬件SPI1并把SPI速率配到2MHz左右,不要超过10MHz,RC522的SPI时钟上限是10MHz,但你还有杜邦线和面包板这种物理瓶颈,跑快了反而丢数据。
2.3 电气连接和上电时序的三个细节
第一,RC522模块是3.3V供电,绝对不能接5V,模块上的AMS1117-3.3虽然能稳压,但输入5V后输出的纹波会干扰读卡模拟前端。第二,STM32的PB和PA引脚全是3.3V逻辑电平,RC522模块也是3.3V,不需要电平转换,但如果你用的是5V的LCD1602,背光和控制引脚要串1kΩ电阻限流。第三,RC522模块的IRQ引脚是可选的,做停车场道闸这种低速刷卡车位,用轮询替代中断完全够用,IRQ悬空就行。
提示:F103C8T6的SPI1的NSS引脚(PA4)如果配置成复用推挽输出,默认会启用硬件片选模式,RC522读不出数据。要么把PA4设成普通GPIO输出,要么干脆用别的引脚做CS。
3. 读卡核心流程:M1卡的扇区结构、ISO14443A寻卡和密钥认证
3.1 M1卡的存储结构决定你能存什么数据
S50卡(Mifare Classic 1K)内部有16个扇区,每扇区4块,每块16字节。第0扇区的块0是厂商代码(UID所在的块,只读),写卡程序绝不能动它。每个扇区的块3是尾块,存储KeyA(6字节)、访问位(4字节)、KeyB(6字节),控制读写的访问条件。停车项目里常用的做法是:把卡号(4字节UID)当主键,把用户姓名、车牌号、余额存进扇区1的块0和块1,月卡到期时间存进块2。默认出厂Key是FFFFFFFFFFFF,如果项目源码里有自定义密钥,要先去读扇区0块3确认密钥是否被改过,否则后面所有读写操作都会显示“认证失败”。
用RC522读一张空卡,第一次读到的UID就是卡的全球唯一序列号,这个序列号后面接的1字节checksum是BCC校验值,不要把它当数据存进去。常见错误是在解析UID时把整个5字节数组都存了,做卡号比对时永远匹配不上。
3.2 RC522初始化、寻卡和读卡号的最小代码
MFRC522库的初始化核心是SPI配置和寄存器设置。初始化完成后的读卡三步是:寻卡(REQUEST/ANTICOLLISION)、选卡(SELECT CAS)、认证(AUTHENTICATE)。下面这段代码是在标准MFRC522库基础上精简后的核心,可以直接替换到项目主循环里。
uint8_t RFID_RequestRead(void) { uint8_t status; uint8_t cardType[2] = {0x00, 0x00}; // 发送寻卡命令 PCD_REQA 0x26,TypeA卡会回复2字节ATQA status = MFRC522_Request(PICC_REQIDL, cardType); if (status == MI_OK) { // cardType[0]保存ATQA高字节,cardType[1]保存低字节 // 0x0400表示Mifare Classic 1K return 1; } return 0; } uint8_t RFID_AntiCollisionAndSelect(void) { uint8_t status; uint8_t uid[5] = {0}; // 4字节UID + 1字节BCC校验 // 防碰撞:多卡同时在场时返回单卡的UID status = MFRC522_Anticoll(uid); if (status != MI_OK) { return 0; } // 选中这张卡,SDD和UID拼成完整帧发给读卡器 status = MFRC522_Select(uid); if (status != MI_OK) { return 0; } // uid[0]到uid[3]才是真正的卡号,uid[4]是校验和 memcpy(g_cardId, uid, 4); return 1; } uint8_t RFID_ReadBalance(uint8_t *balance) { uint8_t status; uint8_t blockData[16] = {0}; // 默认密钥:整整6个0xFF uint8_t defaultKey[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 认证扇区1(块4到块7属于扇区1) status = MFRC522_Auth(PICC_AUTHENT1A, 4, defaultKey, g_cardId); if (status != MI_OK) { return 0; } // 读取扇区1的块0(绝对地址为4) status = MFRC522_Read(4, blockData); if (status != MI_OK) { return 0; } // 这里假设项目约定:blockData[0] = 余额高字节,blockData[1] = 余额低字节 *balance = (blockData[0] << 8) | blockData[1]; return 1; }代码背后有两个关键点:MFRC522Request发出的0x26是REQA指令,用于唤醒休眠状态的TypeA卡;Anticoll发送0x93:20,当两张卡同时靠近时序列冲突算法会自动逐位仲裁,返回其中一张的UID。很多人在这里翻车是因为没有先调Anticoll直接去读UID数组,从Request拿到的ATQA里抠卡号,当然抠不出来。
3.3 主循环里的读卡状态机
停车场的读卡逻辑不能是“一卡一进一出”的简单轮询,因为刷卡动作会触发两次以上同卡的读取。要做一个简单的状态机:等待刷卡→寻卡成功→记录卡号→执行认证→读数据→清除状态。清除状态这步特别重要,否则同一张卡会在500ms内被连续识别4到5次,一个月卡进出记录会被重复写。
项目源码里常见的轮询写法是while (1) { if (RFID_RequestRead()) { ... } },问题在于RFID_RequestRead只负责识别有没有卡,真正取卡号还得再调用Anticoll,这两步之间只要你稍微delay超过5ms,卡可能已经离开了射频场,后续认证全失败。正确姿势是把寻卡、防冲突、选卡三个动作合并成一个函数,一气呵成,任何一步失败就直接返回,不要分拆开。
4. 停车业务逻辑和“全部数据”的落地:卡号映射、进出记录、掉电保存
4.1 停车场业务状态机的设计
停车场的业务本质是一个“四态切换”:月卡/临时卡进、场内、出场、缴费/放行。状态机的事件来源有三个:读卡器、按键(模拟取卡/出场按钮)、红外对射(模拟入场车辆检测)。项目源码里如果只有“读卡→判断卡号→舵机转→结束”这种线性流程,那它不是停车场项目,只是个门禁demo。地道一点的做法是先定义车辆状态结构体,再定义状态机表。
typedef struct { uint8_t valid; // 0无效 1有效 uint8_t cardId[4]; // 卡号 uint8_t balance; // 余额 uint32_t entryTime; // 入场时间戳(SysTick) uint8_t inParking; // 当前停车场内状态 } VehicleNode; VehicleNode g_vehicles[50]; // 最多同时管理50辆车 uint8_t Vehicle_FindByCardId(uint8_t *cardId) { for (int i = 0; i < 50; i++) { if (g_vehicles[i].valid && memcmp(g_vehicles[i].cardId, cardId, 4) == 0) { return i; } } return 0xFF; // 未找到 }这里有一个新手容易踩的设计坑:把余额直接写进M1卡,每次扣费都改卡上数据。这不是不能做,但M1卡每个扇区有10万次擦写寿命,频繁扣款会导致扇区损坏,逻辑上也没有“黑匣子”——卡丢了余额记录就没了。更稳妥的做法是把卡号当索引,车辆档案和余额存MCU侧Flash或SD卡,卡里只存固定ID。这也是“全部数据”这个标题点出来的隐含需求——你不光要刷卡开门,还要能输出“今日进场车辆列表、出场扣费明细”这种数据。
4.2 进出记录的数据存储:SD卡方案和Flash方案
“全部数据”最直接的落地方案是加一个SD卡模块(SPI模式),把每条记录以文本行追加写入CSV文件。SD卡的好处是数据在电脑上直接能看,对答辩展示很有用。缺点是SD卡模块在3.3V下写文件时电流峰值能到100mA以上,稳压不好的3.3V电源会导致写文件时读卡器同时掉电压,卡写一半就断。我一般会给SD卡模块单独用一个LDO,和STM32、RC522分开供电。
用FATFS文件系统时,注意长文件名支持是默认关闭的。项目源码里如果直接用f_open(&file, "record.csv", FA_OPEN_APPEND | FA_WRITE),文件名超过8.3格式就会失败。追加一行记录的正确写法是:
FIL file; FRESULT res; // 注意宏定义:_USE_LFN要设置为2,且提供了exFAT支持 res = f_open(&file, "0:/parking_log.csv", FA_OPEN_APPEND | FA_WRITE); if (res == FR_OK) { char line[64]; // 卡号为16进制,时长用秒,采用CSV便于Excel直接导入 sprintf(line, "%02X%02X%02X%02X,%lu,%u\r\n", cardId[0], cardId[1], cardId[2], cardId[3], durationSec, fee); f_write(&file, line, strlen(line), &bw); f_close(&file); }FPGA的系统如果没有SD卡模块,那就用STM32内部Flash。但F103C8T6的Flash只有64KB,每页1KB,你要自己管理磨损均衡。最简单的方案是“存储槽位”:每次写一条新的进出记录,写进下一个空闲页,写满128页后按顺序循环覆盖最老的页。这里不推荐直接擦写固定地址,因为停车场记录一天几十条,很快就磨损到固定页。
4.3 参数设置:扣费标准、月卡有效期、时长的获取
停车扣费通常有两种方式:按时长计费或按次计费。课设项目里建议用按时长计费,因为这样能展示RTC(实时时钟)的使用。但注意,F103C8T6没有内部RTC电池,断电后RTC时间会丢失,要用外部DS1302或DS3231模块,或者在开机时手动校时。如果你只是模拟,可以用SysTick做“本次上电后时长”计算,定义entryTime为系统上电以来的秒数即可。扣费建议用三档标准:0到30分钟免费,30到120分钟收2元,超过120分钟每小时加1元。这样SPA给评委展示了逻辑判断,又不会让代码复杂到不可维护。
5. 避坑手册:读不出卡、读到重复卡、LCD花屏、掉电丢数据
5.1 读不到卡,排查顺序不能乱
现象:上电后LCD显示“请刷卡”,任何卡片靠近都没反应,MFRC522库的状态码一直返回MI_ERR。
原因:第一步先量RC522模块的VCC和GND是不是3.3V,很多劣质杜邦线接触电阻大,正常空载3.3V,一接模块掉到2.8V,RFID模拟前端直接罢工。第二步是确认SPI接线——MISO和MOSI接反是最常见的翻车现场,表面上看程序没跑飞,但寻卡命令回包数据全是0xFF或0x00。第三步是检查复位引脚,RC522模块的RST脚必须由MCU拉高一次并保持,有些库代码只在初始化时输出一次高电平,如果这个模块的复位电路有RC延迟,会比SPI初始化晚几百毫秒,第一次寻卡必然失败。
解决:在初始化末尾加一个HAL_GPIO_WritePin(RST_Pin, GPIO_PIN_SET); HAL_Delay(10);然后立刻做一次软复位(发送PCD_RESETPHASE命令),多试两次再报“未找到卡”。
5.2 同一张卡被连续触发好几次
现象:刷卡入场后道闸打开,卡片还没离开,系统又识别到同一卡号,再次执行“入场”逻辑,计数翻倍。
原因:状态机里缺少“处理中”的中间态。停车场的读卡事件是“电平触发”而不是“边沿触发”,卡片在射频场内停留的几百毫秒内,寻卡命令能多次成功。
解决:在寻卡成功后清标志,置一个cardProcessed = 1标志,并记录当前卡ID。在主循环里每次寻卡前先读UID,若UID等于当前处理卡的UID并且距离上次处理时间小于1秒,就跳过逻辑。最简单的实现是加一个冷却时间变量:
if (HAL_GetTick() - g_lastDealTime < 1000) { return; } // 小于1秒的重复卡事件直接忽略5.3 LCD1602花屏或显示乱码
现象:LCD和RC522共用SPI没问题,但LCD显示偶尔有随机字符闪烁,尤其在校时或读SD卡时。
原因:LCD的数据线走线过长,和RC522的SPI时钟线并排,电磁耦合导致LCD数据线被干扰幅值飘移,4位模式下任何一根线状态不对就是乱码。
解决:给LCD所有数据线和控制线各加一个10kΩ上拉到3.3V,并且把LCD_EN的下降沿触发改成“先拉高再延时1us再拉低”。在每次刷屏前做一次软件复位(LCD1602_Cmd(0x01); HAL_Delay(2);),虽然会带来一点点屏幕闪烁,但能显著减少乱码。如果还不行,检查PB2-PB5这些引脚是不是复用到JTAG口了——PB3、PB4默认是JTAG复用,要先把AFIO重映射关掉才能当普通GPIO。
5.4 掉电后卡片余额丢了
现象:程序在Keil里单步调试时一切正常,断电再上电,上次刷卡存储的余额或白名单信息丢失,变成初始值。
原因:用的是RAM数组存车辆信息,掉电即失。F103C8T6的Flash写操作需要按页擦除,不能像EEPROM那样单字节改写,很多人直接用HAL_FLASH_Program逐字节写,结果在Bank1写入时把正在运行的代码段擦掉了,程序直接跑飞。
解决:不要在运行时直接调用HAL_FLASH_Program去写程序Flash。建议把车辆信息结构体放进指定的一个Flash页,例如放在地址0x0803F800(最后一页),将程序代码限制在64KB - 1KB内,通过f_write改造成自定义简易文件系统,或者直接使用W25Q16 SPI Flash芯片做掉电保存。如果你不想改硬件,把Simulation模式关掉,用Debug时使用RAM执行是不可能的——最保险的做法是**接受“重启后车辆信息清空”**这个限制,只把进出记录写到SD卡,SD卡里的CSV就是恢复依据。
5.5 RC522中断和主循环抢占导致的偶发“卡死了”
现象:系统运行几个小时后就卡在读卡循环,LCD不再刷新,重新复位才能恢复。
原因:MFRC522库的中断处理和主循环共用全局数组g_cardId,读卡函数正在进行“防冲突→选卡”期间被定时器中断打断,中断里再次调用读卡函数,形成了对同一全局变量数组的并发写,互相踩了彼此的缓冲区。
解决:在Anticoll和Select之间禁止中断(__disable_irq();),完成后立刻打开(__enable_irq();)。如果还担心,把g_cardId定义为函数内局部变量,返回时拷贝到全局区,彻底避免临界区竞态。
6. 把“全部数据”变成加分项:卡号白名单的增量写入与磨损均衡
当你的停车场项目能稳定读卡之后,最值得投入的进阶改进不是换更快的芯片,而是完善“数据”部分——因为课设高分项往往不是硬件多精美,而是“数据如何被有意义地保存和展示”。
一个我强烈建议实现的技巧:把M1卡的扇区1块0做成“最近N次进出记录”的环形缓冲,而不是每次都擦除重写。每次车辆出场时,先读取块0的第15字节(记录索引),加1后对16取模,写入当前时间戳到该索引对应的字节位置。M1卡单个扇区擦写寿命是10万次,如果每天进出200次,理论上能用一年多,完全覆盖课设的演示周期。这比每次重写整个块要可靠得多,因为Flash擦写是按整页最小单位的,M1卡虽然允许单字节写,但访问位控制不允许块内任意地址随意写——你得把完整16字节读出来、改掉目标字节、再整块写回。
另一个验证方法是做“掉电再读”测试:写卡后立刻断电,30秒后重新上电读卡,看数据是否还在。很多项目在调试器里表现正常,脱机运行就丢数据,原因其实是板子上的3.3V大电解电容在断电瞬间维持了MCU运行几十毫秒,但代码没保存到Flash就被杀掉了。测这个的时候要在读卡写卡完成后加一条while (1);故意阻塞程序,看数据是否真的持久化。
还有一点容易被忽略:SD卡里的记录文件换到电脑上打开Excel中文乱码,是因为f_write写的是UTF-8字符串,而Excel默认按ANSI(GBK)解析。要么记录里只写ASCII字符(卡号、时长、金额),要么在写文件前用stm32 gbk转utf8这种经典代码转码,我一般图省事直接传ASCII码“00FFA1B1,120,3”,答辩时再解释编码差异。
最后说一句血泪经验:别在答辩前夜去调LCD的汉字字库,风险极高且收益极低,最多做个“欢迎光临”的滚动ASCII效果就够了。把时间花在读卡稳定性、数据落盘和恢复测试上,评委问到的概率最大、拿到高分的概率也最大。这套项目做到“5分钟内完成入库、掉电数据可恢复、SD卡日志能用Excel打开”就是成品状态,剩下的都是给真正做产品的人留的余地。希望帮到你。
本文还有配套的精品资源,点击获取