FEATURED · 精选文章

基于STM32与NFC的智能停车场系统实战:从硬件选型到软件架构

发布时间 / 2026/9/19 12:38:28
来源 / 创域科博编辑部
栏目 / 资讯中心
基于STM32与NFC的智能停车场系统实战:从硬件选型到软件架构 1. 项目缘起与整体设计思路1.1 为什么选择智能停车场作为嵌入式练手项目智能停车场这个题目在嵌入式圈子里算是被做烂了的经典项目但恰恰因为它经典才特别适合拿来练手。我前后做过三版停车场系统从最早的51单片机到后来的STM32F103再到最近用STM32F407加NFC重新撸了一遍每一版踩的坑都不一样。这个项目最大的价值在于它几乎把嵌入式开发中所有核心模块都串了一遍GPIO输入输出、中断、定时器、RTC实时时钟、SPI/I2C通信、LCD显示驱动、串口调试一个都不少。从需求角度看一个能跑起来的停车场系统要解决的核心问题其实就三件事车辆身份识别、车位状态管理、计费与显示。身份识别用NFC或者RFID车位管理靠红外或者地磁传感器计费和显示则依赖RTC和LCD。你看正好对应了STM32、NFC、LCD、RTC这四个热搜关键词。所以这个项目不是硬凑的而是需求天然就把这些技术点串起来了。适合谁来参考如果你已经点过灯、调过串口想找一个能把多个外设综合起来的项目这个就非常合适。如果你还在纠结“嵌入式学习路线”怎么走我的建议是先把GPIO和中断吃透然后直接上这种综合项目在做的过程中缺什么补什么比按部就班看教程效率高得多。1.2 系统架构的选型考量整个系统我采用的是“主控外设模块”的分层架构。主控选STM32F103C8T6也就是大家常说的“蓝板”或者最小系统板原因很简单便宜、资料多、性能够用。72MHz主频64KB Flash20KB RAM跑这个项目绰绰有余。如果你手头有F407或者F411当然更好但没必要为了这个项目专门换芯片。外设方面NFC读卡模块我用的是MFRC522通过SPI接口和主控通信。这里要说明一下RFID和NFC的区别很多人搞混RFID是一个大范畴工作频率有低频、高频、超高频NFC是在13.56MHz高频RFID基础上发展出来的兼容ISO14443协议通信距离一般在10cm以内。MFRC522支持的就是ISO14443A协议属于NFC范畴。选它是因为模块便宜、SPI接口简单、Arduino和STM32的驱动库都很成熟。LCD我用的是一块1.8寸TFT屏分辨率128x160SPI接口驱动芯片是ST7735。为什么不用OLED因为TFT能显示彩色做停车场界面更直观而且1.8寸这个尺寸刚好能放下车位状态和计费信息。RTC方面STM32F103内部自带RTC但需要外部接32.768kHz晶振。我一开始想省事用内部RC振荡器实测下来走时误差太大一天能差好几分钟后来还是老老实实焊了晶振。提示STM32内部32kHz做RTC这件事网上讨论很多。内部RC振荡器精度大概在±5%左右做闹钟勉强能用做计费系统绝对不行。外部晶振加两个22pF电容成本不到一块钱精度能到±20ppm一个月误差也就几十秒。2. 核心硬件模块的细节解析2.1 STM32最小系统与GPIO分配拿到STM32第一步不是写代码而是把引脚分配表画出来。这个项目用到的引脚不少我整理了一张表你可以直接抄功能模块引脚模式备注MFRC522 SCKPA5复用推挽SPI1时钟MFRC522 MISOPA6浮空输入SPI1主入从出MFRC522 MOSIPA7复用推挽SPI1主出从入MFRC522 NSSPA4推挽输出软件片选MFRC522 RSTPB0推挽输出复位TFT SCKPB13复用推挽SPI2时钟TFT MOSIPB15复用推挽SPI2数据TFT CSPB12推挽输出片选TFT DCPB14推挽输出数据/命令TFT RSTPB1推挽输出复位TFT BLKPB2推挽输出背光控制红外对管1PA0浮空输入入口检测红外对管2PA1浮空输入出口检测蜂鸣器PB3推挽输出提示音RTC晶振PC14/PC15晶振输入32.768kHz这里有几个点要特别注意。第一SPI1和SPI2分开用避免NFC和LCD抢总线。我试过把两个设备挂同一条SPI上结果NFC读卡的时候LCD会闪原因是片选切换时序没处理好。分开用虽然多占两个引脚但省心。第二PA0和PA1做红外输入我配的是浮空输入加外部上拉电阻。红外对管输出是开漏的不加上拉读出来一直是低电平。这个坑我踩过当时以为传感器坏了换了三个才发现是上拉的问题。第三PB3和PB4默认是JTAG引脚直接配GPIO会不工作。需要在代码里开启复用功能时钟关闭JTAG保留SWD。具体操作是RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这两行不加PB3输出永远没反应新手很容易卡在这里。2.2 NFC读卡模块的驱动要点MFRC522的驱动网上有很多版本但质量参差不齐。我建议直接用官方提供的驱动库然后自己封装一层。核心操作就三个寻卡、防冲突、读卡号。寻卡用PICC_RequestA返回ATQA值。防冲突用PICC_Anticoll拿到4字节UID。读卡号之后就可以和本地存储的白名单比对判断是月卡用户还是临时车。这里有个细节MFRC522的天线匹配电路很关键。模块上一般已经有匹配电容和电感但如果你自己画板子天线线圈的电感值要控制在1uH左右匹配电容要调到能谐振在13.56MHz。我第一版自己画的天线读卡距离只有1cm后来用示波器看波形发现谐振频率偏了换了电容才调到5cm。注意NFC中继攻击这个话题最近很热简单说就是攻击者用两个NFC设备把信号延长实现远距离读卡。对于停车场系统防范方法是在读卡时校验距离或者加时间戳。不过对于练手项目这个层面暂时不用考虑知道有这回事就行。2.3 LCD显示驱动的实现1.8寸TFT屏用ST7735驱动SPI接口。初始化序列比较长但都是固定的直接抄数据手册就行。关键是坐标系设置和颜色格式。ST7735默认是RGB565格式一个像素两个字节。显示中文需要取模我一般用PCtoLCD2002这个软件设置成阴码、逐行式、十六进制。取出来的数组直接放到代码里用LCD_ShowChinese函数逐点写入。显示刷新率是个问题。128x160全屏刷新一次要写40960字节SPI跑到18MHz也要20多毫秒。如果每帧都全刷界面会闪。我的做法是局部刷新只更新变化的区域。比如车位状态从“空”变“满”只重绘那一小块。背光控制用PWMPB2配成定时器通道。亮度调节范围0到100晚上调低一点不刺眼。这里注意背光LED的限流电阻要算好一般屏的背光电流在20mA左右电压3.0V串一个100欧姆电阻差不多。2.4 RTC实时时钟的硬件与软件配置RTC这块我单独拿出来说因为坑最多。STM32F103的RTC是一个独立的BCD计数器走时靠外部32.768kHz晶振。硬件上晶振要尽量靠近芯片负载电容选6pF到12pF之间具体看晶振规格书。我用的晶振负载电容是12.5pF配两个22pF电容实测走时准确。软件配置步骤开启PWR和BKP时钟使能后备寄存器访问复位备份域开启LSE等待LSE稳定选择RTC时钟源为LSE使能RTC时钟设置预分频值预分频值怎么算LSE是32768Hz要得到1Hz的秒信号预分频值设为32767。代码里写RTC_SetPrescaler(32767)。RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); BKP_DeInit(); RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_WaitForLastTask(); RTC_SetPrescaler(32767); RTC_WaitForLastTask();设置时间用RTC_SetCounter读时间用RTC_GetCounter。注意RTC计数器是秒数要转换成日期需要自己写转换函数。我一般用Unix时间戳的方式从1970年1月1日开始算转换起来方便。提示RTC硬件连接上VBAT引脚一定要接纽扣电池不然断电时间就丢了。我见过有人VBAT悬空结果每次重启时间都归零还以为是代码问题。3. 软件架构与核心逻辑实现3.1 主程序流程与状态机设计整个系统我用一个状态机来管理状态定义如下IDLE空闲等待车辆ENTRY_CHECK入口检测到车辆等待刷卡ENTRY_OPEN刷卡成功道闸打开PARKED车辆已入场车位减一EXIT_CHECK出口检测到车辆等待刷卡EXIT_OPEN刷卡成功计费完成道闸打开状态切换靠红外对管和NFC事件驱动。主循环里轮询红外状态NFC用外部中断触发。这里注意NFC中断里不要做太多事只置一个标志位具体处理放到主循环。void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { g_nfc_flag 1; EXTI_ClearITPendingBit(EXTI_Line0); } }主循环里判断g_nfc_flag然后调用读卡函数。这样中断响应快又不会阻塞其他任务。3.2 计费逻辑与时间计算计费规则我设的是首小时5元之后每小时3元不足一小时按一小时算。月卡用户免费。实现上入场时记录时间戳entry_time出场时用当前时间戳减去入场时间戳得到停车秒数。然后换算成小时向上取整。uint32_t duration current_time - entry_time; uint32_t hours (duration 3599) / 3600; uint32_t fee 0; if (hours 0) { fee 5 (hours - 1) * 3; }这里(duration 3599) / 3600就是向上取整的写法比用浮点数再ceil快得多也避免浮点误差。月卡判断靠UID比对。我把月卡UID存在一个数组里读卡后遍历比对。数组大小根据实际月卡数量定我一般留20个位置。3.3 LCD界面布局与刷新策略界面分三个区域顶部显示当前时间和日期中间显示车位状态底部显示最近一次刷卡信息。顶部时间每秒刷新一次只更新数字区域。中间车位状态用图标加数字空位显示绿色满位显示红色。底部刷卡信息在每次刷卡后更新。刷新策略上我定义了一个dirty_flag哪个区域需要更新就置对应位主循环里检查标志位再刷新。这样避免无谓的SPI传输界面流畅很多。#define DIRTY_TIME 0x01 #define DIRTY_SLOT 0x02 #define DIRTY_CARD 0x04 if (dirty_flag DIRTY_TIME) { LCD_ShowTime(current_time); dirty_flag ~DIRTY_TIME; }3.4 数据存储与掉电保护车位数量和月卡UID需要掉电保存。STM32F103没有内置EEPROM我用的是AT24C02I2C接口2Kbit容量存这些数据足够了。I2C驱动用软件模拟因为硬件I2C在F103上有些已知问题调试起来麻烦。软件模拟虽然慢一点但稳定可控。SCL和SDA分别用PB6和PB7加上拉电阻。存储结构体typedef struct { uint8_t total_slots; uint8_t used_slots; uint8_t month_card_count; uint8_t month_card_uid[20][4]; } ParkingData;每次车位变化或者月卡增减都写一次EEPROM。AT24C02写周期大概5ms不要频繁写不然寿命会受影响。我一般加个延时或者用页写模式一次写多个字节。4. 实操过程与关键环节记录4.1 开发环境搭建与工程配置开发环境我用的是Keil MDK5配合STM32CubeMX生成初始化代码。CubeMX的好处是引脚配置和时钟树可视化省去查手册的时间。但要注意CubeMX生成的代码和标准库混用会冲突要么全用HAL库要么全用标准库。我习惯用标准库所以CubeMX只用来参考时钟配置代码还是自己写。如果你用VS Code开发嵌入式需要装Cortex-Debug插件和OpenOCD配置launch.json和tasks.json。这套流程配置一次要花点时间但配好之后写代码体验比Keil好很多尤其是代码补全和Git集成。Keil5兼容C51和STM32安装这个事简单说就是装两个包一个是MDK核心一个是器件支持包。C51和ARM的编译器是分开的装在一起不冲突但要注意License管理。4.2 分模块调试与联调调试顺序很重要不要一上来就全接上。我的顺序是先调GPIO点灯确认最小系统正常调串口打印调试信息调SPI先驱动LCD显示纯色调NFC串口打印卡号调RTC串口打印时间调I2C读写EEPROM最后全部联调每一步都单独验证出问题容易定位。我见过有人全接上之后不工作查了两天发现是某个模块的电源没接。LCD调试的时候先显示红绿蓝三色确认SPI通信正常。然后显示字符确认字库没问题。最后显示中文确认取模正确。NFC调试用串口打印UID拿手机NFC或者门禁卡去刷看能不能读到。读不到先查天线再查SPI通信最后查驱动。4.3 道闸控制与电机驱动道闸我用的是SG90舵机PWM控制角度0到90度对应关到开。PWM频率50Hz周期20ms脉宽0.5ms到2.5ms对应0到180度。定时器配置72MHz主频预分频72-1得到1MHz计数频率。自动重装值20000-1得到20ms周期。比较值500到2500对应0.5ms到2.5ms。TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_InitStruct.TIM_Period 20000 - 1; TIM_InitStruct.TIM_Prescaler 72 - 1; TIM_InitStruct.TIM_ClockDivision 0; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_InitStruct); TIM_OCInitTypeDef OC_InitStruct; OC_InitStruct.TIM_OCMode TIM_OCMode_PWM1; OC_InitStruct.TIM_OutputState TIM_OutputState_Enable; OC_InitStruct.TIM_Pulse 500; OC_InitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC2Init(TIM3, OC_InitStruct); TIM_Cmd(TIM3, ENABLE);舵机供电要单独给不要从STM32的3.3V取电电流不够会导致复位。我用的是5V 2A的电源适配器共地就行。4.4 系统联调与性能优化全部模块调通之后联调阶段主要解决时序冲突和资源竞争。比如NFC读卡的时候LCD刷新会卡顿原因是SPI总线被占用。解决办法是把NFC读卡和LCD刷新分时处理读卡时暂停刷新。还有一个问题是红外对管的抖动。车辆经过时红外输出会有毛刺导致状态机误触发。我在软件里加了20ms的消抖连续检测到低电平20ms才认为是有效信号。if (IR_Read() 0) { debounce_count; if (debounce_count 20) { car_detected 1; debounce_count 0; } } else { debounce_count 0; car_detected 0; }主循环周期大概1ms20次就是20ms消抖效果很好。5. 常见问题与排查技巧实录5.1 硬件类问题速查现象可能原因排查方法LCD白屏背光没开、复位时序不对、SPI没通先测背光电压再查复位引脚波形NFC读不到卡天线匹配不对、SPI片选没拉低、供电不足示波器看天线波形万用表测3.3VRTC走时不准晶振负载电容不对、LSE没起振示波器测PC14波形调整电容舵机抖动电源电流不够、PWM频率不对单独供电示波器测PWM红外误触发没有上拉、没有消抖加上拉电阻软件消抖串口乱码波特率不对、时钟配置错误确认系统时钟和波特率5.2 软件类问题排查思路软件问题最难查的是“没反应”。我的排查顺序是先确认时钟开了没再确认引脚配了没然后确认外设使能了没最后确认中断开了没。比如SPI不工作先查RCC里SPI时钟开了没再查GPIO复用配置对不对然后查SPI使能位最后查片选和时序。还有一个常见问题是中断优先级冲突。NFC中断和串口中断如果优先级配得不对会导致NFC响应慢。我一般把NFC中断设成最高优先级串口次之定时器最低。5.3 独家避坑经验第一个坑STM32F103的SPI1和SPI2时钟源不同SPI1在APB2SPI2在APB1。APB1最高36MHzAPB2最高72MHz。配置分频的时候要注意不然SPI速率不对。第二个坑TFT屏的DC引脚数据/命令选择在初始化之前就要配好不然第一条命令就发不出去。我一开始把DC配在初始化之后结果屏一直不亮。第三个坑MFRC522的复位引脚要拉低再拉高延时至少100ms。我试过延时10ms模块偶尔不启动。第四个坑AT24C02的写周期是5ms连续写要加延时不然数据会丢。我一开始没加延时存的车位数量偶尔会变成255。第五个坑RTC初始化之前一定要先PWR_BackupAccessCmd(ENABLE)不然写不进去。这个在参考手册里有写但很容易忽略。5.4 嵌入式面试常见问题关联这个项目做下来面试里常问的几个点都能覆盖GPIO的几种模式区别推挽、开漏、复用、浮空中断的响应流程和优先级配置SPI和I2C的时序区别RTC的时钟源选择和预分频计算状态机的设计思路掉电保护的数据存储方案如果你正在准备嵌入式面试把这个项目吃透八股文里的很多问题都能结合实际讲比死记硬背强得多。6. 项目扩展与进阶方向6.1 从单机到联网现在这个系统是单机版数据存在本地EEPROM。如果要联网可以加一个ESP8266或者ESP32模块通过串口AT指令连WiFi把车位数据上传到服务器。这样手机就能远程查看车位状态也能实现无感支付。联网之后要注意数据安全UID传输要加密不然容易被复制。简单的做法是用AES加密UID服务器端解密比对。6.2 车牌识别替代NFCNFC需要车主摇窗刷卡体验一般。进阶方案是用摄像头加车牌识别OpenMV或者K210都能做。识别到车牌后查数据库自动放行。这个方案成本高一些但体验好很多。6.3 多车位管理与引导现在只做了一个入口一个出口实际停车场有多个车位。扩展方向是加车位引导屏每个车位上方装超声波或者地磁传感器检测到有车就亮红灯空位亮绿灯。引导屏显示剩余车位数量和方向。这个扩展需要用到CAN总线或者RS485组网多个节点通信。STM32的CAN外设正好派上用场。6.4 低功耗设计如果停车场系统用电池供电低功耗就很重要。STM32F103有睡眠、停止、待机三种低功耗模式。待机模式功耗最低大概2uA但唤醒后要重新初始化。停止模式功耗大概20uARAM保持唤醒快。低功耗设计的关键是外设管理不用的时候关掉时钟。NFC和LCD都是耗电大户空闲时断电有车靠近再唤醒。可以用红外对管做唤醒源配置成外部中断唤醒。我在实际做低功耗版本的时候发现LCD背光是最耗电的干脆加了个光敏电阻白天亮度高晚上亮度低省电又护眼。这个思路你可以参考根据实际场景灵活调整。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻