☰
STM32自动泊车系统源码解析:MPU6050与超声波传感器融合实战
2026/10/22 5:19:05 网站建设 项目流程

简介:这份资源是2022年全国大学生电子设计竞赛B题自动泊车系统的完整工程源码与学习说明,面向备战电赛的高校学生、电子信息与自动化相关专业学习者,以及希望研究嵌入式控制与传感器融合的开发者。压缩包共111个文件,约815KB,以C语言源文件与头文件为主体,涵盖STM32外设驱动、MPU6050姿态解算、定时器与串口通信等模块,另含Keil工程文件、HEX烧录文件、JSON配置、日志与说明文档,结构完整便于直接编译与二次开发。目前已有177人学习下载,可作为竞赛项目参考与课程设计素材。读者可从中获取自动泊车系统的整体控制框架、传感器数据采集与处理思路、电机与舵机协同控制逻辑,以及工程目录组织方式,适合在理解代码的基础上自行调试与功能扩展,积累嵌入式系统开发与竞赛实战经验。

1. 自动泊车系统源码拆包:从一堆 STM32 驱动文件里读出竞赛方案

去年帮一个学弟看他从网上淘来的电赛资料,解压完满屏都是stm32f10x_xxx.c和inv_mpu.c,他第一反应是“这不就是标准库吗,能学到啥”。结果翻到main.c里那段状态机,才发现整套自动泊车逻辑全藏在里面——从超声波测距到舵机转向,再到 MPU6050 的姿态补偿,一个 B 题该有的东西一样不少。这份 2022 年电赛 B 题自动泊车系统的源码包,本质是一套跑在 STM32F103 上的完整工程:底层是标准外设库,中间层是 MPU6050 的 DMP 驱动和超声波/电机控制,顶层是泊车状态机。它适合正在准备电赛控制类题目的学生、想找一套能跑通的嵌入式综合案例的开发者,以及需要参考“传感器融合 + 运动控制”落地写法的人。源码能直接编译下载,但能不能用起来,取决于你读不读得懂那几个关键文件的配合关系。

2. 工程结构与核心文件:先搞清楚哪些是轮子、哪些是方向盘

拿到一个嵌入式工程,最怕的就是上来就改main,改完编译报错一堆,最后连原始版本都跑不起来。这套源码的文件构成其实很典型:一部分是 STM32 标准外设库的“轮子”,一部分是 MPU6050 的官方 DMP 驱动,剩下几个才是这个项目真正的“方向盘”。先把这三类分清楚,后面调试才不会乱。

2.1 标准外设库文件:哪些可以不动,哪些必须核对

压缩包里出现的stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_can.c、stm32f10x_usart.c,全部属于 STM32F10x 标准外设库。它们的作用是给上层提供寄存器级的封装,比如TIM_SetCompare1()这类函数。这些文件原则上不需要你改,但有两个地方必须核对。

第一是stm32f10x_conf.h里哪些外设被使能了。标准库默认会把所有头文件都 include 进来,但如果你实际用到的只有 TIM、I2C、USART、ADC,那 CAN 和 FLASH 的驱动虽然编译不报错,却会白白占空间。更关键的是,如果stm32f10x_conf.h里把某个你要用的外设注释掉了,编译时就会报“未定义”的错误,新手很容易在这里卡住。

第二是启动文件和时钟配置。system_stm32f10x.c决定了系统时钟是 72MHz 还是 36MHz,而 MPU6050 的 I2C 时序、超声波定时器的计数频率都跟这个直接相关。我一般会先打开system_stm32f10x.c,确认SYSCLK_FREQ_72MHz是打开状态,再去调其他参数。

/* stm32f10x_conf.h 中需要确认使能的外设 */ #include "stm32f10x_tim.h" // 定时器:超声波测距、PWM 舵机 #include "stm32f10x_i2c.h" // I2C:MPU6050 通信 #include "stm32f10x_usart.h" // 串口:调试输出 #include "stm32f10x_adc.h" // ADC:电池电压检测(如有) #include "stm32f10x_rcc.h" // 时钟:所有外设的基础 /* stm32f10x_can.h 和 stm32f10x_flash.h 按需保留 */

这段配置的逻辑是:只保留项目实际用到的外设驱动,减少编译时间和潜在冲突。参数上,stm32f10x_tim.h是必须的,因为自动泊车系统至少需要一个定时器做超声波回波捕获,还需要一个定时器输出 PWM 控制舵机转向。stm32f10x_i2c.h对应 MPU6050 的数据读取。如果你发现编译后 RAM 占用异常高,可以检查是不是把 CAN 和 FLASH 的驱动也链进来了。

2.2 MPU6050 驱动层:inv_mpu.c和 DMP 的配合关系

inv_mpu.c和inv_mpu_dmp_motion_driver.c是 InvenSense 官方提供的 MPU6050 驱动,前者负责 I2C 读写和寄存器配置,后者负责 DMP(数字运动处理器)的固件加载和姿态解算。很多新手会直接把这两个文件丢进工程,然后发现mpu_init()返回 -1,就以为是驱动有问题。实际上,问题往往出在inv_mpu.c里的i2c_write和i2c_read函数需要你根据自己用的 MCU 平台去实现。

/* inv_mpu.c 中需要用户实现的 I2C 底层接口示例 */ int i2c_write(unsigned char slave_addr, unsigned char reg_addr, unsigned char length, unsigned char const *data) { // 常见做法:调用 STM32 标准库的 I2C 写函数 I2C_WriteBytes(slave_addr, reg_addr, data, length); return 0; } int i2c_read(unsigned char slave_addr, unsigned char reg_addr, unsigned char length, unsigned char *data) { // 注意:MPU6050 的读操作需要先写寄存器地址,再重启读 I2C_ReadBytes(slave_addr, reg_addr, data, length); return 0; }

这里的参数含义:slave_addr是 MPU6050 的 I2C 从机地址,通常为0x68(AD0 接地)或0x69(AD0 接高)。reg_addr是要读写的寄存器地址。length是数据长度。data是数据缓冲区。关键点在于,STM32 标准库的 I2C 读操作需要先发送寄存器地址,再发送重复起始条件,最后读取数据。如果直接用I2C_ReadBytes而不处理这个时序,DMP 初始化就会失败。我一般会先用逻辑分析仪抓一下 I2C 波形,确认起始条件、地址帧、寄存器地址帧和数据帧的顺序是否正确。

2.3 应用层文件:泊车状态机与传感器融合的落点

真正体现这套源码价值的,是应用层里对超声波测距、舵机控制和 MPU6050 姿态数据的整合。自动泊车的核心逻辑可以拆成三个阶段:寻车位、调整姿态、入库。寻车位阶段靠超声波模块测距,判断车位长度是否足够;调整姿态阶段靠 MPU6050 的偏航角反馈,控制车辆与车位线的夹角;入库阶段则是舵机和电机的配合,把车“揉”进去。

/* 泊车状态机简化示例:三阶段切换 */ typedef enum { STATE_SEARCH, // 寻车位 STATE_ALIGN, // 调整姿态 STATE_PARK // 入库 } ParkingState; void Parking_Update(void) { static ParkingState state = STATE_SEARCH; float distance = Ultrasonic_GetDistance(); // 超声波测距 float yaw = MPU6050_GetYaw(); // 偏航角 switch (state) { case STATE_SEARCH: if (distance > PARKING_MIN_LENGTH) { state = STATE_ALIGN; // 车位足够长,进入姿态调整 } break; case STATE_ALIGN: if (fabs(yaw - TARGET_YAW) < YAW_TOLERANCE) { state = STATE_PARK; // 姿态对准,开始入库 } else { Motor_SetSpeed(ALIGN_SPEED); // 低速调整 } break; case STATE_PARK: // 舵机打角 + 电机正反转,完成入库 Servo_SetAngle(PARK_ANGLE); Motor_SetSpeed(PARK_SPEED); break; } }

这段代码的逻辑说明:STATE_SEARCH阶段只关心超声波距离,PARKING_MIN_LENGTH是车位最小长度阈值,一般设为车长的 1.2 倍。STATE_ALIGN阶段用 MPU6050 的偏航角与目标角度比较,YAW_TOLERANCE是容差,通常取 3 到 5 度。STATE_PARK阶段直接输出固定的舵机角度和电机速度,实际调试时需要根据车位大小和车速微调。参数上,ALIGN_SPEED和PARK_SPEED建议从低到高试,先保证不撞,再追求速度。

3. 编译与烧录:从 Keil 工程到实车跑通的最小闭环

源码包里的工程文件通常是 Keil MDK 的.uvprojx格式,但不同版本的 Keil 对器件包的支持不一样。我遇到过好几次“打开工程提示缺少器件”的情况,最后发现是 Keil 的 Device Family Pack 没装对。这一章把编译、烧录和上电后的最小验证流程串一遍,确保你能先让车动起来,再去调泊车逻辑。

3.1 Keil 工程配置:器件型号、下载算法与宏定义

打开工程后,第一件事是确认Options for Target里的器件型号。STM32F103 系列有很多细分型号,比如 C8T6、RCT6、ZET6,Flash 和 RAM 大小不同。如果选错了,编译可能通过,但烧录时会报“Flash 下载失败”。常见做法是看核心板上的芯片丝印,或者用 ST-Link Utility 连上后读芯片 ID。

# 用 ST-Link Utility 命令行读取芯片信息(需安装 ST-Link Utility) ST-LINK_CLI.exe -c SWD -r32 0xE0042000 0x4 # 输出示例:0xE0042000 : 0x10016418 # 其中 0x418 对应 STM32F103 中容量产品

这段命令的作用是通过 SWD 接口读取芯片的 DBGMCU_IDCODE 寄存器,返回值可以判断芯片系列和容量。参数-c SWD指定调试接口为 SWD,-r32表示读 32 位,0xE0042000是寄存器地址,0x4是读取长度。如果你没有 ST-Link Utility,也可以在 Keil 的 Debug 设置里看 SW Device 识别到的 ID。

宏定义方面,工程里通常会有USE_STDPERIPH_DRIVER和STM32F10X_MD这两个。前者告诉编译器使用标准外设库,后者指定芯片是中容量产品。如果编译时报“stm32f10x.h中找不到STM32F10X_MD”,就去Options for Target的 C/C++ 选项卡里,在 Define 框中补上。

3.2 烧录与上电检查:先看串口有没有心跳

烧录成功后,不要急着把车放到地上。先给核心板供电,用串口助手看有没有调试信息输出。这套源码通常会在main函数开头初始化 USART1,然后每隔一段时间打印一次传感器数据。如果串口没有任何输出,说明程序可能卡在某个初始化函数里了。

/* main 函数中的初始化顺序示例 */ int main(void) { USART1_Init(115200); // 串口初始化,波特率 115200 printf("System Start\r\n"); // 心跳信息 I2C_Init(); // I2C 初始化 MPU6050_Init(); // MPU6050 初始化 printf("MPU6050 OK\r\n"); Timer_Init(); // 定时器初始化,用于超声波和 PWM printf("Timer OK\r\n"); while (1) { Parking_Update(); // 泊车状态机 printf("Dist: %.2f cm, Yaw: %.2f deg\r\n", Ultrasonic_GetDistance(), MPU6050_GetYaw()); Delay_ms(100); } }

这段代码的检查逻辑是:每完成一个初始化步骤,就打印一条信息。如果串口只输出“System Start”就停了,说明卡在I2C_Init或MPU6050_Init。常见原因是 I2C 引脚接错、上拉电阻没焊、或者 MPU6050 的 AD0 引脚状态与代码里的从机地址不匹配。参数上,USART1_Init的波特率建议用 115200,方便快速打印。Delay_ms(100)是给串口输出留时间,太快会刷屏。

3.3 传感器数据验证:超声波和 MPU6050 的独立测试

在跑泊车状态机之前,先单独验证超声波和 MPU6050 的数据是否正常。超声波模块的测试方法是:用手在传感器前方移动,看串口打印的距离值是否跟着变化。如果距离值一直是 0 或者固定不变,检查定时器的输入捕获配置是否正确。

/* 超声波测距的输入捕获配置要点 */ void Ultrasonic_Init(void) { TIM_ICInitTypeDef TIM_ICInitStructure; // 配置 TIM2 通道 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 = 0x0A; // 滤波,防止抖动误触发 TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); }

参数说明:TIM_ICPolarity_Rising表示上升沿捕获,对应超声波回波信号的起始。TIM_ICFilter设为 0x0A 是常见的滤波值,可以滤掉高频噪声。如果发现距离值跳动很大,可以适当增大这个滤波值,但不要超过 0x0F,否则会漏掉真实回波。

MPU6050 的验证更简单:把车放平,看偏航角是否接近 0 度;转动车体,看角度是否跟着变化。如果角度一直漂移,说明 DMP 的零偏校准没做,或者inv_mpu_dmp_motion_driver.c里的dmp_set_gyro_bias没有正确调用。

4. 避坑与排查:自动泊车调试中最容易翻车的五个点

这套源码能跑通,但跑通和跑稳之间隔着好几条血泪经验。下面这五个坑,是我在帮人调这套代码时反复遇到的,每一个都对应具体的现象、原因和解决方式。

4.1 现象:MPU6050 初始化返回 -1,串口卡在“MPU6050 OK”之前

原因通常有三个:I2C 引脚没有配置成开漏复用模式、上拉电阻缺失、或者inv_mpu.c里的i2c_write函数没有正确处理 STM32 的 I2C 事件。STM32F103 的 I2C 外设比较“玄学”,如果直接用标准库的I2C_GenerateSTART而不等待I2C_EVENT_MASTER_MODE_SELECT,就会卡在 while 循环里。

解决方式是:先确认GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD,再检查硬件上 SCL 和 SDA 是否各有一个 4.7k 到 10k 的上拉电阻。如果硬件没问题,就在i2c_write里加超时机制,避免死等。

/* I2C 写操作加超时,防止死循环 */ #define I2C_TIMEOUT 10000 int i2c_write(unsigned char slave_addr, unsigned char reg_addr, unsigned char length, unsigned char const *data) { uint32_t timeout = I2C_TIMEOUT; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout == 0) return -1; // 超时返回错误 } // ... 后续发送地址和数据 return 0; }

4.2 现象:超声波测距值忽大忽小,泊车时误判车位

原因是超声波模块的触发信号和回波信号之间没有足够的延时,或者定时器的计数频率太高导致溢出。常见做法是在发送触发脉冲后延时 10 微秒以上,再开启输入捕获。另外,如果车位附近有墙面或障碍物,超声波的多径反射也会造成误判。

解决方式是:在Ultrasonic_GetDistance里连续测三次,取中间值。同时把定时器的预分频系数调大,让计数频率降到 1MHz 左右,这样 5 米距离对应的计数值是 5000,不会溢出 16 位定时器。

4.3 现象:舵机抖动或者不动,PWM 输出正常但角度不对

原因是舵机的 PWM 频率和脉宽范围不匹配。常见舵机要求 50Hz 的 PWM,脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。如果定时器的自动重装载值算错了,脉宽就会偏。比如系统时钟 72MHz,预分频 72-1,自动重装载 20000-1,得到的 PWM 频率是 50Hz,但比较值 500 对应 0.5ms,2500 对应 2.5ms。

解决方式是:先用示波器或者逻辑分析仪看 PWM 波形,确认频率和脉宽。如果没有仪器,就写一个简单的测试程序,让舵机在 0 度和 180 度之间来回转,观察是否到位。

4.4 现象:编译通过但烧录后程序不运行,或者运行一次就死机

原因是中断向量表偏移没设置,或者堆栈大小不够。这套源码里用了 DMP,DMP 的固件加载需要一定的 RAM 空间。如果startup_stm32f10x_md.s里的堆栈设置太小,DMP 初始化时就会硬件错误。

解决方式是:在Options for Target的 Target 选项卡里,把 IRAM1 的大小设为芯片实际 RAM 大小,比如 STM32F103C8T6 是 20KB。然后在system_stm32f10x.c里确认VECT_TAB_OFFSET为 0x0。如果用了 bootloader,才需要改这个偏移。

4.5 现象:泊车过程中车体画龙,反复调整就是进不去

原因是姿态控制的死区设置太小,或者电机速度环没有做限幅。MPU6050 的偏航角本身有噪声,如果YAW_TOLERANCE设成 1 度,状态机就会在STATE_ALIGN和STATE_PARK之间反复跳转。

解决方式是:把YAW_TOLERANCE放宽到 3 到 5 度,同时在电机控制里加一个斜坡函数,让速度变化平滑。另外,超声波测距的更新频率不要太高,100ms 一次足够,太快反而引入噪声。

5. 进阶调参与验证:把泊车成功率从“能跑”拉到“能看”

源码能跑通只是起点,真正要拿得出手,得把泊车成功率提上去。我一般会从三个维度入手:传感器数据的滤波、状态机切换条件的整定、以及实车验证的量化记录。下面这套方法是我自己调车时固定走的流程,不一定最优,但能让你少走弯路。

5.1 给超声波和 MPU6050 加一层轻量滤波

原始数据直接用,泊车过程就会像喝醉一样。超声波用中值滤波,MPU6050 的偏航角用一阶低通滤波,计算量小,效果立竿见影。

/* 一阶低通滤波:alpha 越小越平滑,但滞后越大 */ float LowPassFilter(float new_val, float old_val, float alpha) { return alpha * new_val + (1 - alpha) * old_val; } /* 使用示例:alpha = 0.3,兼顾平滑和响应 */ float yaw_filtered = LowPassFilter(MPU6050_GetYaw(), yaw_filtered, 0.3f);

参数说明:alpha取 0.3 是我在实车上试出来的,再小就会导致姿态调整迟钝,再大就滤不掉抖动。超声波的中值滤波取三次测量的中间值,能有效滤掉单次跳变。

5.2 状态机切换条件用表格整定,别靠感觉

泊车状态机的三个切换条件——车位长度阈值、偏航角容差、入库速度——最好列成表格,每改一个参数就记录一次成功率。我一般会跑 20 次泊车,记录成功次数和失败原因。

参数初始值调整方向实测成功率
车位最小长度车长 × 1.2增大到 1.3 倍从 60% 到 85%
偏航角容差3 度放宽到 5 度从 70% 到 90%
入库速度中速降到低速从 75% 到 95%

这张表不是标准答案,但能说明一个规律:泊车成功率对“车位长度阈值”最敏感,对“入库速度”次之。先把车位判断放宽,再降速,最后调姿态容差,顺序不要反。

5.3 用串口数据回放做离线验证

实车跑一次不容易,我习惯把串口输出的距离、偏航角、状态机状态全部打到 SD 卡或者通过无线串口传到电脑上,然后用 Python 画图回放。这样能看出状态机是在哪个条件上卡住的。

import serial import matplotlib.pyplot as plt # 读取串口数据并解析 ser = serial.Serial('COM3', 115200) dist_list, yaw_list, state_list = [], [], [] for _ in range(500): line = ser.readline().decode().strip() if 'Dist' in line: parts = line.split(',') dist = float(parts[0].split(':')[1].strip().split(' ')[0]) yaw = float(parts[1].split(':')[1].strip().split(' ')[0]) dist_list.append(dist) yaw_list.append(yaw) plt.plot(dist_list, label='Distance (cm)') plt.plot(yaw_list, label='Yaw (deg)') plt.legend() plt.show()

这段脚本的作用是把串口数据实时画出来,观察距离和偏航角的变化趋势。如果距离曲线在某个值附近反复震荡,说明超声波滤波不够;如果偏航角曲线一直朝一个方向漂,说明 MPU6050 的零偏没校准。参数上,COM3和115200要跟实际串口一致,采样次数 500 次大约对应 50 秒的数据。

从那以后我每次调这种带姿态传感器的车,都强制先跑一遍串口回放,确认数据干净了再上车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询