
简介面向毕业设计与课程作业场景的STM32 RFID停车场系统资源包提供了从源码到工程配置的完整参考实现。压缩包共240个文件以C源文件.c、头文件.h、Keil工程文件.uvprojx及编译过程文件.o/.d/.crf为主另含可烧录的.hex固件与调试辅助文件整体约8.33MB便于直接打开工程研读或烧录验证。该项目将STM32微控制器与RFID射频识别技术结合覆盖RFID卡身份识别、进出控制、计时计费及状态显示等典型停车场管理需求相关代码注释与工程组织可作为嵌入式课程设计和毕业设计的参考模板适合电子、计算机类学生及嵌入式初学者学习。目前已有239人学习下载可作为选题方向与实现思路的参照。1. 基于STM32的RFID停车场毕设源码里最值得拆的三套外设一个能答辩、能演示的RFID停车场demo难点从来不在“读卡”这一件事上而在于MCU要同时伺候MFRC522的SPI时序、闸机舵机的PWM翻转、LCD显示和串口日志还要在掉电时把卡号白名单写进内部Flash。这套基于STM32F103加标准外设库的毕设源码把刷卡、校验、开闸、过车、关闸、计费串成一个完整闭环状态逻辑在main循环里能一次看完。适合两类人一类是毕业设计或课程作业选了STM32方向、想把RFID做扎实的同学另一类是写惯了HAL库、想回头看看标准外设库怎么组织外设与中断的一线工程师。下文按硬件链路、外设配置、状态逻辑、排错验证四层拆解代码均为可复现级别。2. STM32与RC522射频链路SPI时序、防冲突与卡片读写2.1 为什么选RC522而不是125kHz方案13.56MHz的MFRC522几乎是国内STM32课程设计的默认选择模块十几块钱支持ISO/IEC 14443A能把寻卡、防冲突、选卡、密码认证、读写扇区这一整套协议走完。相比之下125kHz的EM4100只能读出一串固定UID无法演示写卡、改余额这类交互答辩时只能讲“识别到了”功能单薄。RC522对外提供SPI、I2C、UART三种接口STM32这边一般接SPI1数据手册标称SPI时钟最高10MHz但杜邦线一长、板子布局一乱实际跑2.25MHz比什么都稳。MIFARE Classic 1K卡共16个扇区每扇区4块、每块16字节块0被厂商占用能自由写的是后面60个块这份代码里把卡UID当身份标识白名单比对通过就放行只有涉及计费演示时才往扇区里写余额和时间戳。2.2 硬件接线与三个常见坑先给一张用在F103C8T6上的接线表拿RC522模块直接对插就行。RC522引脚STM32F103C8模式SDAPA4推挽输出软件NSSSCKPA5复用推挽 SPI1_SCKMOSIPA7复用推挽 SPI1_MOSIMISOPA6浮空输入 SPI1_MISORSTPA3推挽输出默认拉高IRQ悬空轮询模式不用3.3V / GND3.3V / GND单独一路供电2.2.1 GPIO重映射与JTAG占用表里PA5/PA6/PA7是SPI1的默认引脚不需要重映射。如果你的板子占用了这几个脚想换到PB3/PB4/PB5就得先调GPIO_PinRemapConfig(GPIO_Remap_SPI1, ENABLE)打开重映射同时关掉JTAG——PB3和PB4在F103上默认是JTDO、JNTRST不执行GPIO_Remap_SWJ_JTAGDisable这两个脚一直被调试器逻辑占用SPI波形根本出不来。另外一个高频错误是MISO配成复用推挽必须配浮空输入否则主机会把从机的输出脚强拉读回来的字节全是0xFF。2.2.2 IRQ引脚可以不接模块上的IRQ是MFRC522的中断输出轮询模式下悬空即可。有些教程为了显得“高级”把IRQ接进EXTI中断里再查SPI状态属于为了中断而中断。MFRC522一次寻卡操作只需几毫秒主循环轮询PCD_Request的返回值足够。真正要警惕的是死等卡片没放好时某些读卡函数会卡在等待FIFO标志位上这个用定时器超时兜底第3章会给出写法。2.2.3 供电分开读卡才稳RC522发射瞬间电流有几十毫安波动和舵机共用一个3.3V LDO时舵机一动作电压就往下掉读卡偶发失败。我一般分三路供电MCU一路、RC522一路、舵机走5V。板子上只有一个LDO的话至少给RC522的VCC并一个10uF陶瓷电容并且让读卡动作和舵机动作错开。天线和模块之间的匹配电容是出厂调好的不要动。2.3 寻卡、防冲突与读写块代码SPI初始化和RC522驱动是这套源码的核心先看SPI部分。void RC522_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCKPA5, MOSIPA7 复用推挽 */ GPIO_InitStruct.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); /* MISOPA6 必须浮空输入 */ GPIO_InitStruct.GPIO_Pin GPIO_Pin_6; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStruct); /* NSSPA4 普通推挽软件控制 */ GPIO_InitStruct.GPIO_Pin GPIO_Pin_4; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStruct); SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; SPI_InitStruct.SPI_CPHA SPI_CPHA_FirstEdge; SPI_InitStruct.SPI_NSS SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_32; SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStruct); SPI_Cmd(SPI1, ENABLE); GPIO_SetBits(GPIOA, GPIO_Pin_4); /* NSS 默认拉高 */ }这段代码有几个参数值得盯一下。SPI_NSS_Soft配合软件片选NSS的电平变化完全由PA4的GPIO控制排错时能直接量到预分频32表示72MHz主频除以32得到2.25MHz的SPI时钟比极限10MHz保守但配合长杜邦线更可靠。CPOLLow、CPHAFirstEdge组合起来是SPI Mode 0MFRC522的时序要求就是Mode 0改错任意一个沿读回的字节顺序和内容都会乱。GPIO初始化时SCK和MOSI必须是复用推挽GPIO_Mode_AF_PPMISO是浮空输入如果照抄HAL里的GPIO_Mode_AF_OD波形也是坏的。读卡函数按ISO 14443A的流程走请求、防冲突、选卡、认证、读写。uint8_t RF_ReadCardUID(uint8_t *uid_out) { uint8_t status; uint8_t type[2]; uint8_t snr[5]; /* 前4字节是UID第5字节是BCC校验 */ status PCD_Request(PICC_REQALL, type); /* 寻卡 */ if (status ! MI_OK) return 0; status PCD_Anticoll(snr); /* 防冲突 */ if (status ! MI_OK) return 0; memcpy(uid_out, snr, 4); /* 只取UID */ status PCD_Select(snr); /* 选卡进入Active */ if (status ! MI_OK) return 0; PCD_Halt(); /* 休眠下次可再唤醒 */ return 1; }PCD_Request的第二个参数接收的是16bit卡片类型常见值有0x4400MIFARE ClassicPCD_Anticoll把5字节写到snr其中前4字节是UID、第5字节是防冲突校验值所以这里用了5字节缓冲再拷贝前4字节直接传4字节数组会越界。PCD_Select让卡片从Ready状态进入Active只有激活后才能做密码认证和读写块。PCD_Halt放在最后是为了把卡打到HALT态不然卡片可能不回应用户下一次的REQA指令——这是新手最容易漏的一步。读写扇区要先认证补一段关键代码uint8_t key[6] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t block[16] {0}; status PCD_Authenticate(PICC_AUTHENT1A, 1, key, g_uid); if (status MI_OK) { status MIFARE_Read(1, block); PCD_StopCrypto1(); /* 认证状态用完必须清掉 */ }这里有一个经典的错位坑MIFARE_Read读某个块时MFRC522会把下一块的数据也顺带装进FIFO函数返回后缓冲区里其实是连续两块的内容。所以读块1没问题读块2、块3拿到的就是从下一块错位过来的数据。密钥方面六个0xFF只对出厂默认的Key A有效如果有人改过扇区密钥PCD_Authenticate返回MI_AUTHENTERR代码里必须做分支处理不能默认成功。2.4 UID编码与白名单查询4字节UID在工程里通常拼成一个32位无符号数既方便比较也方便存Flashuint32_t UID_ToNumber(const uint8_t *uid) { return (uint32_t)uid[0] | ((uint32_t)uid[1] 8) | ((uint32_t)uid[2] 16) | ((uint32_t)uid[3] 24); } int WhiteList_Check(uint32_t card_no) { for (int i 0; i g_white_count; i) { if (g_white_list[i] card_no) return 1; } return 0; }按小端把第0字节放最低位这是MFRC522例程里最常见的写法。白名单数组启动时从Flash读进RAM几百张卡线性查找毫无压力不需要引入哈希。卡片合法性判断不要在等卡状态里直接做切到专门的校验状态处理第4章的状态机会讲。3. 标准外设库工程拆解TIM、USART、Flash在停车场景里的分工3.1 源码包里的 .c 文件对应哪块业务这个工程用的是STM32F10x标准外设库而不是HAL。判断依据很直接源码包里有stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_usart.c这些外设源文件加上uvguix.a5192这类Keil5的窗口布局文件。标准库的每个外设函数和寄存器一一对应写设计文档时对着参考手册讲就行缺点是初始化代码比HAL啰嗦但课程设计规模完全承受得起。文件在这个停车场项目里的职责stm32f10x_tim.c读卡超时、舵机PWM、计费时基stm32f10x_usart.c调试点printf、上位机协议stm32f10x_flash.c白名单卡号掉电保存stm32f10x_rcc.c系统时钟树和总线时钟使能stm32f10x_adc.c电源电压监测、光电传感器模拟量stm32f10x_i2c.cOLED显示与24C02计费记录stm32f10x_can.c多车道组网时下发白名单system_stm32f10x.c启动阶段SystemInit时钟初始化上表基本覆盖了这套源码的多样性它不只是RFID而是把定时器、串口、Flash、ADC、I2C、CAN这些F103的外设都调度起来了。答辩时考官随便点一个外设问“为什么这么配”都能从具体场景回答这是课程作业拿高分的常见套路。3.2 用TIM2做读卡超时而不是Delay读卡和红外触发这类场景最怕死等。见过不少毕设代码在PCD_Request后面跟一个几百毫秒的for循环卡片没放好整个系统就卡住闸机、LCD、串口全部停摆。常见的做法是用TIM2产出一个时基中断主循环检测超时标志void TIM2_Base_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStruct.TIM_Period 5000 - 1; /* 计数到4999 */ TIM_InitStruct.TIM_Prescaler 7200 - 1; /* 72MHz/720010kHz */ TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); NVIC_InitStruct.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); } void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_tick_500ms; if (g_tick_500ms 600) g_card_timeout 1; /* 300秒强制复位 */ } }预分频7200把72MHz降到10kHz计数周期5000更新事件正好500ms一次。中断里只动变量不做SPI、不刷屏避免中断过长反过来破坏RFID时序。抢占优先级2低于复位和错误处理高于普通USART接收这样读卡超时永远能按时被响应。g_tick_500ms这个节拍同时充当计费时钟进出场时间差乘以费率就是金额。3.3 USART重定向printf输出调试日志停车场状态切换、读卡失败、超时复位这类事件用串口日志观察比点灯高效得多。标准外设库下最省事的办法是重定向fputcint fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }两个前提容易漏。第一Keil工程要勾选MicroLIB否则printf走半主机模式运行到第一个printf直接进HardFault第二USART1的GPIO要配好复用推挽PA9TX、PA10RX波特率1152008数据位、1停止位、无校验。日志格式我一般用“状态名:参数”比如RCV_OK:0x12A34B56串口助手里按关键字过滤日志脚本排错效率明显高。3.4 白名单掉电保存Flash写入的三个边界条件stm32f10x_flash.c负责把合法卡号写进内部Flash断电再上电白名单不丢。F103的Flash以页为单位擦除C8T6是64KB每页1KB。写之前三个条件必须满足先FLASH_Unlock解锁否则写操作被直接忽略地址要在页边界对齐写入不能频繁Flash擦写寿命在万次级别。进出场才写一次实际根本碰不到上限。#define CARD_SAVE_BASE 0x0800F000 void Card_Save(uint32_t card_no) { FLASH_Unlock(); FLASH_ErasePage(CARD_SAVE_BASE); FLASH_ProgramWord(CARD_SAVE_BASE, card_no); FLASH_Lock(); }0x0800F000选在C8T6最后一个1KB页离代码区最远正常编译不会踩到。先擦后写这个顺序不能换直接ProgramWord到一个没擦过的页写入结果不可预期。擦完可以用一个读操作确认整页是0xFFFFFFFF再往下走。读卡号比对时从CARD_SAVE_BASE按字读出来转成数组后再进WhiteList_Check。4. 出入口控制与计费状态机、红外对射与LCD联动4.1 六状态进出车状态机停车场逻辑用状态机写最干净。我在这套代码里把系统拆成六个状态空闲、等卡、校验、开闸、过车、关闸。当前状态触发事件动作下一状态空闲入口红外检测到车提示刷卡等卡等卡读到有效卡号查白名单校验等卡读卡超时清状态空闲校验卡合法开闸、记录进场时间开闸校验卡非法蜂鸣器响2次空闲开闸红外检测到车前进开始计时过车过车红外恢复且延时3秒关闸关闸关闸闸机到位复位节拍空闲对应的主循环任务void Parking_Task(void) { switch (g_state) { case SYS_IDLE: if (IR_Entry_Get() IR_BLOCKED) { g_state SYS_WAIT_CARD; } break; case SYS_WAIT_CARD: if (RF_ReadCardUID(g_uid)) { g_card_ok WhiteList_Check(UID_ToNumber(g_uid)); g_state SYS_VERIFY; } else if (g_card_timeout) { g_card_timeout 0; g_state SYS_IDLE; } break; case SYS_VERIFY: if (g_card_ok) { Gate_Open(); rec.tick_in g_tick_500ms; /* 进场时间 */ g_state SYS_OPEN_GATE; } else { Buzzer_Beep(2); g_state SYS_IDLE; } break; case SYS_OPEN_GATE: if (IR_Entry_Get() IR_BLOCKED) { g_state SYS_PASSING; } break; case SYS_PASSING: if ((IR_Entry_Get() IR_FREE) (g_tick_500ms rec.tick_in 6)) { Gate_Close(); g_state SYS_CLOSE_GATE; } break; case SYS_CLOSE_GATE: if (Gate_IsClosed()) g_state SYS_IDLE; break; default: g_state SYS_IDLE; break; } }两处设计值得说。等卡状态只负责“拿到卡号”合法性判断全部丢给校验状态以后加黑名单、挂失、月卡到期都不用动主流程过车状态里“红外恢复且延时3秒”是为了防止车尾没过完就落杆。IR_BLOCKED表示红外被车身遮挡IR_FREE表示通光恢复。状态切换变量g_tick_500ms在中断里自增主循环里只读这个单生产者单消费者模型不需要加锁。4.2 舵机闸机与红外去抖闸机最朴素的实现是一颗SG90舵机加挡板50Hz PWM0.5ms到2.5ms脉宽对应0到180度。舵机挂在TIM3的CH1上预分频72、计数周期20000得到10kHz计数、20ms周期、1us精度的PWM。void Gate_Open(void) { TIM_SetCompare1(TIM3, 1500); } /* 1.5ms 中间位 */ void Gate_Close(void) { TIM_SetCompare1(TIM3, 500); } /* 0.5ms 最小位 */1500对应1.5ms脉宽舵机在中间位置配合挡板安装角度当横杆500对应0.5ms舵机回摆到另一边。不同厂家SG90的零点和行程有偏差数值要按实际舵机标定不要照抄。上电瞬间舵机会猛甩一下初始化时先把PWM输出到关位延时100ms再建主循环能避免每次复位都“打杆”的尴尬。红外对射的沿在车辆经过时会有毛刺我一般要求连续两次读到相同电平才确认状态变化防抖延时用TIM2节拍累加而不是Delay不阻塞其他任务。舵机转动期间恰好去读红外容易误触发和供电地噪声有关调不通就从硬件上先隔离再谈软件滤波。4.3 计费记录、OLED显示与CAN组网stm32f10x_i2c.c在这里通常驱动0.96寸OLED显示“欢迎光临、余位”等提示也可能挂24C02存计费流水。F103硬件I2C的时钟拉伸和总线锁死问题在圈内被吐槽多年低速设备用GPIO模拟I2C更省心标准库下把两个GPIO配成开漏输出加外部上拉按手册写起始、停止、应答时序OLED和EEPROM都能稳定驱动。计费流水写24C02时一条记录包含UID、进场节拍、出场节拍、金额结构体直接按字节写入typedef struct { uint32_t uid; uint32_t tick_in; uint32_t tick_out; uint16_t fee; } ParkRecord; void EEPROM_WriteRecord(ParkRecord *rec) { uint8_t *p (uint8_t *)rec; for (uint8_t i 0; i sizeof(ParkRecord); i) { I2C_Soft_WriteByte(0xA0, RECORD_BASE i, p[i]); delay_us(10); /* 24C02页写间隔至少5ms */ } }24C02按字节写每两次之间要留5ms写周期地址0xA0是器件地址加写位RECORD_BASE建议从0x20开始避开器件内部前32字节容易和A0/A1/A2地址混淆的区域。金额fee按“分钟计费”计算出场时tick_out减去tick_in乘以500ms再除以120就是分钟数。stm32f10x_can.c则是更进阶的玩法多车道停车场用CAN把每台道闸控制器连起来上位机以500kbps广播白名单版本号终端电阻120欧放在总线两端课程设计不接CAN完全不影响演示。5. 移植到自己的板子Keil5芯片包、烧录验证与三条排错5.1 Keil5芯片包与工程配置源码是Keil5的uvprojx工程直接双击工程文件前先装STM32F1系列的芯片包Keil.STM32F1xx_DFP。打开后在Options for Target里确认Device是STM32F103C8C/C选项卡的Define写STM32F10X_MD和USE_STDPERIPH_DRIVER。前者让中等容量芯片的内存映射生效后者让标准外设库头文件被正确包含漏一个都会出现几十个未定义的报错。下载器用ST-Link或J-Link接SWDPA13/PA14两根线烧录前点一下Erase全片能清掉历史工程留在Flash里的数据。5.2 三个高频故障的定位顺序现象根因排查手段下载报No Target ConnectedBOOT0电平、SWD线序、芯片被锁BOOT0拉高进ISP模式能连说明芯片没坏烧录成功但程序不跑Define宏和Device不匹配核对STM32F10X_MD与F103C8型号读卡距离不足1cmSPI时钟过高或RC522供电不稳预分频降到64RC522单独供电printf输出乱码晶振频率和RCC配置不一致确认外部8MHz晶振量OSC脚波形以上顺序值得细说。第一类故障里芯片被上一个小工具锁死的本质是Option Bytes被改过用ST-Link Utility的Erase把整个Flash和选项字节擦干净就恢复了不需要换芯片。第三类故障如果换了供电和时钟仍然存在把天线区域3cm内的覆铜和走线避让一下RC522天线对金属敏感。提示判断SPI是否通不读卡直接调用PCD_ReadRegister读MFRC522的版本寄存器回读0x92或0x91表示SPI链路正常回读0x00查接线回读0xFF查MISO配置或片选极性。这把“读卡失败”拆成“链路问题”和“协议问题”两步是最快的定位手段。5.3 交付前的回归验证最后给一个可抄的验证流程上电后连读50次卡每次把成功/失败、4字节UID、耗时用USART1打出来统计成功率。代码里用一个测试宏包住循环正常模式下完全透明。50次里任何一次失败先把SPI时钟降一档再测连续三次失败检查卡片是不是MIFARE Classic而不是NTAG系列——NTAG不支持这套读写流程只能读UID。成功率稳定在95%以上再把测试宏关掉这版固件就可以拿去录演示视频了。测试脚本留在源码里不删答辩时展示50次全过比口头说“测试过”有说服力。本文还有配套的精品资源点击获取