
简介这套基于STM32的智能停车场车位检测系统源码与实现方案定位为嵌入式、物联网与自动化控制方向的课设/毕设参考项目适合有编程基础的开发者二次开发。压缩包共1027个文件、约32.34MB以C/H源码、备份文件、编译产物及工程配置文件为主也包含原理图/PCB预览、STM32CubeMX配置及HMI界面文件主控程序、外设驱动与展示模块结构清晰便于按模块阅读和改造。已有53人学习浏览属于小型精选案例。整套资料不仅给出传感器采集、通信传输与状态显示模块的协作框架还带有硬件设计文档和实施指导手册结合Keil工程文件可快速恢复开发环境无论用于毕业设计、课程实践还是工程实训都能直接参考主控逻辑、硬件连接与调试思路覆盖从硬件到软件的关键实现路径并可按需裁剪功能。1. 智能停车场车位检测不是传感器选型难而是状态机难写做毕设或课设选“智能停车场车位检测”这个题目的人十有八九一开始都会以为难点在硬件——超声波模块买哪个、STM32用哪款、显示屏怎么接。等真正把板子焊好、代码烧进去才发现真正折磨人的是另一件事怎么可靠地判断“这个车位现在有没有车”。超声波传感器测距本身太成熟了数据手册一翻就有例程但停车场场景里车的形状、停歪了、相邻车位干扰、雨天误触发任何一个因素都能让你的状态判断逻辑崩掉。这套源码包的价值不在于把HC-SR04的例程抄一遍而在于给了完整的“检测→滤波→状态判定→上报”链路直接把毕设最耗时间的部分做完了。适合正在做STM32课程设计或毕业设计、想快速跑通一个完整物联网项目的学生也适合想抄一套稳定判定逻辑的嵌入式入门开发者。2. 车位检测方案选型为什么是超声波STM32而不是摄像头或地磁2.1 三种主流检测方案的对比与选型理由停车场车位检测在工业界常见做法无非三种地磁感应、摄像头视觉、超声波测距。地磁靠地球磁场扰动判断安装时要在地面开槽毕设场景里基本没有施工条件而且金属车身对磁场的扰动和环境里钢筋的干扰很难区分误报率不低。摄像头视觉最“高级”但OpenCV跑在PC上不算嵌入式跑在树莓派或K210上又涉及模型训练对课设来说工作量直接爆炸。超声波是性价比最稳的选择一个HC-SR04或HY-SRF05模块几块钱STM32的GPIO加定时器就能驱动距离数据直观调试时拿个尺子就能验证。这套源码采用的方案是一车位一超声波、STM32F103系列主控通过定时器输入捕获模式读取回波脉宽。选择F103而不是F407或H7的理由很实际——F103的例程资源最多HAL库和标准库的坑网上都有现成答案而且车位检测这个任务根本用不到高主频72MHz绰绰有余。超声波模块用TRIG引脚发10us以上高电平触发ECHO引脚返回高电平脉宽脉宽时间乘以声速除以二就是距离这一整套逻辑不算难但源码里值得看的是它对“脉宽超时”和“首波误触发”的处理——这是很多自己写的例程忽略掉的后续章节会展开。2.2 硬件连接与GPIO分配方案源码包里的接线方式我直接说结果HC-SR04的VCC接5V注意不是3.3V很多模块在3.3V下测距会明显偏短GND共地TRIG接STM32的PA8ECHO接PA6定时器TIM3的通道1另外还有一个OLEDI2C接口接PB8和PB9用来现场显示车位状态。如果你用的是HY-SRF05或US-015引脚定义完全兼容不需要改代码。下面是引脚分配表照着接就行模块引脚STM32引脚说明HC-SR04VCC5V必须5V供电HC-SR04GNDGND与STM32共地HC-SR04TRIGPA8普通GPIO输出HC-SR04ECHOPA6必须接定时器捕获通道OLEDSCLPB8I2C时钟OLEDSDAPB9I2C数据ECHO引脚必须接到定时器的输入捕获通道上原因在于测距精度取决于脉宽测量的分辨率。如果用GPIO中断加HAL_GetTick()来计时毫秒级分辨率根本不够——1毫秒的误差换算成距离就是17厘米停车位里一个17厘米的抖动足以让状态判断出错。定时器捕获的时钟是72MHz或经过预分频单次计数就是微秒级甚至更高精度实测重复精度能到±1厘米以内。2.3 一个车位需要探头方向朝下还是朝前另一个容易被忽视的点是探头的安装朝向。这套方案的代码逻辑默认探头安装在车位正前上方向下前方倾斜约30到45度角测量的是车顶或车前挡风玻璃的距离。车位空时地面回波距离固定一个值比如2米车停进来后回波距离骤减到0.5米左右差值明显状态判定阈值好设。但如果你是侧面安装探头对着车位中间平射那测的就是车的侧面空车位时探头对着对面墙距离可能5米以上车进来后距离变化同样明显。两种安装方式代码都能跑差异在全阈值设置上源码里的初始值是按前上方安装标定的侧面安装需要重新采样空车位的基准距离。这也是我想强调的这套源码不是烧录就能跑完事的安装方式决定了阈值参数后面章节会讲重新标定的具体步骤。2.4 源码包结构说明与关键文件定位拿到源码包后先别急着编译建议花五分钟过一遍文件结构。源码用的是标准库不是HAL库工程基于Keil MDK5核心文件在USER目录下的main.c、USERT目录下的oled.c和hcsr04.c。头文件里config.h集中定义所有阈值和引脚映射参数改参数只动这个文件就行。我之前接过不少毕设优化的活见过太多学生把参数散落在各个.c文件里改个阈值要全局搜索半天这个源码把配置集中起来的做法很舒服直接改宏定义即可。源码的采集逻辑放在定时器中断里每100ms触发一次TRIG脉冲读回ECHO脉宽后做中值滤波连续五次测量去掉最大值和最小值再取平均。这样做的好处是能滤掉偶发的噪声尖峰——比如旁边车道有车经过时超声波打到侧面的不规则反射单次测量可能出现很大的跳变但中值滤波后这个跳变就被剔除了。状态机则是另外一套逻辑它不直接用瞬时距离判断而是要求连续N次测量结果稳定超过阈值才切换状态这个N被定义为3到5次可配置在config.h里作用是防止车辆缓慢移动时反复触发空/占切换。/* main.c - 核心采集与状态判定循环 */ #define SAMPLE_TIMES 5 // 每轮采样次数 #define STATUS_THRESHOLD 3 // 连续N次超阈值才切换状态 uint8_t check_car_status(float distance) { static uint8_t stable_count 0; static uint8_t current_status 0; // 0空闲, 1占用 if (distance DISTANCE_OCCUPIED) { // 小于占用阈值 if (stable_count STATUS_THRESHOLD) { current_status 1; } } else { if (stable_count STATUS_THRESHOLD) { current_status 0; } } return current_status; }上述代码的逻辑是做一个简易滞回状态机stable_count记录了连续达到阈值条件的次数只有连续3次满足才翻转状态。这能避免那种“车还没停稳、超声波读数来回跳”导致的反复报警。参数说明DISTANCE_OCCUPIED在config.h里默认定义为80单位是厘米表示距离小于80cm判定为有车STATUS_THRESHOLD的3次是经验值不够灵敏就改成2太灵敏就改成5但改动后要重新做稳定性测试后面避坑章节会细说。3. 超声波测距实战定时器捕获与距离换算的细节3.1 为什么不能用延时读电平必须用定时器输入捕获很多从51单片机转过来的同学有个惯性思维TRIG发完脉冲后用while循环等ECHO引脚变高再开启一个计时循环等它变低两个时间戳相减就是脉宽。这个做法在STM32上问题很大——while循环的功耗和不确定延时先不谈关键是计时精度取决于CPU执行循环的速度CPU在跑其他中断时会直接拉长计时循环测出来的脉宽就偏大。这套源码用的是定时器输入捕获模式CH1引脚PA6配置为上升沿和下降沿都捕获每次捕获自动记录计数器值两次捕获值之差就是脉宽整个过程由硬件完成不占CPU精度由定时器时钟决定。下面这段是源码里TIM3初始化和捕获中断的核心代码我把关键注释写在里边/* hcsr04.c - TIM3输入捕获初始化与中断处理 */ void TIM3_Cap_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 72MHz时钟预分频72计数频率1MHz每微秒计1个数 TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 通道1配置为上升沿捕获不滤波硬件无机械抖动 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x00; TIM_ICInit(TIM3, TIM_ICInitStructure); // 同时使能上升沿和下降沿中断用于记录两个时间戳 TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM3, ENABLE); } uint32_t rising_time 0; uint32_t falling_time 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { if (TIM_GetCapture1(TIM3) 0) { // 判断是否需要切换边沿 // 读取上升沿时刻 rising_time TIM_GetCapture1(TIM3); // 切换为下降沿捕获 TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling); } else { falling_time TIM_GetCapture1(TIM3); // 切换回上升沿捕获 TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising); } } }这段代码有一个细节值得注意rising_time和falling_time的读取顺序以及边沿切换的时机。我在第一次自己写这个逻辑时犯过错误——在中断里同时读取两次捕获值结果发现平台计数溢出时差值变成负数后来改成先记录再切换边沿问题就没了。参数说明预分频设为71是因为STM32的系统主频是72MHz72分频后计数频率正好是1MHz每个计数单位对应1微秒脉宽值直接就是微秒数不需要再做换算TIM_Period设为0xFFFF是因为超声波最大测距约4米对应脉宽约23毫秒远小于65535微秒的溢出周期不会出现计数器翻转问题。3.2 距离换算公式与温度补偿脉宽拿到后距离换算本身很简单距离厘米脉宽微秒×0.0343÷2因为声波走了个来回所以要除以2。这个0.0343是标准气温20°C下的声速值约343米/秒。但如果你的毕设在北方冬天室外测试气温零下声速会降到约331米/秒误差能到3.5%左右1米距离偏3.5厘米车位判定阈值如果正好卡在临界就可能误判。源码里把这部分写成了常量定义如果你在极端低温或高温环境用建议按下面的公式修正声速/* hcsr04.c - 声速温度补偿可选功能 */ #define TEMPERATURE 25.0f // 当前环境温度可按季节手动修改 float distance_cm(uint32_t pulse_width_us) { float sound_speed 331.4f 0.607f * TEMPERATURE; // 声速公式 float distance pulse_width_us * sound_speed / 2.0f / 10000.0f; return distance; }声速补偿的公式是331.4 0.607×温度这个经验公式在嵌入式里够用不需要更高阶的湿度修正。参数说明pulse_width_us是定时器捕获到的脉宽值单位微秒sound_speed计算出来单位是米/秒除以10000是为了把微秒换算成秒、把米换算成厘米数学过程不复杂但单位换算容易出错建议每一步都打印出来对一遍我调试时发现源码的注释里单位标得很清楚只要不改动宏不太会踩单位坑。3.3 滤波策略为什么选中值滤波而不是均值滤波车位检测的应用环境里超声波回波偶尔会出现毛刺——可能是相邻车位的超声波串扰也可能是车辆表面不规则反射。均值滤波会把毛刺拉平到结果里让距离值偏离真实值而中值滤波直接从窗口里取中间值只要毛刺占比不超过一半它就会被完全剔除。源码里的做法是连续采集5次排序后取第3个值作为本次有效结果。窗口大小选5而不是3或7我实际对比过3次窗口太小两个连续毛刺就能穿透7次窗口太大响应慢车辆驶入时状态切换有可见延迟。5次是响应速度和抗干扰的折中这话我在调代码时也踩过——一开始用3次调试时发现旁边车道有个大货车经过状态机会抖一下改成5次就稳了。3.4 触发周期与功耗的权衡源码里默认每100ms触发一次测距也就是一秒钟10次。这个频率下CMOS超声波模块的功耗可以忽略但如果你在做电池供电的低功耗项目需要把周期拉长到500ms甚至1秒。需要注意的是触发周期改长后状态机里的STATUS_THRESHOLD也要同步调整——100ms周期时连续3次判定只需300ms如果周期改成1秒连续3次就要3秒才能判定用户体验上会觉得反应慢。量产项目里常见的做法是“快速预判慢速确认”先用50ms周期连续测两次发现距离骤变再切换成500ms周期连续确认三次才翻转状态。源码没做这个优化但它的状态机框架很容易改成这种两段式判定把check_car_status函数拆成两个状态即可。4. 车位状态判定与上报状态机设计、OLED显示与串口协议4.1 车位状态机的设计逻辑先明确一个概念车位检测的状态机一共就两个状态——PARKING_EMPTY和PARKING_OCCUPIED但真正复杂的是这两个状态之间的迁移条件。如果只用一个阈值判断距离小于80cm就判定有车那你会在停车场现场被连续打脸——行人走过、隔壁车倒车入库、甚至树叶飘过都可能导致瞬时距离低于阈值。源码的做法是双条件约束距离低于DISTANCE_OCCUPIED阈值且持续稳定超过STATUS_THRESHOLD次采样才判定为占用。同时还有一个隐藏条件从占用恢复到空闲时阈值用的是另一个值DISTANCE_EMPTY这个值比占用阈值大20cm。/* config.h - 状态判定参数 */ #define DISTANCE_OCCUPIED 80 // 距离小于80cm判定为有车 #define DISTANCE_EMPTY 100 // 距离大于100cm判定为空车位 #define STATUS_THRESHOLD 3 // 连续N次满足才翻转状态 #define SAMPLING_INTERVAL_MS 100 // 采样间隔毫秒两个阈值之间的20cm间隙是滞回区间。假设车位空时地面回波稳定在150cm一辆车开进来后距离变成50cm状态判定为占用随后车开走距离恢复150cm但中间有个过程是距离从50cm变成150cm的渐变比如车身扫过探头时距离可能连续经过60、70、85cm。如果没有滞回区间距离到85cm时还没超过空阈值但已经超过占用阈值状态机就会来回翻转。有了100cm的空阈值要求距离恢复到100cm以上才认为空中间这段85cm就落在“既不判定占用也不判定空闲”的滞回区里状态保持原来的占用不变保证不会抖。这个滞回设计是整套源码里最值得抄的思想直接决定了系统在现场稳不稳。参数说明DISTANCE_OCCUPIED和DISTANCE_EMPTY的差值至少要大于超声波本身的重复精度约±2cm加上车辆表面反射波动约±5cm20cm这个值留足了余量如果你在精细车位比如机械式立体停车库里用可以把两个阈值分别调成60和70。4.2 检查状态翻转后的动作OLED刷新与串口上报当状态机从空闲翻转到占用时需要立即刷新OLED显示并向外发送数据。OLED部分用的是I2C接口的0.96寸屏显示内容分两行第一行显示车位号源码里配了4个车位编号的变量第二行显示当前状态和距离值。串口上报部分用的是USART19600波特率低速率更抗干扰帧格式设计成定长协议帧头0xAA、车位号、状态位0/1、距离值低字节在前、校验和前面所有字节累加和取低八位。这个帧格式虽然简单但足够做多车位扩展——如果一个STM32F103ZET6带多个超声波探头每个探头分配一个车位号上报时在电脑端按帧头解析即可。/* uart_report.c - 车位状态上报帧 */ void report_car_status(uint8_t park_id, uint8_t status, uint16_t distance) { uint8_t frame[6]; frame[0] 0xAA; // 帧头 frame[1] park_id; // 车位号 0-3 frame[2] status; // 0空, 1占用 frame[3] distance 0xFF; // 距离低字节 frame[4] (distance 8) 0xFF; // 距离高字节 frame[5] (frame[0] frame[1] frame[2] frame[3] frame[4]) 0xFF; // 校验和 for (int i 0; i 6; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, frame[i]); } }这段上报代码在我调试时有过教训校验和函数一开始只累加了车位号和状态位忘了加距离字节结果电脑端解析时偶尔校验失败。后来把全部四个数据字节都加进去问题就消失了。参数说明帧长固定为6字节接收端每次读6字节才算一帧如果字节数不对说明产生了粘帧或半包需要做滑动窗口同步——这是串口通信里最常见的问题PC端接收代码里应该有帧头搜索逻辑先找0xAA再收完整6字节。4.3 多车位扩展的实现思路这套源码本身是单车位方案但代码结构上预留了扩展点。park_id是上报函数里的参数意味着多加一个超声波模块就多加一个park_id只要把第二个探头的ECHO引脚接到TIM3的通道2上用通道2的捕获中断单独维护rising_time_2和falling_time_2即可。我见过有人把多个探头接在同一个定时器的不同通道上TIM3有4个通道理论上可以带4个探头但要注意硬件上ECHO引脚不能直接并联——多路复用会互相干扰每个通道必须独立引脚。软件侧的扩展点是把check_car_status函数做成带车位参数的版本内部维护一个长度为4的数组存储每个车位的stable_count和current_status这样一条逻辑管四个车位状态机代码只写一份。4.4 上位机解析与调试工具源码包里附带了一个简单的PC端串口调试工具源码VS2019工程它做的事很简单从串口读取原始字节流解析帧头0xAA后按格式取出车位号和状态更新UI界面上的车位格子颜色绿色空红色占用。界面不是重点重点是这个工具帮我省了大量调试时间——把串口接上电脑屏幕上就能实时看到车位翻转状态配合OLED本地显示两台设备互相对照能快速判断是传感器问题还是代码问题。如果你不想用附带的工具用SSCOM按字符显示也能看到原始帧但解析要自己写。我建议毕设答辩时直接用自带工具演示效果比串口终端好很多因为评委能直观看到“车开走格子变绿”的过程。5. 现场调试与避坑超声波模块的五个常见坑及排查思路5.1 坑一测距值偶尔跳变到满量程现象OLED上距离显示会从正常的150cm突然跳成400cm左右模块最大量程过几秒又恢复。原因超声波首波到达前模块内部还在等待回波如果回波太弱或者被遮挡模块会一直等到超时并输出最大脉宽。这个问题的根源往往不是代码而是探头前方有遮挡物或者探头角度偏离太多导致回波打不到接收头。解决先用硬纸板做一个简单的“瞄准管”——卷一个直径2cm、长10cm的纸筒套在探头上限制超声波发散角再重新测试。如果套上纸筒后数值稳定说明问题在安装角度如果仍然跳变检查探头的VCC是否确实为5V用万用表量供电不足也会导致接收灵敏度下降。5.2 坑二相邻车位互相串扰现象A车位有人停车B车位的距离读数会偶尔往下掉一点虽然不足以触发状态翻转但干扰了B车位的距离显示。原因两个HC-SR04的超声波频率都是40kHz同时工作时回波会互相接收到对方的信号。解决最彻底的办法是让相邻探头错开触发时间即延迟错相触发——B探头的TRIG脉冲在A探头触发后30ms再发等A的回波窗口关闭再触发。源码里因为默认是单探头方案所以没处理这个你扩展成多车位时一定要在触发循环里加延时错相逻辑。5.3 坑三车停进去但状态不翻转距离值只降到90cm现象车明明已经停进去了OLED显示距离还在80-100cm之间徘徊状态一直显示空闲。原因默认占用阈值是80cm但车的造型或者探头安装位置导致回波落点比较远。举个具体场景SUV的前脸是斜的超声波打上去会反射到别处接收探头收到的可能是边缘散射的回波距离比实际车身近得多。解决不要迷信源码里的默认80现场标定方法是——车位空时记录一次基准距离比如150cm车停好后记录一次实际稳定距离比如65cm把占用阈值设为两者中间值偏车端的地方比如15065/2≈107cm再往下调10%到95cm。记住阈值的含义是“距离小于它就算有车”所以应该小于空车基准距离大于实测停车距离的中间值。5.4 坑四状态机翻转慢半拍车停稳两秒才显示占用现象车倒进车位停稳后要等接近2秒OLED才变成“占用”。原因状态机要求连续3次距离低于阈值才翻转而每次采样间隔100ms但如果滤波窗口是5次中值那意味着状态翻转需要最多5次采样填满滤波窗口3次判定算下来大约53×100ms800ms逻辑上应该够了出现2秒延迟说明其中某些采样因为毛刺被中值滤波丢掉了导致stable_count一直在增长但始终达不到3。解决把STATUS_THRESHOLD从3改成2如果还是慢再把采样间隔从100ms改成50ms。但要注意改小阈值会牺牲抗抖动能力改完之后要模拟“行人走过”场景看看状态会不会误翻。5.5 坑五断电重启后第一个距离值是0现象每次上电OLED显示第一秒的距离是0然后才恢复正常读数。原因HC-SR04上电后内部需要一段稳定时间约500ms如果在这个期间发了TRIG脉冲模块不会正常响应定时器捕获超时返回0。解决初始化代码里在TRIG第一次置高之前加一个HAL_Delay(1000)或Delay(1000)等模块完全稳定再开始测距。这部分源码里已经处理了但如果你是自己改过的代码可能会忽略这个——我第一次上手时就是因为先发TRIG再延时导致每次开机都在OLED上飘一个0看着很不专业。6. 现场标定的实操技巧用“三分法”快速调出一套不误判的参数调车位检测系统的阈值最忌讳的就是拿源码里的默认值直接上电然后祈祷它能工作。正确做法是带一张纸和一把卷尺去真实的车位旁边做一组标定记录。第一步安装好探头后测三次空车位距离记下最小值、最大值取中间值作为DISTANCE_OCCUPIED的设置参考第二步找一辆车随便什么型号都行停进车位等30秒让数据稳定测三次占用距离同样取中间值作为DISTANCE_EMPTY的设置参考第三步看这两个值的间距——如果车停进去的距离和空车位的距离差值小于30cm说明探头安装位置不对需要调整角度或高度不要硬调阈值掩盖安装问题。这个方法我叫它“三分法”——三个点记录、三个值调整、三次验证。验证阶段把车开出车位再重新倒进去重复五轮盯着状态日志看有没有漏判和误判。如果五轮下来全部正确再换一辆车比如轿车换SUV测试如果新车型导致距离值差异大SUV和轿车在同一个探头角度下距离可能差20cm以上说明你的安装位置对车型敏感需要调整探头角度让两种车型的距离落在同一判定区间里。把这些标定后的参数写回config.h再重新编译烧录——这个动作我在帮学生调完参数后一定会强调因为很多人现场用串口调试助手直接在RAM里改了寄存器看着正常就交差断电重启后参数全丢回到默认值又变成误报状态。从那以后我每次帮人调超声波项目第一句话就问“你做过现场标定记录没”没有记录就默认参数是没验证过的系统跑飞了也是正常现象。希望我写下来的这一整套从选型到状态机到现场标定的思路能帮到做这个题目的你至少让你在烧录第一次代码之前就知道坑在哪里省掉一部分本来不必要的熬夜调试时间。本文还有配套的精品资源点击获取