
简介面向高校电子设计竞赛实训场景以南航电赛校赛自动泊车为背景基于STM32与OpenMV实现车位识别与自动泊车控制整体项目包含完整源码、工程文件与说明文档适合毕业设计、课程设计、工程实训及各类学科竞赛直接复用或二次扩展。资源共收纳201个文件压缩包仅6.45MB涵盖C语言源码36个头文件、34个源文件、Keil工程配置、编译生成文件、OpenMV视觉识别的Python脚本与模型文件另有PDF说明文档目录结构完整便于检索。单片机端包含定时器、闪存、系统时钟、模数转换、I2C通信、串口、FSMC等常用外设驱动是学习标准外设库工程组织的良好范例。目前已有205人学习下载资料经过实测验证按文档连线烧录即可复现适合想快速搭建自动泊车原型或深入理解机器视觉与单片机协同工作的读者。1. 从模拟车库到实车自动泊车项目为什么用STM32OpenMV赛场上一块黑色地胶贴出几道白色边线小车开到车位入口前又冲出去半米再倒回来车尾压线。问题不是电机太慢而是OpenMV已经把车位识别出来了STM32却不知道“该往哪打方向盘”。这个自动泊车系统项目就是解决“看到了却没停进去”的最后一环OpenMV做图像识别与车位坐标计算STM32F103做底层控制与泊车决策两边通过串口交换数据。资源包内包含完整源码、工程文件和接线说明代码按STM32标准库组织适合毕业设计、课程设计、电子设计竞赛、工程实训和初期项目立项复刻。只要手头有STM32F103最小板、OpenMV摄像头、一块电机驱动和两个直流电机再配上杜邦线就能搭出一台能自动找车位、侧方位入库的小车。2. 系统架构与硬件链路串口协议决定上下位机协作方式2.1 视觉与运动控制分离的选型逻辑既然是自动泊车为什么不让OpenMV直接驱动电机OpenMV内部虽然也是STM32加外置摄像头但它的MicroPython环境把大部分算力花在图像采集、颜色识别和线段提取上线程调度和实时性不够稳定。而车模控制需要微秒级定时的PWM输出和中断式串口接收用标准库直接操作寄存器更可控。资源包源码对应的是STM32F103标准库文件stm32f10x_tim.c用于输出PWM和定时器计数stm32f10x_usart.c负责与OpenMV通信stm32f10x_adc.c可采集电池电压或灰度传感器stm32f10x_fsmc.c用来驱动LCD屏显示参数。任务边界清晰OpenMV只回答“车位在哪、车正不正”STM32回答“给多少PWM、何时打死方向”。2.2 硬件接线与引脚规划常见做法是用STM32F103C8T6最小系统板配合OpenMV3及以上型号。如果手头没有扩展板就按引脚定义用面包板和杜邦线连接。接线表如下STM32F103引脚外设引脚说明PA9 (USART1_TX)OpenMV P4 (RX)下发调试命令与配置参数PA10 (USART1_RX)OpenMV P5 (TX)接收车位坐标帧5VVIN 或 5V给OpenMV供电GNDGND必须共地否则串口乱码PA6 / PA7电机驱动IN1 / IN2左轮方向PB0 / PB1电机驱动IN3 / IN4右轮方向PA8舵机PWM信号转向舵机可选注意如果使用电池底盘STM32和OpenMV的电源建议都从主电源稳压后引出避免两路GND电位不一致。信号线较长时在TX与RX之间串联一个1kΩ电阻能降低电机启动瞬间的干扰。2.3 通信帧格式坐标数据怎么承载OpenMV与STM32通信最常见的坑是直接用print发送字符串坐标。STM32端无论用scanf还是自行解析遇到断包、负号、小数点和粘包都要写一堆容错代码。项目里建议下沉协议固定帧结构#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define FRAME_TAIL1 0x0D #define FRAME_TAIL2 0x0A typedef struct { uint8_t head[2]; // 0xAA 0x55 uint8_t msg_id; // 0x01: 车位中心坐标; 0x02: 超声波距离; 0x03: 系统状态 uint8_t length; // 后面数据的字节数 int16_t data[8]; // 连续存放 x_center, y_center, x_offset, angle 等 uint8_t crc; // 对 msg_id 到 data 末尾做累加校验 uint8_t tail[2]; // 0x0D 0x0A } ParkingFrame_t;CRC采用简单累加发送端和接收端都对msg_id、length和data每个字节求和取低8位。接收端计算结果与收到的crc不相等就丢弃整帧。这种设计下即使0xAA出现在数据区也不会误判因为后面还有帧尾确认符。实际运行时OpenMV每20ms发送一帧坐标STM32收到后设置活动标志位连续500ms没收到帧就停车防止摄像头卡死导致小车冲出场地。3. OpenMV端视野处理线提取、角点计算与停车位判断3.1 降低环境干扰的预处理流程OpenMV的镜头在实验室日光灯下和室外阳光下反差极大直接对彩图找线阈值需要反复改。项目里应先转成灰度图再做二值化把停车线变成白色像素地面变成黑色。实际调试时我会让OpenMV侧保留“原灰度”和“二值图”两条输出路径用IDE窗口同时观察比只盯着数值更直观。预处理的目标是强化白和黑的边界让后续霍夫变换找到的线段更稳定。3.2 用find_lines找线段再组装出缺失的车位边框OpenMV自带的find_rects()可以找出四边形轮廓但自动泊车场景的车位一般不是完整矩形——对应倒车入库车位前方是开口的缺一条边。直接调用find_rects()往往只找到三条边矩形置信度低。更稳的做法是先用find_lines()把轮廓线段全部找出来再按线段角度和端点距离组装成车位边框。线段角度范围类别用途theta 20° 或 theta 160°垂直车位左右边界70° theta 110°水平车位底线/顶线import sensor, image, time, ustruct from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QQVGA) # 160x120提高帧率 sensor.skip_frames(30) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) uart UART(3, 115200, timeout_char1000) gray_threshold (170, 255) # 停车线是白色灰度值高 def lines_to_parking(lines): vertical_lines [] horizontal_lines [] for l in lines: if l.theta() 20 or l.theta() 160: vertical_lines.append(l) elif 70 l.theta() 110: horizontal_lines.append(l) if len(vertical_lines) 2 and len(horizontal_lines) 2: left min(l.x1() for l in vertical_lines) right max(l.x2() for l in vertical_lines) top min(l.y1() for l in horizontal_lines) bottom max(l.y2() for l in horizontal_lines) return (left, top, right, bottom) return None clock time.clock() while True: clock.tick() img sensor.snapshot() img.binary([gray_threshold]) img.erode(1) # 去除孤立噪点 lines img.find_lines(threshold2500, theta_margin15, rho_margin15) rect lines_to_parking(lines) if rect: img.draw_rectangle(rect, color200) x_center (rect[0] rect[2]) // 2 y_center (rect[1] rect[3]) // 2 # 按第2章帧协议发送: AA 55 01 04 x_hi x_lo y_hi y_lo crc 0D 0A data ustruct.pack(hh, x_center, y_center) crc (0x01 len(data) data[0] data[1]) 0xFF uart.write(b\xAA\x55 bytes([0x01, len(data)]) data bytes([crc, 0x0D, 0x0A])) print(clock.fps())逻辑说明lines_to_parking把线段按角度分成垂直和水平两类分别取最小和最大值组成车位包围框。img.erode(1)在二值图上做一次腐蚀能去掉细小杂点但车位线本身很细时腐蚀后可能消失所以核大小设置为1。find_lines的threshold对应梯度强度阈值越大则只保留对比度强的线段室内灯光建议在2000~3000室外强光可以提高到4000以上。3.3 把车位的可用状态编码成状态机识别到矩形之后还要判断这个车位能不能停。我在项目里维护三个状态NO_PARKING、PARKING_EMPTY、PARKING_OCCUPIED。判断空位的条件是矩形内部灰度均值接近地面背景且矩形宽度与像素距离的比值落在合理范围。如果矩形内部有大片高亮物体说明有障碍物占位仍发送PARKING_OCCUPIEDSTM32继续寻找下一个空位。状态机可以避免小车停在已经被占用的车位前反复折腾。4. STM32端数据处理与运动控制串口中断、状态机与PID4.1 标准库串口初始化与DMA接收STM32标准库新建工程时多数人习惯用轮询方式接收串口。但OpenMV每20ms发一包数据主循环如果同时处理PWM、传感器和LCD刷新轮询很容易漏掉末尾两个字节。这里改用USART1空闲中断加DMA接收一次性把不定长帧搬到缓冲区。void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); USART_Cmd(USART1, ENABLE); } void DMA1_Channel5_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)usart1_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); }参数说明波特率必须与OpenMV端一致这里用115200。DMA_Mode_Circular循环模式省去手动清零但缓冲区满了会自动覆盖所以需要在空闲中断里判断当前写入位置。USART_ITConfig开启IDLE空闲中断串口一帧结束后触发读取DMA1_Channel5-CNDTR就能知道本帧字节数随后把整个缓冲区交给解析状态机。4.2 帧解析状态机与防粘包状态机解析是嵌入式可靠通信的兜底手段。每次串口空闲中断触发后把DMA收到的原始字节依次喂给下面这个函数uint8_t parser_step(uint8_t byte, ParkingFrame_t *frame) { static uint8_t state 0; static uint8_t idx 0; static uint8_t expected_len 0; static uint8_t sum 0; switch (state) { case 0: if (byte 0xAA) state 1; break; case 1: if (byte 0x55) { state 2; sum 0; } else state 0; break; case 2: frame-msg_id byte; sum byte; state 3; break; case 3: frame-length byte; sum byte; expected_len byte; idx 0; state expected_len ? 4 : 5; break; case 4: ((uint8_t *)frame-data)[idx] byte; sum byte; if (idx expected_len) state 5; break; case 5: state 0; return (sum byte) (frame-msg_id 0x01 || frame-msg_id 0x02 || frame-msg_id 0x03); } return 0; }这个状态机的关键在于只要一个字节不匹配帧头就退回状态0数据长度只信任帧内length字段不会因为粘包把下一帧数据错误消化。如果车内环境电磁干扰较强把累加校验换成CRC8可以进一步提升可靠性代价是发送端计算量稍大。帧解析成功后把frame-data[0]和frame-data[1]作为车位中心像素坐标换算成横向偏差送入控制模块。4.3 差速转向与增量式PID转向控制我习惯在STM32上实现两级底层是速度环顶层是角度环。OpenMV给的是图像坐标偏差经过比例缩放变成目标角速度差量再用增量式PID补偿避免车位线抖动导致方向来回甩。typedef struct { int16_t prev_err; float integral; float Kp, Ki, Kd; int16_t max_integral; int16_t max_output; } PID_t; int16_t pid_calculate(PID_t *pid, int16_t target, int16_t current) { int16_t err target - current; int16_t output; pid-integral err; if (pid-integral pid-max_integral) pid-integral pid-max_integral; if (pid-integral -pid-max_integral) pid-integral -pid-max_integral; output pid-Kp * err pid-Ki * pid-integral pid-Kd * (err - pid-prev_err); pid-prev_err err; if (output pid-max_output) output pid-max_output; if (output -pid-max_output) output -pid-max_output; return output; }增量式PID比位置式好在对误差累积不那么敏感不会长期饱和。参数初值我一般取Kp18Ki0.02Kd8积分限幅100输出限幅200。配合串口调试PID波形观察车位中心偏差曲线如果曲线在零线附近来回震荡降低Kd或Kp如果到达目标位置后回正慢增大Ki。两轮差速小车控制时左轮PWM等于基础速度加输出量右轮PWM等于基础速度减输出量差速越大转弯半径越小。基础速度建议设在30到60的占空比百分比之间太快会导致图像帧率不足时冲出车位。4.4 泊车动作决策表场景图像特征控制动作未发现空车位车位矩形宽度小于阈值沿车道直行保持车位搜索空车位在车身右侧x_center 画面中心15px减速并右转保持车位在中心车位对齐车身x_center在中心±15px内回正方向挂倒挡车尾进入车位矩形高度到达设定阈值原地修正方向保持车身平行到位停车超声波距离小于15cm刹车进入锁定状态动作表只是顶层逻辑实际泊车效果依赖最后几步的阈值。阈值太大车身没完全进去就停太小车尾可能撞到障碍物。调试时建议把超声波距离通过另一个串口打印出来对比实际车身位置确定停车点。5. 联调排错与动态ROI自适应扩展技巧5.1 Keil烧录与虚拟串口驱动常见问题拿到资源后先不用改代码把工程编译烧录进STM32再单独给OpenMV烧录main.py。OpenMV IDE下载安装后连接板子如果设备管理器里虚拟串口出现黄色感叹号说明驱动没装好这就是STM32 Virtual COM Port的典型问题换一根USB线也可能解决。Keil5安装完要确认芯片包安装否则编译会报找不到stm32f10x.h。连接ST-Link下载时如果提示error: no STM32 target found先检查SWD接线是否接反再确认目标板供电正常。5.2 动态ROI与曝光参数自动适配很多复刻项目死在换场地后识别不到车位线。与其反复改阈值不如在OpenMV里留一组串口命令让STM32在启动时把当前场地的ROI和曝光时间发给OpenMV。具体做法是在帧协议里扩展msg_id0x04的配置帧if msg_id 0x04: roi_x unpack(h, data[0:2]) roi_y unpack(h, data[2:4]) sensor.set_auto_exposure(False, exposure_us(data[4] 8) | data[5])这套方法的核心是把场地适配从“重新编译”变成“运行时配置”。比赛现场可以用STM32端的一个按键循环切换预设的ROI和曝光参数甚至在stm32f10x_flash.c操作的Flash区域里保存最近一次成功配置。这样既避免频繁拔线烧录也保留了OpenMV的快速原型优势。后续扩展多车位记忆、语音播报时只需要在帧类型里继续增加字段控制逻辑不用推倒重写。本文还有配套的精品资源点击获取