
简介基于STM32单片机的停车场车位占用检测系统完整项目面向电子信息、物联网、自动化等专业学生及毕业设计、课程设计需求。资源包含完整源码与教程文档共858个文件、约32.23MB其中C源文件与头文件构成核心嵌入式代码uvprojx、IOC、ICF等工程配置便于直接编译调试SCHDOC、PCBDOC等硬件设计文件辅助理解电路LOG、MD、TXT等记录开发与排错过程。代码源于高分毕业设计答辩得分95分经测试运行成功具备较强工程参考价值。包内还提供硬件原理图、PCB设计、项目说明及调试日志可帮助初学者快速理解车位检测流程也可作为课设、毕设整体方案。已有170人学习/浏览适合需要快速上手STM32项目开发或完成课题设计的读者。1. STM32停车场车位占用检测到底在解决什么问题开车进地下车库负一层绕了两圈才在角落发现一个空位这种体验每个人都经历过。停车场的空位信息如果能实时显示在入口或每个车位的指示灯上找位效率会明显提升。本标题所指向的项目就是一套基于 STM32 单片机、用红外或超声波传感器感知每个车位“有没有车”的嵌入式系统传感器把物理占用转换成电平信号STM32 读取后用状态机维护剩余车位数量再通过显示屏或指示灯呈现结果。它不涉及车牌识别、道闸联动或云端计费核心就一件事——把“车位被占”这个事实变成单片机里一个稳定的电平状态。这类项目在单片机课程设计和毕业设计中出现频率极高因为它的知识点覆盖非常典型GPIO 输入、传感器选型、消抖滤波、状态机设计、外设驱动全部是嵌入式入门必须掌握的技能。也正因为如此网络上的同名资源很多但不少资料要么只给原理图和 main.c要么把关键的状态变化逻辑藏起来。这篇文章会把检测方案选型、硬件接线、固件实现和调试排错一次讲透让拿到这套题目的人能够真正复现并理解每一行代码的作用。2. 车位占用检测方案选型红外对管、超声波与 STM32 的 GPIO 建模2.1 两种常见探测方式怎么选停车位占用检测在工业上有地磁、雷达、视频等多种手段但在 STM32 单片机课程设计和毕业设计这个场景下常见做法只有两种红外对管或漫反射红外传感器和超声波测距模块。它们的共同点是输出信号能被单片机的 GPIO 直接处理差别在于信号的形态和抗干扰能力。红外对管采用 E18-D80NK 这类一体化漫反射传感器时模块内部已经把红外发射、接收和比较电路集成好了输出的是 TTL 电平检测到前方有障碍物时输出低电平或高电平取决于型号没有障碍物时输出相反电平。超声波 HC-SR04 则输出的是一个宽度代表距离的脉冲单片机需要先发一个 10us 以上的触发信号再测量 Echo 引脚高电平持续时间换算成厘米距离。对比项红外对管 / E18-D80NK超声波 HC-SR04输出信号TTL 电平直接读 GPIO脉冲宽度需测距换算检测逻辑有障碍物/无障碍物距离值小于阈值视为占用响应速度毫秒级实时性高单次测量约 20ms周期约 50ms成本单个约 5-15 元单个约 3-10 元环境适应受强光影响但车内场景可控不受光线影响对软物体吸收敏感安装方式车位正上方或侧壁斜向下车位正上方垂直向下从“好不好演示”的角度看红外对管更直接手伸过去灯就亮拿开灯就灭观众一眼能看懂。超声波的演示效果则需要一个合理的阈值判断手伸过去时测距值变化不如红外那么干脆。从工程可靠性角度看红外对管的电平输出天然就对应“有/无”两个状态省去了测距和阈值比较的代码让整个固件逻辑更简洁。2.2 把“有没有车”建模成 IO 状态无论选哪种传感器最终都要落到一个抽象模型上每个车位是一个布尔量1 表示被占用0 表示空闲。在 STM32 单片机项目中这个布尔量存储在结构体或数组里与物理引脚一一对应。以 4 车位系统为例用 PA0-PA3 四个引脚接四路红外传感器输出单片机只需要周期性地执行“读引脚→滤波→更新状态”这三个动作。这里有一个容易被新手忽略的点传感器输出的电平和“车位被占用”的映射关系并不是固定的取决于传感器型号和接线方式。比如 E18-D80NK 默认无障碍物时输出高电平有障碍物时输出低电平那 GPIO 读到低电平才表示“有车”如果代码里写反了显示结果会完全颠倒。一个稳妥的做法是在原理图上标注清楚传感器输出极性并在代码里用宏定义统一管理#define SENSOR_OCCUPIED_LEVEL GPIO_PIN_RESET /* E18-D80NK: 有车输出低电平 */ #define SENSOR_FREE_LEVEL GPIO_PIN_SET /* 无车输出高电平 */这样当更换不同极性的传感器时只需要修改这一处宏不需要改动业务逻辑代码。这个习惯在单片机项目里很重要因为硬件极性、引脚编号这类信息散落在原理图中集中定义能避免后续维护时到处找魔数。2.3 为什么直接读电平而不维护测距值超声波方案的代码里通常会维护一个 uint16_t distance 变量然后写if (distance 30)判断占用。这在毕设答辩时显得更有技术含量但实际运行中会引入一个麻烦阈值怎么定。车身高度、安装角度、地面反射特性都会影响测距值阈值设大了旁边车道的人走过会误触发设小了 SUV 和轿车的车顶高度差异又可能导致漏检。红外方案的思路完全不同它不关心物体距离只关心“有没有东西在传感器正前方”。通过调节传感器上的电位器可以设定检测距离E18-D80NK 可调范围约 3-80cm调好后它是一个硬阈值由模拟比较电路完成不需要单片机参与计算。这样单片机侧的代码就退化成纯 GPIO 状态读取逻辑上更接近“按下开关”而不是“测量距离”。如果坚持用超声波建议把距离值打印到串口多次实测后取一个中间值作为阈值并加上迟滞区间比如占用阈值 30cm释放阈值 40cm避免车辆停在临界位置时状态反复跳变。但在这类题目中以 STM32 单片机课程设计的要求来看红外对管方案明显更容易把项目做稳定也更容易讲清楚。3. STM32 最小系统与传感器接线从原理图到 GPIO 初始化3.1 最小系统器件清单STM32 单片机要跑起来最小系统只包含电源、晶振、复位、下载接口和去耦电容这几部分。市面上常见的 STM32F103C8T6 核心板已经把最小系统做好了直接插面包板或接杜邦线就能用但如果是自制 PCB 底板需要自己排布以下器件器件参数作用STM32F103C8T6LQFP48 封装主控64KB Flash20KB RAM8MHz 晶振 两个 20pF 负载电容无源晶振提供 HSE 时钟源32.768kHz 晶振可选32768HzRTC 使用本场景可省略复位电路10kΩ 上拉电阻 100nF 电容NRST 引脚复位电源滤波100nF 每个 VDD 就近放置 4.7uF 钽电容去耦防止电机或继电器启动时复位AMS1117-3.3输入 5V输出 3.3V给单片机供电SWD 下载口4PinSWDIO、SWCLK、GND、3.3V下载调试这里特别强调电源部分。停车场检测系统里如果带了继电器或 LED 灯带感性负载启动瞬间电流会拉低电源电压导致 STM32 单片机复位重启。常见做法是传感器和单片机共用 5V 输入但 3.3V 和 5V 地要共地红外传感器的信号输出引脚是 TTL 电平可以直接进 3.3V 的 GPIO不需要电平转换。3.2 传感器与单片机的接线方式以 4 个车位、4 路 E18-D80NK 红外传感器为例接线表如下传感器引脚单片机引脚说明VCC棕色5VE18-D80NK 工作电压 5VGND蓝色GND共地必须与单片机同地OUT黑色PA0 / PA1 / PA2 / PA3信号输出TTL 电平E18-D80NK 的输出在无障碍物时是高电平检测到障碍物时拉低。如果把 OUT 直接接到 STM32 的 PA0需要把 GPIO 配置为输入模式内部上拉或下拉取决于传感器空闲电平。由于空闲时输出高检测时输出低理论上配置成浮空输入也能工作但为了防止传感器输出引脚在断电瞬间出现不确定态推荐配置内部上拉这样传感器未供电时单片机读到的是高电平对应“无车”状态不会误报占用。如果需要用指示灯模拟车位占用可以在 PB0-PB3 接 LED 或继电器模块由 STM32 单片机输出控制。LED 串 330Ω 限流电阻继电器模块用 NPN 三极管驱动不要直接用 GPIO 驱动线圈。3.3 STM32 HAL 库的 GPIO 输入初始化代码这里给出一段可以直接抄的 STM32CubeMX 生成的初始化代码手动创建工程时也用同样的配置/* 车位传感器输入引脚定义 */ #define SENSOR_1_PIN GPIO_PIN_0 #define SENSOR_1_PORT GPIOA #define SENSOR_2_PIN GPIO_PIN_1 #define SENSOR_2_PORT GPIOA #define SENSOR_3_PIN GPIO_PIN_2 #define SENSOR_3_PORT GPIOA #define SENSOR_4_PIN GPIO_PIN_3 #define SENSOR_4_PORT GPIOA static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能 GPIOA 时钟 */ GPIO_InitStruct.Pin SENSOR_1_PIN | SENSOR_2_PIN | SENSOR_3_PIN | SENSOR_4_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; /* 输入模式 */ GPIO_InitStruct.Pull GPIO_PULLUP; /* 内部上拉配合传感器空闲高电平 */ HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* PB0-PB3 配置为推挽输出驱动车位指示灯 */ __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }这段代码的逻辑说明第一步使能 GPIOA 时钟这是 STM32 外设使用前的固定动作漏掉的话寄存器写入无效程序跑起来引脚电平始终不变。第二步把 PA0-PA3 设为输入并开启内部上拉对应 E18-D80NK 空闲高电平、检测低电平的极性。第三步把 PB0-PB3 设为推挽输出用于控制 LED 指示灯推挽模式下 GPIO 可以输出高电平和低电平驱动 LED 没有问题。参数调整方面如果用的是超声波 HC-SR04则不需要配置输入引脚而是把触发引脚配成输出、回波引脚配成输入回波引脚必须支持输入捕获或外部中断因为脉冲宽度的测量依赖定时器。对于纯 GPIO 的方案速度档位选 LOW 即可高速档位不仅增加功耗还会引入不必要的边沿噪声。3.4 长线传输与信号抖动处理传感器和单片机之间如果距离超过 1 米信号线本身就会成为天线引入电磁干扰。两个常见做法可以规避大部分问题信号线用双绞线或屏蔽线屏蔽层单端接地在单片机引脚处对地并联一个 100nF 陶瓷电容构成一个低通滤波器把高频毛刺滤掉。注意这个电容不能太大否则会把信号边沿变缓STM32 单片机读到的电平切换会有延迟。100nF 配合 STM32 输入引脚自带的施密特触发器对几毫秒级别的开关信号完全够用。做完硬件滤波之后软件上仍然需要做去抖处理因为传感器在检测边界附近时输出会快速抖动单片机的读取速度极快一次采样读到高、下一次读到低都是正常的。这个问题的解法在下一章给出。4. 车位状态机与剩余车位统计消抖、状态迁移与输出显示4.1 直接读 GPIO 为什么会误判STM32 单片机的主频是 72MHzwhile 循环里读一次 GPIO 只花几十纳秒。红外传感器在车位边界附近时输出的电平波动可能持续几十毫秒这意味着单片机在 1 秒内能读到成百上千次状态变化。如果代码里不做处理剩余车位数字会在“3”和“4”之间疯狂跳变这也是很多同类项目被评委一眼看出问题的原因。核心思路是借鉴按键消抖的方法连续多次采样到相同的电平才认为状态稳定。对于红外传感器建议每 10ms 采样一次连续 5 次共 50ms一致才更新状态这个参数在实际测试中表现很好。/* 带消抖的车位状态读取返回 1 表示有车0 表示无车 */ uint8_t Sensor_ReadDebounced(GPIO_TypeDef *port, uint16_t pin) { uint8_t level_set_count 0; uint8_t level_reset_count 0; for (uint8_t i 0; i 5; i) { if (HAL_GPIO_ReadPin(port, pin) GPIO_PIN_RESET) { level_set_count; /* 有车电平E18-D80NK 输出低 */ } else { level_reset_count; /* 无车电平 */ } HAL_Delay(10); /* 每 10ms 采样一次 */ } if (level_set_count 4) return 1; /* 5 次中 4 次为低确认有车 */ if (level_reset_count 4) return 0; /* 5 次中 4 次为高确认无车 */ return HAL_GPIO_ReadPin(port, pin) GPIO_PIN_RESET ? 1 : 0; }这个函数的参数说明port 是 GPIO 端口基地址比如 GPIOApin 是引脚编号比如 GPIO_PIN_0。函数内部循环采样 5 次每次间隔 10ms总耗时约 50ms。判断条件设定为“5 次中 4 次一致”而不是全部一致是为了容忍偶发的一次干扰采样同时避免响应过慢。如果现场干扰严重可以把采样次数提高到 10 次代价是状态更新的响应时间从 50ms 变成 100ms这对停车位检测完全可以接受。4.2 状态机与剩余车位统计的迁移逻辑多车位系统的剩余车位统计容易犯一个错误每次检测到“有车”就把剩余车位减一检测到“无车”就加一。这在 while 循环里执行会导致剩余车位反复加减最终变成负数或超过总车位数。正确的做法是只在状态发生变化时更新计数。每个车位保存上一次的状态当前状态与上一次不同才执行加减操作typedef enum { CAR_ABSENT 0, CAR_PRESENT } CarState; #define PARKING_SLOT_COUNT 4 CarState current_state[PARKING_SLOT_COUNT]; CarState last_state[PARKING_SLOT_COUNT]; uint8_t free_slot_count PARKING_SLOT_COUNT; void Parking_UpdateSlot(uint8_t slot_index) { /* 读取当前车位状态1 表示有车 */ current_state[slot_index] Sensor_ReadDebounced(GPIOA, GPIO_PIN_0 slot_index); /* 状态没变化不更新计数 */ if (current_state[slot_index] last_state[slot_index]) return; if (current_state[slot_index] CAR_PRESENT) { /* 无车变为有车剩余车位减一 */ if (free_slot_count 0) free_slot_count--; } else { /* 有车变为无车剩余车位加一 */ if (free_slot_count PARKING_SLOT_COUNT) free_slot_count; } last_state[slot_index] current_state[slot_index]; }状态迁移规则可以归纳成下表上一次状态当前状态剩余车位变化触发场景无车有车减 1车辆驶入车位有车无车加 1车辆驶离车位无车无车不变车位持续空闲有车有车不变车辆驻留这个状态机的参数说明slot_index 是车位的索引0 到 3对应 PA0 到 PA3。主循环里依次调用 Parking_UpdateSlot(0) 到 Parking_UpdateSlot(3) 即可。free_slot_count 在每次状态翻转时做边界保护防止加溢出或减到负数。特别注意GPIO_PIN_0 slot_index 的写法依赖引脚连续排列 PA0-PA3如果原理图里传感器接的是 PA0、PA4、PB0 等分散引脚必须改用查表方式不能这样位移。4.3 OLED 显示与串口输出剩余车位显示部分推荐用 0.96 寸 I2C 接口的 OLEDSSD1306 驱动芯片占用 I2C1 的 PB6SCL和 PB7SDA两个引脚接线就四根线非常适合这类单片机项目。OLED 上同时显示总车位数、剩余车位数以及每个车位的占用状态用方块或文字表示。初始化 I2C 后主循环里集中刷新一次显示char line1[32]; char line2[32]; sprintf(line1, Free:%d/%d, free_slot_count, PARKING_SLOT_COUNT); sprintf(line2, Slot1:%c Slot2:%c, current_state[0] ? X : O, current_state[1] ? X : O); OLED_ShowString(0, 0, (uint8_t *)line1); OLED_ShowString(0, 2, (uint8_t *)line2);串口输出的价值在于调试时不需要盯着屏幕直接用串口助手观察状态变化过程。建议在状态发生翻转时打印一条带时间戳的记录方便回放现场情况printf([%lu] Slot%d %s, free%d\r\n, HAL_GetTick(), slot_index, current_state[slot_index] ? OCCUPIED : FREE, free_slot_count);printf 在 STM32 上需要重定向 fputc把它重定向到 USART1 的发送寄存器。调试阶段把波特率设为 115200数据位 8停止位 1无校验。打印频率要控制状态没变化时不打印否则调试信息会刷屏。4.4 多车位轮询时的响应冲突当车同时驶入两个车位时两个传感器可能在同一瞬间发生变化。由于主循环是按顺序处理的先处理的车位立即更新计数后处理的车位在几毫秒后被识别最终计数结果依然是正确的不会丢状态。但如果一次循环里 Sensor_ReadDebounced 内部有 50ms 的 HAL_Delay4 个车位全部处理完需要 200ms这个响应速度在停车场景完全够用但会影响其他外设的实时性比如按键响应会变慢。更专业的做法是用定时器中断驱动采样定时器每 10ms 触发一次中断中断里对 4 路引脚各采样一次并更新各自的连续计数到达 5 次后置一个标志位主循环检测到标志位再去更新状态机。这样传感器去抖过程与主循环解耦不会出现 HAL_Delay 卡住整个系统的情况。范伟老师的课程设计中如果对实时性有要求建议采用中断方案代码结构会复杂一些但可以放在答辩时作为亮点讲解。5. 烧录调试与现场验证ST-Link 连接、常见报错与模拟场景测试5.1 ST-Link 四线接法与启动模式设置下载调试的最常见做法是 ST-Link V2 SWD 四线接口接线就 SWDIO、SWCLK、GND、3.3V 四根。接线顺序建议按“先地线再信号线最后电源”的顺序插避免带电插拔导致芯片异常复位。打开“stm32 st-link utility”可以验证连接是否正常能读到芯片 ID 就说明 SWD 链路已经通了。启动模式 Boot0 需要拉低接地让单片机从 Flash 启动。如果 Boot0 悬空或接高芯片上电后会进入系统存储器内置 bootloaderST-Link 依然能连接但用户程序不会运行表现为指示灯全灭、串口无输出。这是一条很常见的排查路径。5.2 下载报错的排查清单“No STM32 Target Found”是这类单片机项目中排名第一的报错调试时逐项排查现象可能原因排查动作无法连接提示 Target not found供电不足或 3.3V 没接万用表量核心板 3.3V 对地电压SWD 识别失败SWDIO 和 SWCLK 接反对调两根信号线能识别但下载后不运行Boot0 为高电平确认 Boot0 接地下载成功但程序不执行芯片被 debug authentication 锁定检查是否误烧写读保护选项用 ST-Link Utility 全片擦除虚拟串口叹号ST-Link 驱动未装或版本过旧重装 ST-Link USB driver特别提醒 debug authentication 的问题部分新出厂的 STM32 芯片带有调试认证保护如果之前在 ST-Link Utility 里勾选了读保护选项RDP Level 1 以上SWD 连接就会被拒绝。解决方法是先用 ST-Link Utility 执行 Full Chip Erase擦除后再连接。如果是 RDP Level 2芯片将永久禁止调试只能更换芯片所以调试阶段不要轻易调高读保护等级。5.3 用纸箱和手模拟车位的功能验证现场没有真车时用一个纸箱或书包充当“车辆”把它放到传感器下方观察状态翻转是否符合预期。推荐的验证步骤单独遮挡 1 号车位传感器确认 OLED 上 Slot1 从 O 变为 X剩余车位减 1。保持遮挡 2 秒以上确认状态不会来回闪动消抖逻辑生效。同时遮挡 1 号和 3 号车位确认自由计数从 4 变为 2验证多车位并发处理。移走遮挡物逐个确认每个车位恢复到 O剩余车位恢复到 4。用手在传感器前方 5cm 处快速晃动确认状态不被误触发。在这个验证过程中最容易暴露的问题是传感器检测距离调节不当。E18-D80NK 背面有一个十字电位器顺时针调大检测距离逆时针调小。如果传感器安装高度距离地面 1.5 米需要把距离调到 1.2 米左右留出 30cm 的余量避免只检测到车顶中央侧面车身进入时检测不到。5.4 把车位数据上送到 Modbus 从机帧这套检测系统如果做成真正的产品数据需要传给上位机或停车场管理平台。最稳妥且不增加硬件成本的方案是用 STM32 单片机的 USART 接一个 RS485 收发器如 MAX3485以 Modbus RTU 从机模式响应主站查询。把 free_slot_count 和每个车位的状态封装成保持寄存器主站周期性读取即可。Modbus 单片机帧接收数据程序的核心在于帧超时判断接收完最后一个字节后等待 3.5 个字符时间没有新数据就认为一帧接收结束然后解析地址码、功能码、CRC。这个做法比固定长度接收更健壮因为不同功能码的帧长度不同。把 CRC 校验写在中断接收里还是主循环里取决于代码结构但无论如何帧超时定时器一定要用硬件定时器实现不要用 HAL_Delay 死等否则主站在发送广播帧时从机可能来不及响应。本文还有配套的精品资源点击获取