简介:基于STM32单片机的停车场车位占用检测系统完整源码与项目资料包,适合电子、物联网、通信工程、自动化等专业学生用于毕业设计、课程设计或课内实践项目。整包共858个文件,约32.23MB,按功能分成源代码、工程配置、硬件设计与说明文档等模块:C/H源文件提供核心业务逻辑,Keil工程和STM32CubeMX的.ioc配置便于直接编译或二次调整,原理图、PCB及元器件库覆盖硬件参考,另有教程文档辅助理解车位检测流程与STM32外设配置。项目以STM32F1系列为主控,通过传感器采集车位占用状态,经逻辑判断后输出结果,逻辑清晰、模块化程度较高,且有完整调试记录和运行日志可供排错参考。当前已有170人学习下载,整体方案成熟、可直接用于答辩演示,也可作为进一步扩展智能停车功能的基础平台。
1. 从“摄像头盲区”说起:为什么要自己做车位占用检测
地下车库的摄像头方案在光线良好的条件下确实好用,但一旦遇到立柱遮挡、车位转角或者夜间弱光,视频识别的漏检率会明显上升。相比之下,基于 STM32 单片机的车位占用检测系统走的是另一条路:每个车位装一个传感器,单片机直接读取占用状态,再通过串口上报到显示屏或上位机。没有图像处理的算力开销,也没有光线干扰,响应是毫秒级的,成本却能压到几十块钱以内。这套项目源码里既有完整的 STM32 工程,又带 HMI 人机界面工程和教程文档,适合做毕业设计、课程设计,也适合真正想搞懂“传感器 → 单片机 → 串口屏”这条完整链路的开发者。下面从传感器选型开始,逐步拆解这个项目的实现细节。
2. 停车位检测的传感器选型与连接方式
2.1 电平型检测:红外对管与光电开关
最常见也最省钱的做法是红外对管,一只发射管、一只接收管,安装在车位正上方或后方挡轮杆位置。没有车时,接收管能收到地面反射的红外光;有车停入后,车体挡住光路,接收管输出电平翻转。STM32 的 GPIO 直接读取这个电平,就能得到占用状态。源码包里的master工程就是以这种方式为核心的。
接线非常简单,红外对管的 OUT 引脚接 STM32 的 PA0~PA3(具体看你的引脚分配),VCC 接 3.3V 或 5V,GND 共地。这里有一个容易翻车的点:部分红外对管的输出是开漏结构,需要外部上拉电阻,否则 STM32 读到的电平会是浮空的随机值。我一般会在 PCB 上预留 10kΩ 上拉电阻位,调试时如果发现状态跳变,优先补上这个电阻。
2.2 模拟量检测:超声波与地磁方案
如果停车位是室外露天场景,红外方案会受太阳光中的红外成分干扰,这时候可以考虑超声波测距或地磁检测。超声波模块的 TRIG 脚接 STM32 定时器输出引脚,ECHO 脚接输入捕获通道。测距原理是:TRIG 拉高 10μs 以上触发模块发射 40kHz 脉冲,模块检测到回波后把 ECHO 拉高,高电平持续时间乘以声速再除以 2 就是距离。
地磁方案则靠车位下方地磁传感器检测铁磁物质引起的磁场变化,一般用 I2C 接口读取,比如 HMC5883L 或 QMC5883L。STM32 通过 I2C 轮询传感器数据,设置阈值判断磁场畸变,超过阈值认为有车占用。这个方案不怕遮挡,但安装要破坏地面,且对周边钢筋等固定铁磁物比较敏感,需要做基线校准。
| 方案 | 接口类型 | 抗环境干扰能力 | 成本 | 安装难度 |
|---|---|---|---|---|
| 红外对管 | GPIO 电平 | 较差,怕强光与灰尘 | 最低 | 低 |
| 超声波测距 | 定时器输入捕获 | 中等,怕强风噪与倾斜 | 低 | 低 |
| 地磁检测 | I2C | 好,不受光影响 | 中等 | 高,需地面施工 |
2.3 STM32 引脚分配与初始化代码
无论选哪种传感器,初始化代码的结构是通用的。以下是 GPIO 读取红外对管的初始化示例,对应源码包中stm32f1xx_hal_conf.h配置下的 HAL 库工程:
void Sensor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,配合开漏输出的红外对管 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 红外电平变化慢,低速即可 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } uint8_t Sensor_Read_All(void) { uint8_t status = 0; status |= (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) ? 0x01 : 0x00; status |= (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) ? 0x02 : 0x00; status |= (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_RESET) ? 0x04 : 0x00; status |= (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == GPIO_PIN_RESET) ? 0x08 : 0x00; return status; }上面这段代码里,Sensor_Read_All把 PA0~PA3 的电平状态压缩到一个字节的低 4 位,哪一位是 1 就代表对应车位被占用。这样做的目的有两个:一是状态上报时只需要传一个字节,串口帧负载小;二是方便后续扩展——如果车位从 4 个增到 8 个,只需增加一个 GPIO 端口并扩展位掩码。GPIO_SPEED_FREQ_LOW的选择是刻意的,低速模式能减少 GPIO 翻转带来的 EMI 辐射,对红外传感器的慢变信号完全够用。
3. 占用判断核心逻辑与防抖处理
3.1 电平直接判断的隐患
很多初学者拿到源码后第一件事就是把电平读上来直接塞进串口发出去,然后发现显示屏上的车位状态像跑马灯一样乱跳。问题出在哪?红外对管的信号在车辆进出瞬间会被车轮、保险杠反复遮挡和恢复,产生多次电平跳变,也就是硬件抖动。此外,如果供电纹波较大,传感器阈值附近的电平也会出现毛刺。如果 MCU 直接处理原始电平,显示端看到的自然是不断翻转的状态。
源码中master工程的核心算法模块里有一段防抖判断,本质是连续采样加超时复位。我对这段逻辑做了拆解,重新实现如下:
#define SAMPLE_PERIOD_MS 10 // 采样周期,单位毫秒 #define DEBOUNCE_COUNT 5 // 需要连续多少次一致才确认状态变化 #define STUCK_TIMEOUT_MS 60000 // 状态持续超过 60 秒则强制刷新 typedef struct { uint8_t last_level; // 上一次确认后的电平 uint8_t sample_count; // 当前连续一致的计数 uint8_t pending_level; // 暂态电平 uint32_t last_change_tick; // 上次状态变化的时间戳 uint8_t confirmed_status; // 确认后的占用状态 } Sensor_Debounce_t; Sensor_Debounce_t sensor[4]; void Debounce_Update(uint8_t index, uint8_t raw_level) { Sensor_Debounce_t *s = &sensor[index]; if (raw_level == s->pending_level) { s->sample_count++; } else { s->pending_level = raw_level; s->sample_count = 0; return; } if (s->sample_count >= DEBOUNCE_COUNT && s->pending_level != s->last_level) { s->last_level = s->pending_level; s->confirmed_status = s->pending_level; s->last_change_tick = HAL_GetTick(); s->sample_count = 0; } }防抖逻辑的核心是DEBOUNCE_COUNT这个参数,它表示采样连续 5 次一致(10ms 一次,也就是 50ms)才认为电平真正变化。50ms 的确认窗口既能滤掉机械抖动和电气毛刺,又不会让车辆快速通过时的短暂遮挡被漏判。这里的代价是状态变化响应延迟 50ms,在停车场景下完全无感。
3.2 超时刷新机制防止“死状态”
还有一个容易踩的坑:传感器如果被异物永久遮挡,或者红外接收管老化灵敏度下降,电平会卡在“占用”状态不再变化。这时候即使车位已经空了,系统依然显示占用。STUCK_TIMEOUT_MS的作用就是兜底——在某一个车位状态持续 60 秒没有变化时,主动触发一次重新采样并向上位机发送当前状态,这样调度中心能及时发现传感器异常。
这段逻辑我一般在main函数的 while 循环里配合HAL_GetTick()调用,每 10ms 扫描一次四个传感器的状态。要注意HAL_GetTick()默认是 1ms 递增一次,所以时间戳相减直接得到毫秒差值,不需要额外的定时器中断。如果你想把这个系统接到 Modbus 总线上,这种按位压缩的状态字节也能直接映射到 Modbus 的保持寄存器或线圈寄存器里,一个寄存器就能表达 16 个车位。
3.3 现场调试时如何确定防抖参数
不同的停车场环境,防抖参数需要微调。地下车库安静、光线稳定,DEBOUNCE_COUNT设为 3 就够了;露天停车场有风吹树枝晃动遮挡,建议调到 8~10;如果是大货车进出,车身长、遮挡时间长,反而可以把防抖调小一些,否则货车快速驶离时会把最后一个车位遗漏。源码包里的工程默认值是 5,这本质上是在响应速度和误判率之间取了折中。
4. 串口通信协议与 HMI 显示屏对接
4.1 帧格式设计与 CRC 校验
源码包里同时存在master.uvguix和HMI.uvguix两个工程,前者是主控板检测程序,后者是串口屏的界面配置文件。主控板通过 USART1 把车位状态发给串口屏,通信协议就是这套系统能否稳定工作的关键细节。
我看到的帧格式设计很克制,没有用复杂的 JSON,而是用了类似 ModBus 的紧凑二进制帧:
帧头 从机地址 命令字 数据长度 数据区 CRC16 0xAA 0x01 0x10 0x01 0x0F 0xXXXXuint16_t CRC16_Modbus(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; } void Send_Parking_Status(uint8_t slot_count, uint8_t status) { uint8_t frame[8]; frame[0] = 0xAA; // 帧头 frame[1] = 0x01; // 从机地址 frame[2] = 0x10; // 命令字:状态上报 frame[3] = slot_count; // 数据长度,这里表示几个车位 frame[4] = status; // 车位状态位图 uint16_t crc = CRC16_Modbus(frame, 5); // CRC 计算范围是前 5 字节 frame[5] = crc & 0xFF; frame[6] = (crc >> 8) & 0xFF; HAL_UART_Transmit(&huart1, frame, 7, 100); }CRC16_Modbus采用标准的 0xA001 多项式,初始值为 0xFFFF,这是 ModBus 协议中最常用的 CRC16 变体。Send_Parking_Status里需要注意:HAL_UART_Transmit的最后一个参数是超时时间,单位是毫秒,这里给了 100ms——在波特率 9600 下发送 7 个字节大约需要 7.3ms,100ms 的超时足够,不会因为总线阻塞而触发 HAL 库的超时错误码。
4.2 串口屏端的数据接收与解析
HMI 工程对应的串口屏收到这 7 个字节后,需要自己做 CRC 校验才算完整。串口屏的脚本一般支持数组和位运算,判断逻辑很直接:先认帧头 0xAA,再认长度字段,最后校验 CRC16。校验通过后,把frame[4]的每一位拆开,控制对应车位的图标切换颜色——占用亮红灯,空闲亮绿灯。
这里有一个实践中的细节:串口屏串口初始化时,波特率必须与 STM32 保持一致。源码默认使用 9600bps,8 位数据位,1 位停止位,无校验。为什么不选更高的 115200?因为串口屏的 TTL 串口在长走线场景下,9600 的误码率要低得多,尤其是屏和主控板距离超过 50cm 时,高速率会明显增加毛刺导致 CRC 校验失败的帧被丢弃。如果不执着于低波特率,可以把屏和主控板做在同一块 PCB 上、走线控制在 5cm 以内,这时候 115200 完全可用,显示刷新也会更顺滑。
提示:串口屏的 TTL 电平有些是 3.3V,有些是 5V,接 STM32 之前先确认电平兼容性。如果不一致,需要加电平转换芯片(如 MAX232 的 TTL 版或 TXS0108E)。直接硬接轻则通信乱码,重则烧毁 IO 口。
4.3 数据帧丢失后的应对策略
在真实车间里,串口线被压到、接头氧化、现场电焊机干扰,都可能让某几帧数据传不到屏上。如果不做任何处理,屏上的车位状态会一直停在上一次的值,直到下一次状态变化才会有新数据。为避免这个问题,主控侧每 5 秒主动重发一次当前状态,也就是“心跳帧”。这段逻辑在源码中体现在Send_Parking_Status被周期调用的位置,重发机制让屏端的界面状态最大值延迟不超过 5 秒。
如果你把主控数据接到上位机而不是串口屏,同样可以在上位机里做一个超时判断:超过 10 秒没有收到任何有效帧,就提示“通信中断”。这与 STM32 内部的STUCK_TIMEOUT_MS是两层保险,一个防传感器失效,一个防通信链路失效,两者不能互相替代。
5. 上电自检与现场标定的实用技巧
系统装到现场之后,真正决定好不好用的往往是一些不起眼的初始化细节。源码里的master工程在main函数初始化顺序上把传感器自检放在了串口初始化之后、主循环之前,我建议你也保留这个顺序。
自检的核心是查询每个车位传感器的初始状态:红外对管在无车时应该输出空闲电平,如果上电后某个通道直接报占用,说明传感器被异物遮住或者红外管安装角度错了。这时候可以逐个通道看一下HAL_GPIO_ReadPin的返回值,在调试串口打印sensor[x] init abnormal,把所有异常通道一次性列出来,方便现场施工人员快速定位。
关于红外对管的安装高度,我实测过的经验数据是:安装在车位正后方地面以上 30~40cm 处,略高于普通轿车保险杠下沿,但低于 SUV 的底盘高度。这个位置既能保证轿车底盘反射红外光,又不会被 SUV 的底盘连续遮挡导致无法区分“占用”和“空闲”。如果车位是斜列式,传感器要偏向车头中央位置,不能对着车轮——车轮的轮毂表面是漫反射体,会让接收信号强度很不稳定。
源码包里保留了 DSP 数学库文件libarm_cortexM3l_math.a,说明工程在 ADC 采样或传感器数据平滑上使用了浮点运算。如果你后续自己改装成模拟量输出型传感器,可以在 ADC 中断回调里用这个库做滑动平均滤波,比如连续采 16 次去掉最大值和最小值再取平均,能明显降低地面反光造成的尖峰误判。注意 F1 系列没有硬件 FPU,浮点运算全部走软件模拟,尽量不要在中断里做大量三角函数运算,滤波窗口超过 32 个点就会影响主循环的实时性。
最后提一个很多人忽略的细节:压缩包里的*.bak文件是 Keil 工程在上一次编译失败时自动生成的备份,不是源码逻辑的一部分,提交到 Git 仓库时应该加进.gitignore。否则每次编译都会产生新的stm32f1xx_hal_conf.h.bak,污染版本记录,时间长了你会分不清哪个才是当前生效的配置。单独把.c和.h源文件管理起来,配合教程文档里的 PINOUT 表格,这套系统拿到任何一块 STM32F103 核心板上都能在三小时内跑起来——前提是你先把传感器固定支架打好。
本文还有配套的精品资源,点击获取