【项目思路】基于STM32的智能停车场管理系统设计与实现
一、项目概述
1.1 项目背景与目标
随着城市汽车保有量持续增长,停车难已成为普遍问题。传统停车场依赖人工管理,存在以下痛点:
- 车位信息不透明,驾驶员进场后盲目寻找空位,浪费时间和燃油
- 人工收费效率低,高峰期出口排队严重
- 缺乏数据统计手段,管理方无法掌握车位周转率和收入情况
- 纸质记录易丢失,进出车辆无法追溯
本项目设计一套低成本的智能停车场管理系统,实现以下目标:
- 车位实时检测:超声波测距判断车位占用状态,支持多车位扩展
- 身份识别与计费:RFID刷卡进出,自动计算停车时长和费用
- 道闸自动控制:舵机驱动道闸,刷卡后自动抬杆/落杆
- 本地信息显示:OLED屏显示剩余车位数和系统状态
- 远程监控管理:通过ESP8266上报数据,PyQt5上位机实现车位地图、记录查询、费用统计
1.2 技术栈选型
| 技术领域 | 具体选型 | 选择理由 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | 72MHz Cortex-M3,GPIO/定时器资源充足,成本低 |
| 车位检测 | HC-SR04超声波模块 | 量程2-400cm,精度3mm,非接触式检测,成本低 |
| 身份识别 | RC522 RFID读卡器 | 13.56MHz,SPI接口,读卡距离3-5cm,响应快 |
| 道闸控制 | SG90舵机 | PWM控制,扭矩1.8kg·cm,模拟道闸升降 |
| 显示模块 | 0.96寸OLED(SSD1306) | I2C接口,功耗低,显示清晰 |
| WiFi通信 | ESP8266-01S | AT指令控制,MQTT支持成熟 |
| 通信协议 | MQTT v3.1.1 | 轻量级发布/订阅,适合IoT场景 |
| 上位机 | Python + PyQt5 | 跨平台,开发效率高,图表库丰富 |
二、系统架构设计
2.1 整体架构
系统采用三层架构设计:感知层负责车位检测和身份识别,控制层负责业务逻辑和设备驱动,应用层负责远程监控和数据管理。
感知层(传感器与执行器):
- HC-SR04超声波模块:检测车位上方是否有车辆(距离阈值判断)
- RC522 RFID读卡器:读取IC卡UID,识别车辆身份
- SG90舵机:模拟道闸升降动作
- OLED显示屏:本地显示剩余车位和系统状态
控制层(STM32主控):
- 车位状态管理:周期性超声波测距,判断占用/空闲
- 进出场逻辑:刷卡→查询记录→计费→抬杆→延时落杆
- 数据封装上报:JSON格式通过UART发送至ESP8266
应用层(PyQt5上位机):
- 车位地图:实时显示各车位占用状态
- 进出记录:车辆进出时间、停车时长、费用明细
- 统计报表:日/月收入统计、车位周转率、高峰时段分析

2.2 数据流向
进场流程:
车辆到达入口 → RFID刷卡 → STM32读取卡号 → 记录进场时间 → 舵机抬杆 → 车辆驶入 → 延时3秒落杆 → MQTT上报进场事件
出场流程:
车辆到达出口 → RFID刷卡 → 查询进场记录 → 计算停车时长和费用 → OLED显示费用 → 舵机抬杆 → 车辆驶出 → 延时落杆 → MQTT上报出场事件和费用
车位检测流程:
定时器触发(每2秒) → 超声波测距 → 距离<阈值判定为占用 → 更新车位状态 → 状态变化时MQTT上报
三、环境搭建与硬件连接
3.1 硬件清单
| 组件 | 型号 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 | 带ST-Link下载口 |
| 超声波模块 | HC-SR04 | 4 | 模拟4个车位检测 |
| RFID读卡器 | RC522模块 | 1 | 配2张IC白卡 |
| 舵机 | SG90 9g微型舵机 | 1 | 模拟道闸,PWM控制 |
| 显示屏 | 0.96寸OLED(SSD1306) | 1 | I2C接口 |
| WiFi模块 | ESP8266-01S | 1 | 配3.3V独立供电 |
| 蜂鸣器 | 有源蜂鸣器 | 1 | 刷卡提示音 |
| LED指示灯 | 红/绿LED | 2 | 入口状态指示 |
| 电源 | 5V/2A电源适配器 | 1 | 配AMS1117-3.3稳压 |
3.2 关键接线说明
超声波模块引脚分配(4路车位):
| 车位编号 | Trig引脚 | Echo引脚 | 备注 |
|---|---|---|---|
| 车位1 | PA0 | PA1 | TIM2_CH1捕获Echo |
| 车位2 | PA2 | PA3 | TIM2_CH2捕获Echo |
| 车位3 | PA6 | PA7 | TIM3_CH1捕获Echo |
| 车位4 | PB0 | PB1 | TIM3_CH3捕获Echo |
其他模块接线:
| 模块 | STM32引脚 | 接口类型 | 备注 |
|---|---|---|---|
| RC522(SCK/MISO/MOSI/SDA/RST) | PA5/PA6/PA7/PA4/PB0 | SPI1 | 注意:与车位3/4的超声波共用引脚,需分时复用或改用SPI2 |
| OLED(SCL/SDA) | PB6/PB7 | I2C1 | 外接4.7KΩ上拉 |
| ESP8266(TX/RX) | PA9/PA10 | USART1 | 波特率115200,交叉连接 |
| SG90舵机(信号线) | PB9 | TIM4_CH4 PWM | 50Hz PWM,0.5-2.5ms脉宽 |
| 蜂鸣器 | PB12 | GPIO推挽 | 高电平触发 |
| LED(绿/红) | PB13/PB14 | GPIO推挽 | 空位绿灯/满位红灯 |
⚠️ 重要提醒:
- RC522与超声波模块共用SPI1引脚,实际设计中建议将RC522改接到SPI2(PB13/PB14/PB15),或者减少超声波通道数量,避免引脚冲突。
- SG90舵机工作电流峰值可达750mA,必须从5V电源直接供电,不要从STM32取电。
- HC-SR04的Echo引脚输出5V电平,需要通过分压电阻(1KΩ+2KΩ)降至3.3V后再接入STM32。
- 多路超声波模块不要同时触发,需要依次触发避免串扰,每路间隔至少60ms。
3.3 软件环境配置
开发环境:
- STM32CubeIDE 1.13.0 或 Keil MDK 5.38
- STM32CubeMX(HAL库代码生成)
- Python 3.8+(上位机开发)
Python依赖安装:
# 安装上位机依赖包
pip install PyQt5==5.15.9
pip install paho-mqtt==1.6.1
pip install pyqtgraph==0.13.3
四、代码实现详解
4.1 超声波车位检测模块
超声波模块通过发送40kHz脉冲并测量回波时间来计算距离,当距离小于设定阈值时判定车位被占用。
ultrasonic.h
#ifndef __ULTRASONIC_H
#define __ULTRASONIC_H
#include "stm32f1xx_hal.h"
#define PARKING_SLOT_NUM 4 // 车位数量
#define DISTANCE_THRESHOLD 30.0f // 车位占用判定阈值(cm)
#define MEASURE_INTERVAL_MS 500 // 测量间隔(ms)
/* 车位状态 */
typedef enum {
SLOT_FREE = 0, // 空闲
SLOT_OCCUPIED = 1 // 占用
} Slot_Status_t;
/* 单个车位数据 */
typedef struct {
GPIO_TypeDef *trig_port;
uint16_t trig_pin;
float distance; // 当前测量距离(cm)
Slot_Status_t status; // 当前状态
Slot_Status_t last_status; // 上次状态(用于变化检测)
uint8_t confirm_cnt; // 状态确认计数(防抖)
} Parking_Slot_t;
void Ultrasonic_Init(void);
void Ultrasonic_MeasureAll(void);
Slot_Status_t Ultrasonic_GetStatus(uint8_t slot_id);
float Ultrasonic_GetDistance(uint8_t slot_id);
uint8_t Ultrasonic_GetFreeCount(void);
#endif
ultrasonic.c(核心函数)
#include "ultrasonic.h"
static Parking_Slot_t slots[PARKING_SLOT_NUM];
/* 微秒延时(DWT计数器) */
static void delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000);
while ((DWT->CYCCNT - start) < ticks);
}
/**
* @brief 测量单个车位的距离
* @param slot: 车位结构体指针
* @return 距离值(cm),超时返回999.0
* @note 原理:Trig拉高10us触发测量,Echo高电平持续时间 * 声速/2 = 距离
* 声速340m/s,即 time_us / 58 = distance_cm
*/
static float Ultrasonic_Measure(Parking_Slot_t *slot)
{
/* 发送10us触发脉冲 */
HAL_GPIO_WritePin(slot->trig_port, slot->trig_pin, GPIO_PIN_SET);
delay_us(10);
HAL_GPIO_WritePin(slot->trig_port, slot->trig_pin, GPIO_PIN_RESET);
/* 等待Echo变高(超时30ms) */
uint32_t timeout = HAL_GetTick();
while (HAL_GPIO_ReadPin(slot->echo_port, slot->echo_pin) == GPIO_PIN_RESET)
{
if ((HAL_GetTick() - timeout) > 30) return 999.0f;
}
/* 记录Echo高电平持续时间 */
uint32_t start = DWT->CYCCNT;
while (HAL_GPIO_ReadPin(slot->echo_port, slot->echo_pin) == GPIO_PIN_SET)
{
if ((HAL_GetTick() - timeout) > 60) return 999.0f;
}
uint32_t elapsed_us = (DWT->CYCCNT - start) / (SystemCoreClock / 1000000);
return elapsed_us / 58.0f;
}
/**
* @brief 测量所有车位并更新状态
* @note 依次触发每路超声波,间隔60ms避免串扰。
* 状态变化需连续3次确认才生效(防抖),防止行人经过误触发。
*/
void Ultrasonic_MeasureAll(void)
{
for (uint8_t i = 0; i < PARKING_SLOT_NUM; i++)
{
slots[i].distance = Ultrasonic_Measure(&slots[i]);
/* 判断当前测量状态 */
Slot_Status_t measured = (slots[i].distance < DISTANCE_THRESHOLD)
? SLOT_OCCUPIED : SLOT_FREE;
/* 防抖:连续3次相同结果才更新状态 */
if (measured != slots[i].status)
{
slots[i].confirm_cnt++;
if (slots[i].confirm_cnt >= 3)
{
slots[i].last_status = slots[i].status;
slots[i].status = measured;
slots[i].confirm_cnt = 0;
}
}
else
{
slots[i].confirm_cnt = 0;
}
HAL_Delay(60); // 等待回波消散,避免串扰
}
}
/**
* @brief 获取空闲车位数量
*/
uint8_t Ultrasonic_GetFreeCount(void)
{
uint8_t count = 0;
for (uint8_t i = 0; i < PARKING_SLOT_NUM; i++)
if (slots[i].status == SLOT_FREE) count++;
return count;
}
关键函数说明:
Ultrasonic_Measure()— 单次测距核心函数,通过Trig触发和Echo计时实现距离计算,超时保护防止程序卡死Ultrasonic_MeasureAll()— 轮询所有车位,带3次确认防抖机制,避免行人或飞鸟经过导致误判Ultrasonic_GetFreeCount()— 统计空闲车位数,供OLED显示和MQTT上报使用
4.2 RFID读卡与计费模块
RC522通过SPI接口读取IC卡的UID,系统根据UID匹配进出场记录并自动计费。
rfid_parking.h
#ifndef __RFID_PARKING_H
#define __RFID_PARKING_H
#include "stm32f1xx_hal.h"
#define MAX_RECORDS 50 // 最大同时在场车辆数
#define FEE_PER_HOUR 5.0f // 每小时停车费(元)
#define FREE_MINUTES 15 // 免费停车时长(分钟)
/* 停车记录 */
typedef struct {
uint8_t uid[4]; // IC卡UID
uint32_t entry_tick; // 进场时间戳(系统tick)
uint32_t exit_tick; // 出场时间戳
float fee; // 停车费用
uint8_t active; // 记录是否有效(在场)
} Parking_Record_t;
/* 刷卡事件类型 */
typedef enum {
EVENT_ENTRY = 0, // 进场
EVENT_EXIT = 1, // 出场
EVENT_FULL = 2, // 车位已满,拒绝进场
EVENT_ERROR = 3 // 读卡失败
} Card_Event_t;
/* 刷卡结果 */
typedef struct {
Card_Event_t event;
uint8_t uid[4];
float fee; // 出场时的费用
uint32_t duration_min; // 停车时长(分钟)
} Card_Result_t;
void RFID_Parking_Init(void);
Card_Result_t RFID_Parking_Process(void);
uint8_t RFID_Parking_GetCount(void);
#endif
rfid_parking.c(核心函数)
#include "rfid_parking.h"
#include "rc522.h"
#include "ultrasonic.h"
static Parking_Record_t records[MAX_RECORDS];
/**
* @brief 查找UID对应的在场记录
* @param uid: 4字节IC卡UID
* @return 记录索引,未找到返回-1
* @note 遍历所有active记录,比较UID是否匹配
*/
static int8_t Find_Record(uint8_t *uid)
{
for (uint8_t i = 0; i < MAX_RECORDS; i++)
{
if (records[i].active &&
memcmp(records[i].uid, uid, 4) == 0)
return i;
}
return -1;
}
/**
* @brief 计算停车费用
* @param entry_tick: 进场时间戳
* @param exit_tick: 出场时间戳
* @return 费用(元)
* @note 15分钟内免费,超出部分按每小时5元计费,不足1小时按1小时算
*/
static float Calculate_Fee(uint32_t entry_tick, uint32_t exit_tick)
{
uint32_t duration_ms = exit_tick - entry_tick;
uint32_t minutes = duration_ms / 60000;
if (minutes <= FREE_MINUTES) return 0.0f;
/* 超出免费时长的部分,向上取整到小时 */
uint32_t billable_min = minutes - FREE_MINUTES;
uint32_t hours = (billable_min + 59) / 60; // 向上取整
return hours * FEE_PER_HOUR;
}
/**
* @brief 处理一次刷卡事件(核心业务逻辑)
* @return 刷卡结果,包含事件类型、费用等信息
* @note 自动判断进场/出场:
* - 卡号不在记录中 → 进场(检查是否有空位)
* - 卡号已在记录中 → 出场(计费并清除记录)
*/
Card_Result_t RFID_Parking_Process(void)
{
Card_Result_t result = {0};
uint8_t uid[4];
/* 尝试读卡 */
if (RC522_ReadCard(uid) != 0)
{
result.event = EVENT_ERROR;
return result;
}
memcpy(result.uid, uid, 4);
/* 查找是否有在场记录 */
int8_t idx = Find_Record(uid);
if (idx >= 0)
{
/* 已有记录 → 出场 */
records[idx].exit_tick = HAL_GetTick();
records[idx].fee = Calculate_Fee(records[idx].entry_tick, records[idx].exit_tick);
records[idx].active = 0;
result.event = EVENT_EXIT;
result.fee = records[idx].fee;
result.duration_min = (records[idx].exit_tick - records[idx].entry_tick) / 60000;
}
else
{
/* 无记录 → 进场 */
if (Ultrasonic_GetFreeCount() == 0)
{
result.event = EVENT_FULL;
return result;
}
/* 找空位存储新记录 */
for (uint8_t i = 0; i < MAX_RECORDS; i++)
{
if (!records[i].active)
{
memcpy(records[i].uid, uid, 4);
records[i].entry_tick = HAL_GetTick();
records[i].active = 1;
break;
}
}
result.event = EVENT_ENTRY;
}
return result;
}
关键函数说明:
RFID_Parking_Process()— 刷卡业务核心,自动判断进场/出场。卡号不在记录中视为进场(需检查空位),已在记录中视为出场(自动计费)Calculate_Fee()— 计费算法,15分钟内免费,超出部分按小时向上取整计费Find_Record()— 通过UID遍历查找在场记录,时间复杂度O(n),50条记录内性能无压力
4.3 舵机道闸控制模块
SG90舵机通过PWM信号控制旋转角度,0°对应落杆(关闭),90°对应抬杆(开启)。
gate_control.h
#ifndef __GATE_CONTROL_H
#define __GATE_CONTROL_H
#include "stm32f1xx_hal.h"
typedef enum {
GATE_CLOSED = 0, // 落杆
GATE_OPEN = 1 // 抬杆
} Gate_State_t;
void Gate_Init(void);
void Gate_Open(void);
void Gate_Close(void);
void Gate_AutoProcess(void);
#endif
gate_control.c
#include "gate_control.h"
extern TIM_HandleTypeDef htim4;
static Gate_State_t gate_state = GATE_CLOSED;
static uint32_t open_tick = 0;
#define AUTO_CLOSE_MS 5000 // 自动落杆延时(ms)
/**
* @brief 设置舵机角度
* @param angle: 目标角度(0-180)
* @note SG90的PWM周期20ms(50Hz),脉宽0.5ms=0°,1.5ms=90°,2.5ms=180°
* TIM4预分频72-1,自动重装20000-1,则CCR值范围500-2500对应0°-180°
*/
static void Servo_SetAngle(uint8_t angle)
{
uint16_t pulse = 500 + (uint16_t)(angle * 2000.0f / 180.0f);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, pulse);
}
/**
* @brief 抬杆(开启道闸)
* @note 舵机转到90°,记录开启时间用于自动落杆
*/
void Gate_Open(void)
{
Servo_SetAngle(90);
gate_state = GATE_OPEN;
open_tick = HAL_GetTick();
}
/**
* @brief 落杆(关闭道闸)
*/
void Gate_Close(void)
{
Servo_SetAngle(0);
gate_state = GATE_CLOSED;
}
/**
* @brief 道闸自动管理,抬杆后5秒自动落杆
* @note 在主循环中周期调用,检查是否需要自动落杆
*/
void Gate_AutoProcess(void)
{
if (gate_state == GATE_OPEN)
{
if ((HAL_GetTick() - open_tick) >= AUTO_CLOSE_MS)
{
Gate_Close();
}
}
}
关键函数说明:
Servo_SetAngle()— 角度到PWM脉宽的转换,SG90标准:0.5ms对应0°,2.5ms对应180°Gate_Open()/Gate_Close()— 道闸开关控制,开启时记录时间戳Gate_AutoProcess()— 自动落杆逻辑,防止道闸长时间开启,需在主循环中周期调用
4.4 ESP8266 MQTT通信模块
通信模块负责将车位状态、进出场事件和费用信息上报至上位机。
parking_mqtt.h
#ifndef __PARKING_MQTT_H
#define __PARKING_MQTT_H
#include "stm32f1xx_hal.h"
#include "rfid_parking.h"
/* MQTT主题定义 */
#define TOPIC_SLOT_STATUS "parking/001/slots" // 车位状态
#define TOPIC_EVENT "parking/001/event" // 进出场事件
#define TOPIC_CMD "parking/001/cmd" // 控制指令
void Parking_MQTT_Init(void);
uint8_t Parking_MQTT_Connect(void);
uint8_t Parking_MQTT_PublishSlots(uint8_t *slot_status, uint8_t count, uint8_t free_count);
uint8_t Parking_MQTT_PublishEvent(Card_Result_t *result);
#endif
parking_mqtt.c(核心上报函数)
#include "parking_mqtt.h"
#include <stdio.h>
#include <string.h>
extern UART_HandleTypeDef huart1;
static char tx_buf[512];
/**
* @brief 上报车位状态(JSON数组)
* @param slot_status: 车位状态数组(0=空闲,1=占用)
* @param count: 车位总数
* @param free_count: 空闲车位数
* @note JSON格式:{"total":4,"free":2,"slots":[0,1,1,0]}
* 仅在车位状态发生变化时调用,减少不必要的网络流量
*/
uint8_t Parking_MQTT_PublishSlots(uint8_t *slot_status, uint8_t count, uint8_t free_count)
{
char slots_str[64] = "[";
for (uint8_t i = 0; i < count; i++)
{
char tmp[4];
snprintf(tmp, sizeof(tmp), "%s%d", (i > 0) ? "," : "", slot_status[i]);
strcat(slots_str, tmp);
}
strcat(slots_str, "]");
char json[256];
snprintf(json, sizeof(json),
"{\"total\":%d,\"free\":%d,\"slots\":%s}",
count, free_count, slots_str);
snprintf(tx_buf, sizeof(tx_buf),
"AT+MQTTPUB=0,\"%s\",\"%s\",1,0", TOPIC_SLOT_STATUS, json);
return AT_SendCmd(tx_buf, "OK", 3000);
}
/**
* @brief 上报进出场事件
* @param result: 刷卡结果
* @note JSON格式:{"event":"entry","uid":"A1B2C3D4","fee":0,"duration":0}
* 出场时包含费用和停车时长,进场时fee和duration为0
*/
uint8_t Parking_MQTT_PublishEvent(Card_Result_t *result)
{
const char *event_str[] = {"entry", "exit", "full", "error"};
char uid_str[12];
snprintf(uid_str, sizeof(uid_str), "%02X%02X%02X%02X",
result->uid[0], result->uid[1], result->uid[2], result->uid[3]);
char json[256];
snprintf(json, sizeof(json),
"{\"event\":\"%s\",\"uid\":\"%s\",\"fee\":%.1f,\"duration\":%lu}",
event_str[result->event], uid_str, result->fee, result->duration_min);
snprintf(tx_buf, sizeof(tx_buf),
"AT+MQTTPUB=0,\"%s\",\"%s\",1,0", TOPIC_EVENT, json);
return AT_SendCmd(tx_buf, "OK", 3000);
}
关键函数说明:
Parking_MQTT_PublishSlots()— 上报车位状态数组,仅在状态变化时调用,避免频繁发送Parking_MQTT_PublishEvent()— 上报进出场事件,包含卡号、事件类型、费用和时长
4.5 主任务调度
#include "FreeRTOS.h"
#include "task.h"
#include "ultrasonic.h"
#include "rfid_parking.h"
#include "gate_control.h"
#include "parking_mqtt.h"
#include "oled.h"
/**
* @brief 车位检测任务
* 周期2秒,轮询所有超声波模块,状态变化时上报
*/
void Task_SlotDetect(void *param)
{
static uint8_t last_status[PARKING_SLOT_NUM] = {0};
for (;;)
{
Ultrasonic_MeasureAll();
/* 检查是否有状态变化 */
uint8_t changed = 0;
uint8_t current[PARKING_SLOT_NUM];
for (uint8_t i = 0; i < PARKING_SLOT_NUM; i++)
{
current[i] = Ultrasonic_GetStatus(i);
if (current[i] != last_status[i]) changed = 1;
}
if (changed)
{
memcpy(last_status, current, PARKING_SLOT_NUM);
Parking_MQTT_PublishSlots(current, PARKING_SLOT_NUM,
Ultrasonic_GetFreeCount());
}
/* 更新OLED显示 */
OLED_ShowFreeCount(Ultrasonic_GetFreeCount(), PARKING_SLOT_NUM);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/**
* @brief 入口管理任务
* 持续检测RFID刷卡,处理进出场逻辑
*/
void Task_Entrance(void *param)
{
RFID_Parking_Init();
Gate_Init();
for (;;)
{
Card_Result_t result = RFID_Parking_Process();
if (result.event == EVENT_ENTRY)
{
/* 进场:蜂鸣器提示 + 抬杆 + 上报 */
Buzzer_Beep(100);
Gate_Open();
Parking_MQTT_PublishEvent(&result);
OLED_ShowMessage("欢迎进场");
}
else if (result.event == EVENT_EXIT)
{
/* 出场:显示费用 + 抬杆 + 上报 */
Buzzer_Beep(200);
Gate_Open();
Parking_MQTT_PublishEvent(&result);
OLED_ShowFee(result.fee, result.duration_min);
}
else if (result.event == EVENT_FULL)
{
/* 车位已满:蜂鸣器长响 + 显示提示 */
Buzzer_Beep(1000);
OLED_ShowMessage("车位已满");
}
Gate_AutoProcess(); // 检查自动落杆
vTaskDelay(pdMS_TO_TICKS(200));
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
MX_TIM4_Init();
Ultrasonic_Init();
OLED_Init();
Parking_MQTT_Init();
Parking_MQTT_Connect();
xTaskCreate(Task_SlotDetect, "Slot", 256, NULL, 3, NULL);
xTaskCreate(Task_Entrance, "Entry", 512, NULL, 4, NULL);
vTaskStartScheduler();
while (1);
}
任务设计说明:
Task_Entrance优先级(4)高于Task_SlotDetect(3),确保刷卡响应优先- 车位检测任务仅在状态变化时上报MQTT,减少网络流量
- 道闸自动落杆逻辑嵌入入口任务中,每200ms检查一次
4.6 PyQt5上位机实现
上位机通过MQTT订阅车位状态和进出场事件,实现车位地图可视化和记录管理。
parking_monitor.py
import sys
import json
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QGridLayout, QLabel, QGroupBox,
QTableWidget, QTableWidgetItem, QHeaderView,
QStatusBar)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFont, QColor, QPainter, QBrush
import paho.mqtt.client as mqtt
MQTT_BROKER = "broker.emqx.io"
MQTT_PORT = 1883
TOPIC_SLOTS = "parking/001/slots"
TOPIC_EVENT = "parking/001/event"
class SlotMapWidget(QWidget):
"""车位地图组件:用色块表示车位占用状态"""
def __init__(self, slot_count=4, parent=None):
super().__init__(parent)
self.slot_count = slot_count
self.slot_status = [0] * slot_count # 0=空闲 1=占用
self.setMinimumHeight(120)
def update_slots(self, status_list):
self.slot_status = status_list
self.update() # 触发重绘
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
w = self.width()
slot_w = min(120, (w - 40) // self.slot_count - 10)
start_x = (w - (slot_w + 10) * self.slot_count) // 2
for i in range(self.slot_count):
x = start_x + i * (slot_w + 10)
color = QColor("#e74c3c") if self.slot_status[i] else QColor("#2ecc71")
painter.setBrush(QBrush(color))
painter.setPen(Qt.NoPen)
painter.drawRoundedRect(x, 10, slot_w, 80, 8, 8)
# 车位编号和状态文字
painter.setPen(QColor("#fff"))
painter.setFont(QFont("Arial", 14, QFont.Bold))
painter.drawText(x, 10, slot_w, 50, Qt.AlignCenter, f"P{i+1}")
painter.setFont(QFont("Microsoft YaHei", 10))
text = "占用" if self.slot_status[i] else "空闲"
painter.drawText(x, 45, slot_w, 40, Qt.AlignCenter, text)
class ParkingMonitorApp(QMainWindow):
"""停车场管理上位机主窗口"""
def __init__(self):
super().__init__()
self.setWindowTitle("智能停车场管理系统 v1.0")
self.setMinimumSize(900, 650)
self._init_ui()
self._init_mqtt()
def _init_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# 顶部:统计信息
info_layout = QHBoxLayout()
self.total_label = QLabel("总车位: 4")
self.free_label = QLabel("空闲: 4")
self.occupied_label = QLabel("占用: 0")
self.income_label = QLabel("今日收入: ¥0.0")
for lbl in [self.total_label, self.free_label,
self.occupied_label, self.income_label]:
lbl.setFont(QFont("Microsoft YaHei", 14))
lbl.setAlignment(Qt.AlignCenter)
info_layout.addWidget(lbl)
layout.addLayout(info_layout)
# 车位地图
map_group = QGroupBox("车位状态")
map_layout = QVBoxLayout()
self.slot_map = SlotMapWidget(4)
map_layout.addWidget(self.slot_map)
map_group.setLayout(map_layout)
layout.addWidget(map_group)
# 进出记录表格
record_group = QGroupBox("进出记录")
record_layout = QVBoxLayout()
self.record_table = QTableWidget(0, 5)
self.record_table.setHorizontalHeaderLabels(
["时间", "事件", "卡号", "停车时长", "费用(元)"])
self.record_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
record_layout.addWidget(self.record_table)
record_group.setLayout(record_layout)
layout.addWidget(record_group)
self.statusBar().showMessage("正在连接MQTT服务器...")
self.today_income = 0.0
def _init_mqtt(self):
self.mqtt_client = mqtt.Client(client_id="parking_pc")
self.mqtt_client.on_connect = self._on_connect
self.mqtt_client.on_message = self._on_message
try:
self.mqtt_client.connect_async(MQTT_BROKER, MQTT_PORT, 60)
self.mqtt_client.loop_start()
except Exception as e:
self.statusBar().showMessage(f"MQTT连接失败: {e}")
def _on_connect(self, client, userdata, flags, rc):
if rc == 0:
client.subscribe([(TOPIC_SLOTS, 1), (TOPIC_EVENT, 1)])
self.statusBar().showMessage("MQTT已连接 | 等待数据...")
def _on_message(self, client, userdata, msg):
try:
data = json.loads(msg.payload.decode())
if msg.topic == TOPIC_SLOTS:
slots = data.get("slots", [])
free = data.get("free", 0)
total = data.get("total", 4)
self.slot_map.update_slots(slots)
self.total_label.setText(f"总车位: {total}")
self.free_label.setText(f"空闲: {free}")
self.occupied_label.setText(f"占用: {total - free}")
elif msg.topic == TOPIC_EVENT:
self._add_record(data)
except json.JSONDecodeError:
pass
def _add_record(self, data):
event = data.get("event", "")
uid = data.get("uid", "")
fee = data.get("fee", 0)
duration = data.get("duration", 0)
event_text = {"entry": "进场", "exit": "出场",
"full": "拒绝(满)", "error": "读卡失败"}.get(event, event)
row = self.record_table.rowCount()
self.record_table.insertRow(0) # 最新记录在最上面
self.record_table.setItem(0, 0, QTableWidgetItem(
datetime.now().strftime("%H:%M:%S")))
self.record_table.setItem(0, 1, QTableWidgetItem(event_text))
self.record_table.setItem(0, 2, QTableWidgetItem(uid))
self.record_table.setItem(0, 3, QTableWidgetItem(
f"{duration}分钟" if duration > 0 else "-"))
self.record_table.setItem(0, 4, QTableWidgetItem(
f"¥{fee:.1f}" if fee > 0 else "免费"))
# 进场绿色,出场橙色
color = QColor(232, 245, 233) if event == "entry" else QColor(255, 243, 224)
for col in range(5):
self.record_table.item(0, col).setBackground(color)
if event == "exit" and fee > 0:
self.today_income += fee
self.income_label.setText(f"今日收入: ¥{self.today_income:.1f}")
def closeEvent(self, event):
self.mqtt_client.loop_stop()
self.mqtt_client.disconnect()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ParkingMonitorApp()
window.show()
sys.exit(app.exec_())
上位机核心是SlotMapWidget自定义组件,通过paintEvent绘制车位色块地图,绿色表示空闲、红色表示占用。进出记录表格按时间倒序排列,出场记录自动累加今日收入。
五、调试与优化
5.1 常见问题与解决方案
超声波测距不准确
现象: 距离值跳变大,或固定偏差。
解决方案:
- 确认传感器安装角度垂直于地面,倾斜会导致回波偏弱
- 避免相邻传感器同时触发,依次测量间隔60ms以上
- 增大防抖确认次数(从3次改为5次),牺牲响应速度换取稳定性
- 对于固定偏差,在初始化时测量空车位基准距离,运行时用相对值判断
RFID读卡距离短
现象: IC卡必须紧贴模块才能读取。
解决方案:
- RC522天线焊接质量检查,虚焊会严重影响读卡距离
- 模块供电必须稳定3.3V,电压波动会降低射频功率
- 远离金属表面安装,金属会屏蔽射频信号
- 软件层面增加连续读卡重试(3次),提高成功率
舵机抖动
现象: 舵机到达目标角度后持续微小抖动。
解决方案:
/* 到达目标角度后关闭PWM输出,消除抖动 */
void Servo_SetAngle_Stable(uint8_t angle)
{
uint16_t pulse = 500 + (uint16_t)(angle * 2000.0f / 180.0f);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, pulse);
HAL_Delay(500); // 等待舵机到位
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0); // 关闭PWM
}
到位后将PWM占空比设为0,舵机停止接收信号后会保持当前位置且不再抖动。注意这种方式下舵机没有保持力矩,适合道闸这种无外力负载的场景。
5.2 性能优化
多车位扩展方案
当车位数量超过4个时,单片机GPIO不够用,可以采用以下扩展方案:
| 方案 | 适用规模 | 优缺点 |
|---|---|---|
| 74HC595移位寄存器 | 8-16车位 | 3个GPIO扩展8路Trig,Echo仍需独立引脚 |
| I2C GPIO扩展(PCF8574) | 8-32车位 | 每片扩展8路,可级联4片,但I2C速度限制测距频率 |
| 分布式方案(RS485总线) | 32-200车位 | 每个节点管理4-8个车位,主控通过Modbus轮询,适合大型停车场 |
掉电数据保护
停车记录存储在RAM中,掉电会丢失。生产环境建议:
- 关键数据(在场车辆记录)定期写入Flash或外部EEPROM
- 使用超级电容(1F/5.5V)提供掉电保持时间,检测到掉电后紧急保存
- 上位机端做数据库持久化,STM32重启后从上位机同步恢复
六、项目总结与扩展
6.1 项目成果
本项目实现了一套完整的智能停车场管理系统,覆盖车位检测、身份识别、自动计费、道闸控制和远程监控全流程:
- 4路超声波车位检测,3次确认防抖,状态变化实时上报
- RFID刷卡自动判断进出场,15分钟免费,按小时阶梯计费
- 舵机道闸自动控制,抬杆后5秒自动落杆
- PyQt5上位机车位地图可视化,进出记录管理,收入统计
6.2 关键技术点
| 技术点 | 实现方案 | 设计考量 |
|---|---|---|
| 车位检测 | 超声波测距+阈值判断 | 非接触式,成本低,安装方便 |
| 状态防抖 | 连续3次确认机制 | 防止行人/飞鸟误触发 |
| 身份识别 | RC522 RFID读卡 | 响应快,成本低,适合闸机场景 |
| 自动计费 | 进出场时间差+阶梯费率 | 15分钟免费,按小时向上取整 |
| 道闸控制 | PWM舵机+自动落杆 | 到位后关PWM消除抖动 |
| 远程监控 | MQTT状态变化上报 | 仅变化时上报,减少流量 |
6.3 扩展方向
- 车牌识别: 入口加装摄像头,基于OpenMV或ESP32-CAM实现车牌OCR,替代RFID实现无感通行
- 移动支付: 对接微信/支付宝扫码支付,出口扫码自动扣费抬杆
- 车位引导: 每个车位上方安装LED指示灯(红/绿),引导驾驶员快速找到空位
- 大屏显示: 停车场入口LED大屏显示剩余车位数,满位时显示"车位已满"
- 数据分析: 接入数据库,分析高峰时段、平均停车时长、车位周转率,辅助运营决策
如果这篇文章对您有帮助,请点赞支持!如有问题或建议,欢迎在评论区交流。关注我获取更多嵌入式物联网项目实战内容。
更多推荐
所有评论(0)