简介:基于STM32单片机的智能停车场车位管理系统毕设源码,主要面向计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者,适用于课程设计、大作业和毕业设计参考。源码以C语言编写,涵盖标准外设库、系统初始化、中断处理及车位管理逻辑等模块,整体采用CORE、FWLIB、USER等经典STM32工程目录结构,并附带Keil工程配置文件与说明文档,便于二次开发和调试。压缩包共69个文件,主要有30个头文件、27个C源文件,以及uvprojx/uvoptx工程文件、map/lst调试信息文件、README说明等,资源大小仅304KB,轻量而完整。目前已有784人学习/下载,项目答辩评审98分,全部代码均经过调试测试、可直接运行;既能帮助初学者快速上手STM32开发,也能为进阶者提供可扩展的停车场管理方案。
1. 基于 STM32 的智能停车场车位管理系统:先想清楚要采集什么
智能停车场车位管理系统在毕业设计里的出现频率很高,但动手时卡住的往往是同一件事:红外对管接上去,手一挡电平就变,人一走又偶发误检;OLED 能显示字符时,串口助手却收不到一条完整数据。这套系统的技术栈并不深,STM32 负责采集车位状态并驱动显示与道闸,传感器选型和数据协议才是拉开代码质量的关键。下面按一条完整源码链路来拆:车位检测单元的选型与滤波、OLED 与道闸外设的状态机、串口协议与上位机联调,最后落到底板烧录排错和答辩验收。适合正在做 STM32 单片机课程设计或毕业设计、想把代码从“能跑”提升到“能答辩”的读者。
2. 车位检测单元选型与数值滤波:把电平变成可靠状态
2.1 车位占用检测的四种方案怎么选
先看传感器,因为它的输出形式直接决定了后续用 GPIO 还是 ADC。常用的检测单元是这几类:
| 传感器 | 检测对象 | 输出形式 | MCU 接口 | 典型代价 |
|---|---|---|---|---|
| E18-D80NK 红外对管 | 近距离障碍物/车辆 | 数字电平 | GPIO 直读 | 便宜响应快,对黑车和强日光敏感 |
| HC-SR501 人体红外 | 移动的人 | 数字电平 | GPIO 直读 | 检测的是人不是车,不能直接判占用 |
| 地磁/干簧管 | 铁质车身 | 电平/开关 | GPIO | 需要埋点施工,电机线干扰要处理 |
| HC-SR04 超声波 | 障碍物距离 | 10us 脉冲+回波 | 定时器输入捕获 | 距离连续,可区分行人与车辆,代码量最大 |
| OpenMV/K210 视觉 | 车位线/车牌 | UART/SPI | 串口/SPI | 工作量高,适合做创新展示 |
选型逻辑要落在“毕设到底想展示什么”上。如果目标是短时间内让整体流程跑通,红外对管是默认选择,成本低且电平直读,四个车位就用四路 GPIO;如果想在答辩里讲出“距离阈值”这种可量化指标,用 HC-SR04 加定时器输入捕获,它是连续量,回波脉宽换算成距离后再判占用,比红外多一个维度。HC-SR501 不建议用于车位检测,它判断的是有没有人活动,把车停在那里静止不动时它反而不触发。
2.2 用 STM32CubeMX 初始化 GPIO 与 ADC 通道
常见做法是用 STM32CubeMX 生成工程骨架,再往业务层填代码。以 STM32F103C8T6 为例:RCC 选中 HSE,SYS 里把 Debug 设为 Serial Wire,否则 ST-LINK 下载成功后第二次连接容易报 Target Missing;占用检测引脚配置为 GPIO_Input,内部上拉还是下拉,要看模块在“无车”时的默认电平,E18-D80NK 测到目标时输出低电平,建议开内部上拉,并在代码注释里写清楚逻辑关系。
#define SENSOR_NUM 4 static GPIO_TypeDef* sensor_port[SENSOR_NUM] = {GPIOA, GPIOA, GPIOB, GPIOB}; static const uint16_t sensor_pin[SENSOR_NUM] = { GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_0, GPIO_PIN_1 }; /* 读取一个车位原始电平,返回 1 表示占用 */ static uint8_t sensor_raw(uint8_t idx) { return HAL_GPIO_ReadPin(sensor_port[idx], sensor_pin[idx]) == GPIO_PIN_SET; }sensor_port 和 sensor_pin 两张表把“车位编号”和“引脚”解耦,后续增加车位只改数组,不动判定逻辑。HAL_GPIO_ReadPin 返回的是总线实时状态,引脚一旦悬空会读到随机值,所以这里只是原料,不能直接拿去驱动业务。如果用了模拟量传感器,比如光敏对管,就需要在 CubeMX 里打开 ADC1 对应通道,在主循环里做多次采样取平均。
static uint16_t adc_read_avg(uint8_t channel, uint8_t times) { uint32_t sum = 0; for (uint8_t i = 0; i < times; i++) { ADC_ChannelConfTypeDef cfg = {0}; cfg.Channel = channel; cfg.Rank = ADC_REGULAR_RANK_1; HAL_ADC_ConfigChannel(&hadc1, &cfg); HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); sum += HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); } return (uint16_t)(sum / times); }采样次数 times 建议取 50,在 12 位 ADC 下能把纹波压到 2~3 个 LSB。这里的代价是单次采样约耗时 10ms,50 次就是 500ms 左右,所以不能用阻塞方式放在主循环里,应该放进一个低优先级的周期任务,或者改用 DMA 循环采集。
2.3 滑动窗口滤波与去抖阈值设定
直接在引脚电平上做判定的典型问题:车灯反光、手掌虚晃、电机启动瞬间的电磁干扰,都会产生几毫秒到几十毫秒的毛刺。常见做法是固定 5ms 采样一次,连续 8 次组成窗口,窗口内高电平次数达到 6 次以上才算占用。
static uint8_t raw_history[SENSOR_NUM]; /* 放在 5ms 周期任务里调用 */ void sensor_filter_periodic(void) { static uint8_t history[SENSOR_NUM][8]; static uint8_t pos[SENSOR_NUM]; for (uint8_t i = 0; i < SENSOR_NUM; i++) { history[i][pos[i]] = sensor_raw(i); pos[i] = (pos[i] + 1) & 0x07; uint8_t set = 0; for (uint8_t j = 0; j < 8; j++) { if (history[i][j]) set++; } raw_history[i] = (set >= 6) ? 1 : 0; } }参数说明:8 次乘 5ms 等于 40ms 的判定窗口,响应速度和可靠性都能接受;set >= 6 表示 75% 的采样点持续为占用才确认。现场如果出现占用后偶尔闪回空闲,把阈值提到 7;觉得 40ms 太迟钝,就把窗口缩到 4 次、阈值保持 3。阈值不能设成 0 或 8,前者完全没有滤波意义,后者等于要求连续 8 次全中,一次毛刺就误判。多车位信息建议在这里收敛成一个 uint16_t 位图,每个 bit 代表一个车位,后续 OLED、上报都用这份位图,不要到处读 GPIO,否则“句柄满天飞”最容易引入计数不一致。
3. OLED 显示与道闸控制:用状态机管理智能停车场的现场外设
3.1 为什么外设控制要拆成状态机而不是写在 if 里
一个典型面板上同时存在 OLED 剩余车位、每车位红绿灯、道闸舵机和蜂鸣器。如果主循环里先判断传感器再直接驱动外设,多车位并发时会出问题:A 车位进车时闸机正在开,B 车位恰好又变化,两个 if 的先后顺序会直接影响显示刷新和计数结果。更可靠的做法是把道闸这类执行机构抽象成状态机,传感器只产生“事件”,不直接控制输出。
3.2 道闸状态机定义与迁移
| 状态 | 进入条件 | 执行动作 | 超时 |
|---|---|---|---|
| GATE_IDLE | 复位或关闭完成 | LED 显示空闲 | 无 |
| GATE_OPENING | 任一车位由空变占 | 指示灯变红、舵机转 90° | 2.5s |
| GATE_OPEN | 开闸到位 | 保持舵机角度 | 5s |
| GATE_CLOSING | 保持超时 | 舵机回 0° | 5s |
typedef enum { GATE_IDLE = 0, GATE_OPENING, GATE_OPEN, GATE_CLOSING, } gate_state_t; static gate_state_t gate_state = GATE_IDLE; static uint16_t gate_tick = 0; /* 放在 20ms 主循环周期任务里调用 */ void gate_fsm_periodic(void) { switch (gate_state) { case GATE_IDLE: if (parking_bitmap != 0) { gate_state = GATE_OPENING; gate_tick = 0; servo_set_angle(90); /* 道闸抬杆 */ led_set_occupied(1); } break; case GATE_OPENING: if (++gate_tick >= 125) { /* 20ms * 125 = 2.5s */ gate_state = GATE_OPEN; gate_tick = 0; } break; case GATE_OPEN: if (++gate_tick >= 250) { /* 保持 5s */ servo_set_angle(0); /* 落杆 */ gate_state = GATE_CLOSING; gate_tick = 0; } break; case GATE_CLOSING: if (++gate_tick >= 250) { gate_state = GATE_IDLE; led_set_occupied(0); } break; } }gate_tick 的计数周期由调用方决定,代码里假设 20ms 调用一次,125 次就是 2.5 秒。用变量累加而不是 HAL_Delay 的原因很简单:Delay 会阻塞整个主循环,开闸的 5 秒内车位状态得不到更新,心跳帧也会断。实际工程里开闸到位不应该只靠延时,可以在舵机轴上加限位开关,到位后产生外部中断,状态机收到中断再进入 OPEN 状态。毕设里用延时替代能接受,但要在设计文档里注明这是简化处理。
3.3 OLED 显示剩余车位:I2C 地址与刷新策略
OLED 常见驱动是 SSD1306,I2C 七位地址为 0x3C,写地址是 0x78。CubeMX 生成 I2C1 后,显示逻辑封装成单独函数,参数由业务层传入:
void display_parking(uint16_t total, uint16_t free_slots) { char buf[32]; snprintf(buf, sizeof(buf), "Parking:%u", total); OLED_ShowString(0, 0, buf); snprintf(buf, sizeof(buf), "Free:%u", free_slots); OLED_ShowString(0, 2, buf); }total 和 free_slots 不要各自维护计数器,直接由 parking_bitmap 数 0 的个数得到,避免“入口 ++、出口 --”导致界面显示剩余 0 而位图还有空位的错位。刷新频率 500ms 一次足够,SSD1306 是整屏缓冲,400kHz I2C 下整帧刷新约几十毫秒,刷太频繁会挤占传感器采样窗口。
3.4 舵机 PWM 参数换算
道闸舵机标准频率是 50Hz,也就是周期 20ms。以 72MHz 主频为例,预分频 71 后定时器时钟 1MHz,ARR 设 19999 得到 20ms 周期,角度通过比较值 CCR 调节:
| 舵机角度 | 高电平脉宽 | 占空比 | ARR=19999 时比较值 |
|---|---|---|---|
| 0° | 0.5ms | 2.5% | 500 |
| 90° | 1.5ms | 7.5% | 1500 |
| 180° | 2.5ms | 12.5% | 2500 |
注意不同舵机的脉宽范围有差异,以型号手册为准。比较值只动 CCR,ARR 固定保证 50Hz。舵机抖动时优先查电源电流,USB 口供电经常在拉杆瞬间掉压重启。
4. 串口上报与上位机通信:车位管理系统的数据链路
4.1 用 ESP8266 的 AT 指令建立网络链路
数据链路可以分成三层:采集层只负责把传感器电平洗干净,事件层维护 parking_bitmap 和 free_slots,传输层每次只发一帧完整数据。很多毕设源码把这三层写进同一个循环,检测、显示、上报互相打断,最后哪一层都不干净。最常见的联网方案是 STM32 串口接 ESP8266,让模块把串口数据接入 TCP 连接。接模块之前先用 USB-TTL 单独调试,避免把串口接错的问题算到模块头上。
| 步骤 | 指令 | 期望返回 |
|---|---|---|
| 1 | AT | OK |
| 2 | AT+CWMODE=1 | OK |
| 3 | AT+CWJAP="ssid","pwd" | WIFI CONNECTED,OK |
| 4 | AT+CIPSTART="TCP","192.168.1.88",8080 | CONNECT,OK |
| 5 | AT+CIPSEND=6 | OK,然后接收数据 |
AT 指令必须以 \r\n 结尾,返回包是 \r\nAT\r\n\r\nOK\r\n 的形式,匹配时用 strstr 查 "OK",不能用等值比较。完整的 AT 状态推进可以写成一个小型状态机:
static uint8_t at_step = 0; static const char* at_cmds[] = { "AT\r\n", "AT+CWMODE=1\r\n", "AT+CWJAP=\"myssid\",\"mypwd\"\r\n", "AT+CIPSTART=\"TCP\",\"192.168.1.88\",8080\r\n", NULL }; /* 串口完整收到一行后调用,line 以 '\0' 结尾 */ void handle_at_response(const char* line) { if (at_step < 4 && strstr(line, "OK")) { at_step++; if (at_cmds[at_step]) { uart_send_string(at_cmds[at_step]); } } }这段代码只处理成功路径,实践里还要加超时重发:每发送一条指令后启动一个 3 秒定时器,没有收到 OK 就重发当前指令,连续三次失败直接报错并停止联网流程,不能阻塞车位检测主循环。
4.2 自定义数据帧格式与打包代码
用 printf 直接发 “free=3” 这种文本会有边界问题:AT 响应和业务文本混在同一条串口线里,上位机无法确认数据从哪开始。自定义帧是更可靠的做法:
| 帧头 | 数据长度 | 车位位图 | 剩余车位 | 校验和 | 帧尾 |
|---|---|---|---|---|---|
| 0xA5 | 1 字节 | 2 字节 | 1 字节 | 1 字节 | 0x5A |
uint8_t frame[8]; void build_report_frame(uint16_t bitmap, uint8_t free_slots) { frame[0] = 0xA5; frame[1] = 5; /* 位图2 + free1 + sum1 + 帧尾1 */ frame[2] = (uint8_t)(bitmap & 0xFF); frame[3] = (uint8_t)(bitmap >> 8); frame[4] = free_slots; frame[5] = frame[0] + frame[1] + frame[2] + frame[3] + frame[4]; frame[6] = 0x5A; }校验用累加和而不是 CRC,好处是解析端能直接看出哪一字节算错,便于两个串口助手对拍。位图按 bit 编码车位编号,上位机拿到 frame[2] 和 frame[3] 后先还原 bitmap,再数出剩余车位,与 frame[4] 对比,不一致说明下位机统计逻辑有问题。
上报节奏建议两条规则:车位状态变化时立即发一帧,平时每 2 秒发一帧心跳。心跳的意义是让上位机能显示“最后在线时间”,现场演示时上位机不刷新,先看心跳是否还来,能快速把问题定位到 WiFi 断连还是业务死循环。
4.3 本地联调:用串口助手当 TCP Server 验证
没有现成上位机时,一样能把协议链路验证完。电脑上开一个网络调试助手,选 TCP Server,端口 8080,ESP8266 用 AT+CIPSTART 连到电脑 IP,STM32 复位后助手窗口应收到以 0xA5 开头的一帧。Windows 防火墙首次会拦截入站端口,本地联调连不上 8080 先看防火墙,多半不是下位机代码问题。验证通过后再接真实上位机,能省掉大半天联调时间。
5. 源码移植、烧录排错与答辩验收技巧
5.1 拿到 STM32 工程后先查三件事
第一,Keil 设备型号与启动文件。F103C8T6 对应 STM32F103C8 和 startup_stm32f10x_md.s,把大容量型号的启动文件硬套进小容量芯片,体现为烧录成功但上电跑飞。第二,时钟配置。代码里的 HSE_VALUE 要和板子晶振一致,8M 晶振按 12M 写,串口波特率会差三分之一,OLED 刷新速度也变得奇怪。第三,CubeMX 里的 Debug 模式。默认 No Debug 会把 SWD 引脚释放成普通 IO,ST-LINK 第二次连接不上就是这个原因。
| 排错场景 | 首选检查项 |
|---|---|
| Error: No STM32 Target Found | Boot0 拉高断电重进系统 Bootloader,再查 SWDIO/SWCLK 是否接反 |
| Keil 打开工程报缺芯片包 | 安装 Keil.STM32F1xx_DFP 设备包 |
| 串口助手收到乱码 | 先查 HSE_VALUE,再确认波特率 115200 |
| 虚拟串口设备管理器感叹号 | 重装 USB-TTL 驱动,并核对 ST-LINK V2 驱动版本 |
5.2 现场验证时序的三个信号
不要只靠肉眼观察 OLED,用串口打印来做逻辑验证最简单:在 sensor_filter_periodic 里把判定结果通过调试串口发出,01 表示 1 号位占用,00 表示释放。先验证滤波逻辑,再接入 OLED 和上报,问题就缩小到某一层。有条件的话用示波器看舵机信号线脉宽,周期应约为 20ms,角度变化时高电平时间在 0.5ms 到 2.5ms 之间跳变,这个波形能直接证明定时器配置正确。
5.3 答辩演示参数与测试模式
预留一个手动测试模式:短按按键依次模拟 1 到 4 号车位占用,让状态机在一分钟内完整走一遍开闸、保持、关闸、复位。现场即使没有真实车辆,也能展示全流程,比临时找车挡传感器可靠得多。准备一张 A4 参数速查表,把关键参数写清楚:传感器窗口 8 次、周期 5ms、阈值 6,舵机 20ms 周期、0° 对应 500、90° 对应 1500,串口 115200 8N1,帧头 0xA5,累加和校验。答辩时现场改一个阈值参数,比如把 set >= 6 改成 set >= 4,再用手遮挡传感器,观察判定变快但更容易抖动,用实际现象解释这个取舍,比背套话更能说明源码是你自己调通的。
本文还有配套的精品资源,点击获取