简介一份基于STM32与OpenMV的自动泊车系统完整工程源自南航电赛校赛题目可作毕设或课程作业参考。项目以STM32为主控负责传感器数据采集、电机控制、决策运算并可能引入FreeRTOS或RT-Thread强化多任务调度OpenMV则完成图像采集、车位识别、目标追踪和视觉引导两者通过串行协议交互形成感知—决策—执行的闭环。压缩包共201个文件体积6.45MB以C语言源文件与头文件、Keil工程配置文件、OpenMV的视觉脚本与模型文件为主体附带文本说明、笔记文档及报告目录结构清晰便于对照工程理解系统设计。已有792人浏览学习。借助该工程可学习外设驱动、PWM调速、ADC采样、视觉识别算法部署、串口通信与多模块协同调试等关键技能尤其适合正在做毕业设计或备战电赛的同学作为二次开发的基础框架和排错参考。1. 自动泊车不是视觉难点是主控与视觉的配合难点毕设和课程作业里“基于 stm32 和 openmv 的自动泊车”几乎是控制方向最经典的组合题南航电赛校赛拿它当题目说明它覆盖了串口通信、图像识别、电机控制、状态机设计这门课的完整链路。很多同学一开始把精力放在 OpenMV 认车位线上真正跑起来才发现车位框找得再准车照样停不进去——误差出在 OpenMV 把目标坐标发给了 STM32STM32 却不知道怎么把两轮差速小车稳稳挪进库里。这套方案适合的人群很明确手里有 STM32F103 最小系统板和 OpenMV H7想在两三周内跑通差速转向自动入库而不是去折腾复杂的路径规划算法。2. 系统架构与硬件分工为什么是 stm32 加 openmv 而不是单板搞定2.1 视觉和主控分开开发时不用互相拖累自动泊车的核心矛盾是图像处理的实时性要求和电机控制的确定性要求是两种开发方式。OpenMV 的 MicroPython 环境写色块识别、直线检测只需要十几行代码但如果你让同一块主控去干同步采集编码器和控制 PWM中断优先级、内存分配和图像处理栈很容易互相干扰。常见做法是图像与决策放视觉端电机、编码器、超声波和状态机放 STM32两边只通过串口传小数据。这套分工还有一个额外好处毕设答辩时可以把 OpenMV 的屏幕画面单独展示也能把 STM32 的串口调试数据单独拉出来两个模块各有一张完整的“工作图”。如果你纠结要不要用 K210 换掉 OpenMV我的看法是 K210 算力确实强模型部署也灵活但 OpenMV 的 MicroPython 库对电赛这种“几天内出效果”的场景更顺手find_lines、find_blobs、AprilTag 都是现成的固件升级和串口交互有大量中文资料。学生项目最怕的不是性能不够是时间不够。2.2 硬件清单与接线规划以下是我做这个题目时常用的硬件组合照着买基本不会踩坑模块常用型号作用供电建议主控STM32F103C8T6 最小系统板运动控制、串口接收、泊车状态机5V USB 供电板载 3.3V LDO视觉模块OpenMV Cam H7 Plus图像采集、车位线识别、坐标发送独立 5V 1A不要和电机共电源电机驱动TB6612FNG 或 DRV8833两路电机正反转和调速电池 7.4V 输入输出接电机电机N20 带霍尔编码器减速电机 x2差速驱动与车速反馈由驱动板输出超声波HC-SR04入库后测距防止撞墙5V 供电Trig/Echo 接 STM32调试器ST-Link V2烧录和在线调试接电脑 USB接线时有两条硬规矩。第一所有模块的 GND 必须连在一起尤其是 OpenMV 和 STM32 之间不共地串口数据就是一堆乱码。第二OpenMV 的 UART 引脚和 STM32 的 USART 要交叉连接OpenMV 的 TX 接 STM32 的 RXOpenMV 的 RX 接 STM32 的 TX。别用 USB 转 TTL 模块去和 OpenMV 通信再转给 STM32多一层转换就多一层不可靠。2.3 串口协议帧设计一字节角度加异或校验OpenMV 和 STM32 之间只传“控制指令 偏差”不需要传图像。我一般用 5 字节定长帧帧头1帧头2指令数据校验0xAA0x550x01 停车 / 0x02 前进 / 0x03 泊入偏差角度 int8前四字节异或指令字节用 0x01、0x02、0x03 区分状态数据字节传的是车头相对车位中心线的横向偏差或者角度偏差范围 -127 到 127一字节足够。校验用异或而不是 CRC16因为串口帧短、干扰小而异或在校验失败时可以快速丢弃整帧不会把状态机卡死。这套协议用 STM32 的串口中断接收时代码逻辑非常清晰判定帧头、收满四字节、算校验、置标志位。2.4 控制周期与任务分配STM32 的主循环跑 20ms 一个控制周期包含编码器读取、PID 计算、PWM 输出OpenMV 端跑 50ms 一帧图像检测到目标就发一帧数据。两边用串口解耦之后视觉端偶尔掉一帧不会有问题ST32 侧只要在连续 200ms 没收到帧头时让车进入减速停止状态就不会冲出去。任务分配上串口接收用中断编码器计数用定时器的编码器模式PWM 用定时器通道输出主循环只做控制逻辑。这样中断里不会做浮点运算和延时主循环也不会因为视觉卡顿而失控。OpenMV 发过来的数据只是一个“目标角度”它不知道车当前速度是多少也不应该知道。3. 视觉端落地OpenMV 识别车位线与地面引导线3.1 固件选择与摄像头初始化OpenMV 到手第一件事是检查固件版本。H7 系列如果固件太老find_lines的参数行为和帧率表现会和 H7 新版固件差很多。在 OpenMV IDE 里点工具菜单升级到当前版本就行升级完记得重新插拔 USB。初始化摄像头时我习惯关闭自动增益和自动曝光因为泊车场景下光线随时在变自动曝光会让画面亮度来回跳图像检测的阈值就失效了。用固定曝光值虽然不能适应所有光线但至少行为可预测调试时容易复现。import sensor, image, time from pyb import UART # 初始化视觉传感器 sensor.reset() sensor.set_pixformat(sensor.RGB565) # 需要识别颜色用 RGB565 sensor.set_framesize(sensor.QQVGA) # 160x120帧率优先于分辨率 sensor.set_windowing((0, 0, 160, 120)) sensor.skip_frames(30) sensor.set_auto_gain(False) # 关自动增益防亮度突变 sensor.set_auto_exposure(False, exposure_us20000) # 固定曝光 20ms sensor.set_auto_whitebal(False) # 关白平衡保持颜色稳定 uart UART(3, 115200, timeout_char100)这里窗口分辨率用 160x120而不是 640x480原因是 OpenMV 的处理器资源有限分辨率越低帧率越高对移动中的小车来说20 帧每秒比单帧清晰更重要。曝光时间 20ms 在室内灯光下足够如果现场偏暗可以调到 30ms但帧率会下降需要自己权衡。3.2 用 find_lines 识别车位边框线自动泊车的车位线通常是白漆画出的矩形框。识别思路是在图像下半部分设置一个 ROI在这个区域内做直线检测再根据直线的角度和长度过滤出横向和纵向边线。find_lines返回的线段对象里有rho()和theta()分别代表线段的极坐标距离和角度这两个值可以直接用来算车头偏向角。# 只在车头前方区域检测车位线 roi (20, 40, 120, 70) lines img.find_lines(roiroi, threshold1200, theta_margin25, rho_margin25, seg_mode0) best_line None for line in lines: # 过滤掉太短和角度太偏的线段保留接近水平的边线 if line.length() 40 and (80 line.theta() 100 or line.theta() 10 or line.theta() 170): if best_line is None or line.length() best_line.length(): best_line linethreshold是梯度阈值数值越小检出的线越多噪声也越多1200 到 1500 在大多数实验室光线环境下够用。seg_mode0表示把断开的线段连接成完整直线seg_mode1表示把线段作为独立段返回我推荐 0因为车位线经常被车轮压花或者被阴影切断。如果检测结果不稳定就检查 ROI 是否包含了太多背景杂物把 ROI 上下边界往车位区域收窄。注意OpenMV 的theta()是度不是弧度算偏差角的三角函数时要先转弧度。3.3 用 find_blobs 识别地面红色引导线很多电赛题目会在场地中央画一条红色引导线车先沿线行驶直到发现车位框再转弯入库。识别引导线比识别车位框更简单用find_blobs框出色块取色块中心的 x 坐标和图像中心的差值作为横向偏差。# LAB 颜色阈值L/A/B 三通道各取上下限数值需现场标定 red_threshold (20, 80, 15, 50, 25, 60) roi (10, 50, 140, 60) blobs img.find_blobs([red_threshold], roiroi, pixels_threshold80, mergeTrue, margin10) if blobs: blob max(blobs, keylambda b: b.area()) offset (blob.cx() - 80) / 80.0 # 归一化偏差到 -1 到 1 # 发送给 STM320x02 表示沿线前进offset 转成 int8 data max(-127, min(127, int(offset * 127))) frame bytearray([0xAA, 0x55, 0x02, data 0xFF]) frame.append(frame[0] ^ frame[1] ^ frame[2] ^ frame[3]) uart.write(frame)pixels_threshold80是色块最小面积小于这个值的噪点直接忽略mergeTrue会把相邻色块合并防止引导线被反光点分割成好几段。这里的blob.cx()是色块中心 x 坐标(blob.cx() - 80)/80把偏差映射到 -1 到 1再乘 127 转成 int8 发送。STM32 收到的是“车中心偏离引导线多少”的归一化值只需要按这个值做比例转向就行。3.4 偏差角度的几何换算如果摄像头装在车头中心且有一定俯仰角度那么图像中的纵向位置其实对应着不同的前方距离。简单换算方法是把目标点所在的行坐标换算成前视距离再结合两个时刻的距离差算偏转角。以 OpenMV H7 的 160x120 输出为例如果摄像头安装高度 H 约 18cm俯仰角约 20 度则图像上第 y 行对应的前向距离近似为distance H / tan(radians(90 - pitch) (y - 60) * fov_per_pixel)其中fov_per_pixel是每个像素对应的视场角需要根据镜头视场角计算。实际项目中我很少把偏转角精确算到度因为车位框检测直接给出横向和纵向线的交点车的目标就是让交点保持在图像中心附近。角度误差大一点PID 也能用时间换空间。真正重要的是把几何关系搞清楚避免把左右方向算反。4. 控制端落地串口、编码器与 PID 闭环4.1 串口接收状态机STM32 这边用标准库还是 HAL 库都可以F103 标准库资料多我直接按标准库写。核心是一个接收状态机状态判断帧头、累计长度、校验、置位标志。不要让中断处理函数做任何浮点运算或延时只搬数据。// 串口1中断接收帧格式AA 55 指令 数据 校验 volatile uint8_t rx_buf[5]; volatile uint8_t rx_index 0; volatile uint8_t frame_ready 0; volatile uint8_t rx_cmd 0, rx_data 0; void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); if (rx_index 0 ch ! 0xAA) return; // 等帧头1 if (rx_index 1 ch ! 0x55) { rx_index 0; return; } // 等帧头2 rx_buf[rx_index] ch; if (rx_index 5) { if ((rx_buf[0] ^ rx_buf[1] ^ rx_buf[2] ^ rx_buf[3]) rx_buf[4]) { rx_cmd rx_buf[2]; rx_data rx_buf[3]; frame_ready 1; } rx_index 0; } } }这段代码的关键是“非法数据立即复位下标”不能让错误字节把状态机带偏。如果某帧校验失败整个状态机会回到等待帧头下一帧正确的数据依然会被接收。rx_data是 int8OpenMV 发过来的 -127 到 127 在 C 里有符号数里直接用int8_t强转即可不要用uint8_t去存否则负数会变成正数让转向方向出错。4.2 电机 PWM 与编码器模式配置两轮差速小车的控制在本题目里是最常见底盘方案。PWM 频率我习惯设 10kHz原因是电机电感在这个频率下电流纹波小噪声也低。定时器 3 的 72MHz 时钟预分频 7199 后变成 10kHz再设置自动重装值为 99得到 100Hz 的 PWM这里要注意PWM 频率 72MHz / (预分频1) / (自动重装值1)所以预分频 71、自动重装 99 时频率为 10kHz// TIM3 输出两路 PWM频率 10kHz TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/(711)1MHz TIM_TimeBaseStructure.TIM_Period 99; // 1MHz/(991)10kHz TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OC1Init(TIM3, TIM_OCInitStructure); // PA6 TIM_OC2Init(TIM3, TIM_OCInitStructure); // PA7 TIM_Cmd(TIM3, ENABLE);编码器测速用定时器 2 的编码器模式A 相和 B 相接 PA0、PA1。编码器模式不需要额外中断硬件自动根据相位差计数值加减方向主循环定时读取TIM2-CNT就拿到了速度// TIM2 编码器模式方向自动由硬件根据相位差判定 TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM2, ENABLE); // 每次控制周期读取一次计数值 int16_t new_cnt (int16_t)TIM2-CNT; int16_t delta new_cnt - last_cnt; // 正数代表正转负数代表反转 last_cnt new_cnt;如果发现车轮往前转但delta是负数要么把 A/B 相接反要么把上面配置里的两个TIM_ICPolarity_Rising改成Falling。编码器方向反了会让 PID 变成正反馈车一给油门就发疯一样加速这是新手工最隐蔽的坑。4.3 增量式 PID 与参数初值速度环我用增量式 PID输出的是 PWM 增量而不是绝对 PWM好处是没有积分累积失控的问题而且推到目标速度时输出自动收敛。F103 没有 FPU用浮点做 PID 也可以但能免则免用整数实现更快更稳// 增量式速度 PID返回 PWM 增量 int speed_pid(int target_speed, int current_speed, int kp, int ki, int kd) { static int error_last 0, integral 0; int error target_speed - current_speed; int output; integral error; if (integral 500) integral 500; // 积分限幅防超调 if (integral -500) integral -500; output kp * error ki * integral kd * (error - error_last); error_last error; if (output 500) output 500; if (output -500) output -500; return output; }参数初值我给一组能跑通的参考kp 取 2ki 取 0.05kd 取 5。注意这个 kp、ki、kd 的量纲和你的target_speed、current_speed有关如果用“编码器计数差值”做速度单位就按这组值起步如果你把速度换算成 cm/skp 可能要放大到 10 以上。整定顺序永远是先调 kp 到临界震荡再调 kd 消除震荡最后加 ki 消除静差。不要一上来就把三个参数同时改。4.4 泊车状态机从寻库到入库状态机是这个题目的灵魂。我把状态分成“寻库位、对准、入库、停车”四个每个状态下 OpenMV 发来的指令优先级不同。寻库时车保持中速沿引导线前进OpenMV 上报 0x02发现车位框后 OpenMV 发 0x03 并附上目标偏差STM32 进入对准状态速度降到低速根据偏差角做转向当偏差小于阈值且超声波距离在安全范围时进入入库状态车速进一步降低最后超声波测距小于 20cm 时触发停车。typedef enum { STATE_SEEK, // 寻库位 STATE_ALIGN, // 对准车位 STATE_ENTER, // 入库 STATE_STOP // 停车 } park_state_t; park_state_t state STATE_SEEK; switch (state) { case STATE_SEEK: if (rx_cmd 0x03) state STATE_ALIGN; target_speed 300; // 中速 break; case STATE_ALIGN: target_speed 150; turn_offset rx_data; // 偏差角 if (abs(turn_offset) 5) state STATE_ENTER; break; case STATE_ENTER: target_speed 60; if (distance_cm 20) state STATE_STOP; break; case STATE_STOP: target_speed 0; break; }状态机的重点不是代码本身而是状态迁移条件要可观测。我习惯把当前状态用串口发到调试助手车跑崩了看状态就知道是卡在对准还是入库。不要在一个状态里做太多判断尤其不要把 PID 的目标速度直接改成 0要让速度环自然减速否则车会点头。5. STM32OpenMV 联调避坑6 条常见问题与排查记录5.1 视觉端一接电机就重启画面反复初始化现象OpenMV 的屏幕上画面闪一下又回到开机画面STM32 那边也收不到任何串口数据。原因OpenMV 启动瞬间电流大尤其是 H7 处理器图像处理时峰值电流能到 300mA 以上。如果和电机驱动共用同一个 5V 稳压源电机启动时把电压拉到 4V 以下OpenMV 就欠压重启。解决OpenMV 单独用一个 5V 电源供电或者至少用带使能控制的稳压模块所有模块 GND 仍然共地。常见做法是在 OpenMV 电源输入端并联一个 470uF 电解电容能扛住几十毫秒的电压跌落再把sensor.set_framesize降到 QQVGA减少处理功耗。5.2 串口线接对了但 STM32 收不到数据现象OpenMV 的 IDE 串口终端能打印正常但 STM32 的串口调试助手一片空白用逻辑分析仪抓 RX 引脚却没有波形翻转。原因多半是 TX/RX 接反或者 OpenMV 的 UART3 复用引脚和 IDE USB 口不是同一个串口。OpenMV 的 USB 串口是给 IDE 调试用的物理 UART 引脚必须单独接出来很多新手把 USB 和 UART 搞混了。解决确认 OpenMV 用的引脚是UART(3, 115200)默认是 P4 和 P5接线时 P4 的 TX 接 STM32 的 RXP5 的 RX 接 STM32 的 TX。用一个 USB 转 TTL 先单独测 OpenMV 的 UART 输出确认能打印再到 STM32 端测。共地问题在排查顺序里排第二位但检查起来比接反更快。5.3 车直线行驶左右摇摆像喝醉酒现象车在引导线上走 S 形刚开始偏差小越修越大最后冲出赛道。原因速度 PID 的增量方向反了或者 kp 太大。编码器方向反是最隐蔽的车明明往前开编码器计数却是负数PID 以为车在倒退于是加大向前输出越加越猛。解决先把 PID 关掉用手推着车往前滑观察TIM2-CNT数值确认计数方向。方向反了就交换 A/B 相接法或者改编码器极性。然后从 kp0.5 开始往上涨每次加 0.5直到车出现微微抖动再退回上一个值。记住kp 大到临界抖动是极限永远不要用超过临界值的参数去“提高响应速度”。5.4 车位线识别时有时无光线一变就翻车现象白天在实验室调试正常换到傍晚或者窗帘拉上后OpenMV 画面里车位线断断续续STM32 收不到 0x03 指令。原因用了sensor.set_auto_exposure(True)后OpenMV 自动调节曝光时间低光环境下单帧曝光变长运动中的画面模糊find_lines的梯度阈值就检不出线加上颜色阈值如果是在固定光照下标定的光照一变整个 LAB 阈值全部失效。解决固定曝光和固定增益曝光时间从 20ms 往上加每加一次跑一遍测试找到能覆盖现场光照范围的稳定值同时把find_lines的threshold从 1500 降到 1000让算法对弱边缘更敏感。如果是场地灯光有频闪问题就把曝光时间设为工频周期的整数倍比如 20ms 恰好是 50Hz 灯光周期的整数倍减少明暗条纹。5.5 Keil5 下载时报 Flash 错误程序烧不进去现象Keil5 编译通过点 Download 弹出 “Erase Failed” 或 “Flash Timeout”STM32 板子上的 LED 不闪。原因最常见的是 Debug 配置里选了错误的烧录器型号板载 ST-Link 却选了 J-Link其次是 STM32F103C8T6 的 Flash 驱动Flash Algorithm没选对或者 SWD 四根线顺序接错尤其电视 SWDIO/SWCLK 对调是反人类。解决在 Keil5 的 Options - Debug 里选择 ST-Link Debugger然后到 Utilities - Settings 里确认 Flash Download 里选择的 Programming Algorithm 是 STM32F10x Med-density 128KB。如果还是不行用 ST-Link Utility 做一次全片擦除再回到 Keil5 下载。SWD 接法是质量第一杜邦线太松就直接焊上去接触不良的坑能查一晚上。5.6 超声波测距偶尔读 0入库停车位一犹豫就撞墙现象入库状态下超声波距离在 20cm 到 30cm 之间跳动偶尔读回 0状态机先进入 STOP 又弹回 ENTER车一窜一窜。原因HC-SR04 的 Trig 脉冲至少 10us但读回 Echo 的定时器捕获如果用了中断频繁的串口收发测距流程会被串口中断打乱加上声波打到斜面的车厢壁会产生多次反射值就会跳变。解决测距函数加超时判断Echo 高电平时间超过 30ms 直接返回上一次有效值有效距离 2cm 以下认为是无效数据丢弃状态机里加“连续三次测距小于阈值才停车”的防抖逻辑。超声波在自动泊车题目里是安全兜底不是主控制信号宁可晚停 0.2 秒也不要误触发停车把车卡半路。6. 进阶把泊车过程拆成三段速度提高一次入库成功率自动泊车校赛的评分通常看“一次性入库”和“停车精度”与其调一套万能 PID不如把泊车过程切成三段速度曲线。寻库段用中速保证 OpenMV 有足够的帧率捕捉车位线对齐段降速到寻库速度的一半此时偏差角修正的步进要减小否则低速下车体响应慢反而修过头入库段再降一半让超声波和视觉都有缓冲时间。三段速度的本质是让“感知周期”和“控制周期”在不同阶段匹配高速时视觉帧率跟不上低速时编码器脉冲间隔过长PID 读取计数值的噪声变大所以每段都要单独给 PID 参数不能一套打天下。我实际跑过的参数是寻库目标速度 300编码器脉冲计数单位kp2对齐目标速度 150kp1.2kd 提高到 8入库目标速度 60kp0.8ki 关闭。调整时先把车架起来让轮子悬空用串口上位机在线改这组参数不用每次重烧固件——STM32 串口调试 PID 这条链路省下来的时间比你想的多得多。上线跑之前再做一次视觉标定把 OpenMV 的图传通过 WiFi 或者 SD 卡录一段回放看车位线的检出框是不是始终在目标位置如果框在抖先解决视觉再碰运动控制。踩坑最狠的一次是晚上约了场地测试灯光只开了一侧整个地面偏黄之前标定的红色阈值识别不出引导线车在原地转圈。回去我把阈值标定写成了一个脚本每次换场地先跑一遍“扫描阈值”程序动态算出色块分布再固化进代码。后来再没被光线问题坑过。自动泊车这类题目真正的门槛不是单个模块而是模块之间怎么默契配合。把串口协议定稳、状态转移可观测、速度分段合理这套方案就能从校赛一路用到毕设验收。希望帮到你。本文还有配套的精品资源点击获取