☰
STM32+OpenMV自动泊车实战:从双芯片架构到PID闭环全攻略
2026/11/3 17:18:17 网站建设 项目流程

简介:基于STM32与OpenMV的自动泊车系统完整赛题方案,适配南航电赛校赛及青岛2021市电赛控制题,面向嵌入式竞赛备赛学生与STM32初学者。代码由hjduino开源项目改造,包含OpenMV视觉寻线与识别部分,以及Keil5工程源码,可直接编译烧写验证。压缩包共201个文件,以C源文件、头文件、库文件为主,另有Python脚本、TFLite模型、PDF技术文档及配置文件,整体6.46MB,结构覆盖视觉、控制与底层驱动。已有181人学习,适合需要快速上手视觉识别与电机控制的参赛团队。通过技术文档可了解项目特色、模块划分与调参思路,对照源码能掌握STM32与OpenMV串口通信、PID控制等关键实现。

1. 电赛校赛自动泊车:为什么我选了 STM32 + OpenMV 这对组合

基于 STM32 和 OpenMV 的自动泊车题目,几乎每年都是校内电赛的“常驻嘉宾”。它难不在某个单点,难在把视觉识别、串口通信、编码器测速、PID 闭环和机械结构串成一条完整的控制链。第一次拿到题目,我第一反应是用 OpenMV 同时做识别和控制,结果发现 OpenMV 跑 30 万像素的图像处理已经很吃力,再叠加差速控制逻辑,帧率掉到 8 帧左右,车子转向严重滞后,根本停不进模拟车位。后来拆成 OpenMV 负责“看”、STM32 负责“动”,识别帧率翻倍,控制周期稳定在 10ms,才算真正把题做通。

这篇文章就把这套方案怎么拆、怎么接线、代码怎么写、坑在哪,完整写出来。适合校赛准备期的队伍、做相关毕业设计的同学,以及想快速验证「视觉 + 运动控制」双芯片工作流的从业者。跟着步骤走,半天能跑起来;想改造成寻线、巡迹或者目标追踪,换识别函数就行。

2. 系统架构与硬件选型:两轮差速 + 编码器闭环,视觉跟控制各干各的

2.1 双芯片分工:为什么不让 OpenMV 直接驱动电机

自动泊车这个任务,表面上是“把车开进车位”,实际拆开是三件事:找车位、算路径、跟轨迹。OpenMV 是用 Micropython 写的,处理图像和跑神经网络很顺手,但让它直接输出 PWM 给电机,实时性不够稳。MICROPYTHON 的解释执行特性决定了它的定时精度和中断响应都不适合做 10ms 级的周期控制。STM32 则正好相反,定时器、编码器接口、PWM 输出都是硬件外设,跑中断服务函数很稳。

所以这套方案的分工是:OpenMV 每 80ms 识别一次车位,算出车相对车位的横向偏差和偏转角,打包成 6 字节的帧通过 UART 发给 STM32;STM32 收到后跑 PID,用编码器测实时速度,输出 PWM 给电机驱动模块,完成对准、转向、入库这套动作。两条链路互不干扰,出问题了也好排查——车不动先查串口,转向异常先查 PID,不用对着一个板子挠头。

我一般会强调:OpenMV 和 STM32 之间只传“决策结果”,不传“图像数据”。有的同学图省事,把 OpenMV 的摄像头画面直接通过串口传到 STM32 处理,波特率加到 921600 也扛不住,而且 STM32 处理图像会占掉大量 CPU 时间,控制周期直接崩掉。这不是性能不够,是架构选错了。

2.2 硬件清单与连接表:照着这个清单买,不会踩坑

校赛经费有限,选型不用追求高配。我用的是 STM32F103C8T6 最小系统板,OpenMV4 H7 Plus,TB6612 电机驱动模块,两个带霍尔编码器的 N20 微型减速电机,以及一块 7.4V / 2S 锂电池。F103 虽然是老芯片,但很多学校实验室和毕设都在用,踩坑资料多,上手快。OpenMV4 H7 的算力足够跑 320x240 分辨率的色块识别,没必要上 OpenMV3。

连接表如下,按这个接线基本不会出错:

模块STM32 引脚说明
OpenMV UART3 TXPA10 (RX1)OpenMV 发送数据给 STM32
OpenMV UART3 RXPA9 (TX1)可选,用于 STM32 回传指令
电机 A 编码器 A/BPA0 / PA1接 TIM2 编码器模式
电机 B 编码器 A/BPB6 / PB7接 TIM4 编码器模式
TB6612 AIN1/AIN2/PWMPB0 / PB1 / PA7A 电机方向与速度
TB6612 BIN1/BIN2/PWMPB10 / PB11 / PB8B 电机方向与速度
TB6612 STBY3.3V接高电平才能工作

注意两个关键点。第一,编码器是 OC 输出的,必须外接上拉电阻,一般 10kΩ 就行,否则 STM32 读不到跳变沿。第二,TB6612 的逻辑电源 VCC 接 3.3V,电机电源 VM 接电池正极,两者千万不能接反,一接反芯片大概率烧毁。我第一次接的时候把 VM 接到了 5V 排针上,TB6612 直接冒烟,血泪经验。

2.3 电源拓扑与电平匹配:3.3V / 5V / 12V 的“三等公民”

这套系统里有三种电压等级:STM32 和 OpenMV 是 3.3V 逻辑,TB6612 的逻辑端可以 3.3V 驱动,电机要用 7.4V 电池供电。有的驱动模块(比如 L298N)要求逻辑高电平最低 2.3V,但内部稳压会让 5V 输出反灌到 STM32,搞不好会烧芯片。选 TB6612 这类专用电机驱动反而更省心,它的逻辑输入直接兼容 3.3V,不需要额外电平转换。

电源分配我一般这么做:电池正极分两条路,一条直接进 TB6612 的 VM,另一条进降压模块降到 5V,再给 STM32 的 5V 引脚和 OpenMV 的 USB 供电。STM32 板载稳压会把 5V 变成 3.3V,OpenMV 板载稳压同理。千万别让电池电压直接进 OpenMV,H7 的耐压上限是 5.5V,7.4V 进去大概率烧掉稳压管。

3. OpenMV 端视觉识别:色块拟合车位,算出偏转角和横向偏差

3.1 为什么选色块识别而不是 AprilTag 或模型训练

自动泊车的车位线在电赛现场一般是用黄色胶带或者黑色胶带贴出来的矩形框,背景是浅色地面。这种场景下,色块识别是最稳的方案,识别速度快,参数直观,抗干扰也不错。AprilTag 需要打印固定尺寸的 Tag 贴在车位角落,如果现场不允许贴,方案直接作废。模型训练需要准备数据集,校赛时间紧张,不值得在这上面花时间。

色块识别的核心思路是:把图像从 RGB 转到 LAB 色彩空间,设定一个颜色阈值把车位胶带的像素筛出来,然后找最大色块的宽高和中心坐标。因为车位框是固定尺寸的,根据色块宽度占图像宽度的比例,可以估算出车到车位的距离。这个信息配合偏转角,足够 STM32 完成入库动作。

OPENMV 的 IDE 里有个“阈值编辑器”工具,可以实时调节 LAB 阈值范围并预览二值化结果。我一般把车放在不同位置和光照下,各采集 3~5 帧,取阈值的并集,这样能避免某个特定角度下识别丢失。

3.2 识别与坐标计算:找矩形、算中心、算角度

以下是 OpenMV 端的核心代码,逻辑是找最大色块、计算偏转角、通过串口发给 STM32。

import sensor, image, time, math from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) # RGB565 比灰度图色彩信息丰富,适合 LAB 阈值识别 sensor.set_framesize(sensor.QVGA) # 320x240 足够识别,帧率比 VGA 快一截 sensor.skip_frames(time=2000) sensor.set_auto_gain(False) # 关闭自动增益,防止亮度波动导致阈值失效 sensor.set_auto_whitebal(False) # 同样关闭白平衡,固定色温 # UART3 初始化,波特率 115200,8N1 uart = UART(3, 115200, timeout_char=1000) # 这个是 LAB 色彩空间中黄色胶带的阈值范围,用阈值编辑器测出来的 yellow_threshold = (78, 100, -10, 12, 25, 75) # 像素距离换算系数:色块宽度(像素)到实际距离(厘米)的映射 DIST_COEF = 80 def find_parking_slot(img): blobs = img.find_blobs([yellow_threshold], pixels_threshold=200, area_threshold=200) if not blobs: return None, None, None # 取面积最大的色块,认为是车位框 max_blob = max(blobs, key=lambda b: b.area()) # 偏转角:色块中心相对图像中心的 x 偏移,映射到 -30 ~ 30 度 angle = (max_blob.cx() - img.width() // 2) * 0.18 # 横向偏差:归一化到 -1.0 ~ 1.0 deviation = (max_blob.cx() - img.width() // 2) / (img.width() // 2) # 距离:色块宽度的倒数近似距离,值越小说明越远 distance = DIST_COEF / max_blob.w() return angle, deviation, distance while True: img = sensor.snapshot() angle, deviation, distance = find_parking_slot(img) if angle is not None: # 打包成 6 字节:帧头, 目标ID, 偏差(有符号), 角度(有符号), 距离, 校验和 data = bytearray([0xAA, 0x01, int(deviation * 100) & 0xFF, int(angle) & 0xFF, int(distance) & 0xFF, 0]) data[5] = (data[1] + data[2] + data[3] + data[4]) & 0xFF uart.write(data) img.draw_cross(max_blob.cx(), max_blob.cy(), color=(255, 0, 0)) img.draw_rectangle(max_blob.rect(), color=(255, 0, 0)) time.sleep_ms(80)

代码逻辑分三步:先找色块,再算偏差和角度,最后组帧发串口。需要注意angle和deviation的处理方式不同——deviation乘以 100 是为了把浮点数转成整数传输,避免浮点精度在串口传输中丢失;angle直接取整,单位是度,STM32 端拿回去直接当 PID 输入用。

time.sleep_ms(80)控制了识别主循环的帧率大约是 12.5 帧。这个值不能太小,否则 OpenMV 的 CPU 会打满,串口发送也跟着卡顿;也不能太大,我实测超过 150ms 转向反应就明显迟钝,车冲过头。

3.3 串口帧格式设计:帧头 + 数据长度 + 校验和

串口通信最容易出的问题就是对不上格式。我在设计协议时固定了一个 6 字节帧:0xAA 0x01 deviation angle distance checksum。0xAA是帧头,防止数据错位;第二个字节是数据 ID,区分不同类型的指令;末字节是校验和,接收方把中间 4 字节累加,和末字节比对,不一致就丢弃整帧。

这个格式是「无固定长度 + 校验」的折中方案。有的同学直接用字符串发"1,25,30,60",在 STM32 端还需要解析逗号分隔的 ASCII 字符串,不仅慢,而且一旦某个字符被噪声干扰,整个解析就乱套。用二进制帧则稳定得多,每帧定长 6 字节,接收缓冲区只要凑齐 6 字节就能解析,不需要判断结束符。

波特率建议统一用 115200,这是两个平台的默认值,不用改寄存器。如果数据量大,可以升到 256000,但 115200 对这套数据量(每 80ms 一次 6 字节)来说绰绰有余,稳定优先。

4. STM32 端运动控制:串口协议解析、编码器测速、PID 差速转向

4.1 标准库还是 HAL 库:我选了标准库,理由很直接

STM32 开发环境这块,经常有同学纠结用标准库还是 HAL 库。标准库的代码比 HAL 更底层,寄存器操作直观,编译出来的 BIN 文件小,执行效率高;HAL 库封装度高、跨芯片复用方便,但回调函数多,追查逻辑时跳来跳去。电赛校赛这种一次性项目,代码量不大,重开发速度、轻跨平台迁移,我选标准库。

具体到 Keil5 的工程配置:新建工程后,把标准库的stm32f10x_conf.h打开,启用需要的模块宏定义,比如RCC、GPIO、TIM、USART、NVIC。一个常见的坑是编译时报#error "Please select first the target STM32F10x device used in your application",这是因为没有在预处理器里定义芯片型号宏,比如STM32F10X_MD。在 Keil 的 C/C++ 选项卡里加上这个宏就能解决,英文聊天框里输入这个报错搜索也能搜到大量解决方案。

4.2 编码器定时器配置:TIM2 / TIM4 双通道正交解码

N20 电机的霍尔编码器输出两个相位相差 90° 的方波信号。STM32 的定时器工作在编码器模式下,会在两个信号的上升沿和下降沿都计数,也就是 4 倍频。这样 13 线的霍尔编码器经过 4 倍频后,电机转一圈可以输出 52 个脉冲。配合减速比 30 的齿轮箱,轮子转一圈有 1560 个计数,分辨率足够做精确的速度环了。

配置代码如下,使用的是标准库的寄存器操作方式:

void Encoder_Init_TIM2(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 使能 GPIOA 和 TIM2 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // PA0 和 PA1 配置为浮空输入,编码器信号从这里进入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 定时器时基:不分频,计数上限 65535,这样不会溢出 TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 配置为编码器模式:两个输入都采样,4倍频 TIM_ICInitStructure.TIM_ICMode = TIM_ICMode_ICAP; TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI1, TIM_EncoderMode_TI2, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); }

编码器模式的关键在TIM_EncoderInterfaceConfig这一行,它把 TIM2 的 CH1 和 CH2 配对成正交解码输入。之后不需要再写外部中断,定时器硬件自动计数,TIM_GetCounter(TIM2)直接读当前值。每 10ms 读一次计数差值,除以时间就是转速,这就是速度环的反馈量。

这里有个很容易忽略的参数:TIM_Prescaler必须设为 0。如果分了频,编码器的高频脉冲会被漏掉,速度反馈值偏小,PID 输出就会过大,车会一冲一冲的。判断分频是否设对的土办法:用手慢慢转电机轮子,在调试器里观察计数变化,如果数值跳变呈“台阶状”而不是“连续流”,就说明分频把脉冲吞了。

4.3 PID 差速控制:先速度环,再转向环

差速小车的控制我用的是两级 PID:内环是速度环,让左右轮实际速度分别跟踪目标速度;外环是转向环,根据 OpenMV 传回来的横向偏差计算左右轮的速度差,实现转向。这种串级结构的优势在于转向环的输出不是 PWM 而是“速度差”,速度环会不折不扣地执行,不会因为电池电压下降导致转向力度变化。

// 速度环 PID 参数,P=18 I=0.02 D=0 // 转向环 PID 参数,P=0.5 I=0 D=0 typedef struct { float Kp, Ki, Kd; float integral, last_error; float out_max; } PID_t; // 这两个 PID 实例,一个管速度,一个管转向 PID_t speed_pid = {18.0f, 0.02f, 0.0f, 0, 0, 5000}; PID_t steer_pid = {0.5f, 0.0f, 0.0f, 0, 0, 40}; int16_t Speed_PID_Calc(PID_t *pid, int16_t target, int16_t current) { float error = target - current; pid->integral += error * 0.01f; // 积分项,10ms 周期换算成秒 if (pid->integral > 1000) pid->integral = 1000; // 限幅,防止积分饱和 if (pid->integral < -1000) pid->integral = -1000; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * (error - pid->last_error); pid->last_error = error; if (output > pid->out_max) output = pid->out_max; if (output < -pid->out_max) output = -pid->out_max; return (int16_t)output; }

主循环里每 10ms 执行一次控制逻辑:

void Motor_Control_Task(void) { // 读取编码器计数,注意要读取计数值之后再清0 int16_t enc_l = TIM_GetCounter(TIM2); int16_t enc_r = TIM_GetCounter(TIM4); TIM_SetCounter(TIM2, 0); TIM_SetCounter(TIM4, 0); // 编码器值换算成速度,单位是 cm/s,按照车轮周长的比例算 float v_l = enc_l * 0.036f; float v_r = enc_r * 0.036f; // 速度环:每个轮子分别执行,目标速度来自转向环输出 int16_t speed_out_l = Speed_PID_Calc(&speed_pid, target_l, v_l); int16_t speed_out_r = Speed_PID_Calc(&speed_pid, target_r, v_r); // 转向环:偏差 deviation 来自 OpenMV 归一化的横向偏移 float steer_out = Steer_PID_Calc(&steer_pid, deviation, 0); target_l = base_speed + steer_out; target_r = base_speed - steer_out; // PWM 限幅到 0~1000 的范围,并输出到 TB6612 TIM_SetCompare1(TIM1, LIMIT(speed_out_l, 0, 1000)); TIM_SetCompare2(TIM1, LIMIT(speed_out_r, 0, 1000)); }

参数调参经验:速度环 P 从 10 开始,每隔 5 加一次,观察轮子是否出现高频振荡——如果出现“嗡嗡”声或明显抖动,则 P 回退 2~3。积分项一般为 0.01~0.05,过大容易转向过冲。转向环的参数很小,因为它输出的是速度差(占空比差值),数值区间在 ±40 就足够转向了,不控制好会让车原地打转。

4.4 串口中断接收:不定长协议要凑帧,不是来一帧处理一帧

串口接收 OpenMV 的 6 字节定长帧,用中断方式处理。注意不要在中断服务函数里做 PID 计算,那样会把中断时间拖得很长,影响编码器计数的实时性。正确做法是:中断里只做字节缓冲,凑满 6 字节并且校验通过,就置一个标志位;主循环检测到标志位后,才去解析并更新控制目标。

volatile uint8_t rx_buffer[6]; volatile uint8_t rx_cnt = 0; volatile uint8_t rx_ready = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); // 找帧头:如果当前已接收字节为 0 且 ch 不是 0xAA,则丢弃 if (rx_cnt == 0 && ch != 0xAA) return; rx_buffer[rx_cnt++] = ch; if (rx_cnt >= 6) { // 校验和验证 uint8_t sum = 0; for (int i = 1; i < 5; i++) sum += rx_buffer[i]; if (sum == rx_buffer[5]) { rx_ready = 1; } rx_cnt = 0; // 无论校验是否通过,都重置,准备接收新帧 } } }

这个接收函数里有个重要的长度判断:if (rx_cnt >= 6)。如果在干扰环境下帧头的位置对不上,数据错位导致永远凑不齐 6 字节,那rx_cnt会一直增长,缓冲区溢出。我在接受存储时用了rx_cnt == 0 && ch != 0xAA先做一次帧头过滤,这能挡住大部分噪声。更稳妥的做法是加一个超时定时器:超过 20ms 没收齐就清空重来,避免卡死。但校赛场景里串口干扰不大,帧头过滤已经够用。

rx_ready标志位在主循环中检查,解析完立刻清零,防止重复处理同一帧数据。

5. 自动泊车避坑:串口丢帧、编码器反相、PID 炸车、舵机卡死

5.1 症状:OpenMV 明明在发数据,STM32 就是不动

现象:OpenMV 的 IDE 串口终端能看到数据输出,但车没有任何反应。

原因排查下来有两类。第一类是波特率不匹配,OpenMV 的UART(3, 115200)和 STM32 的USART_InitStructure.USART_BaudRate = 115200虽然看起来一样,但如果 STM32 的时钟配置有误,实际波特率会偏差很大,导致接收端全是乱码,帧头永远匹配不上。第二类是逻辑电平不匹配,OpenMV 的 UART 引脚是 3.3V 电平,STM32 的 USART 能直接接收 3.3V,但有些 USB-TTL 模块会把电平拉高到 5V,反而把 STM32 接收引脚打坏。我遇到过用一根杜邦线把 OpenMV 的 TX 直接连到 STM32 的 RX,工作正常;换了一根长杜邦线就开始丢帧——线材质量差,接触电阻大,信号边沿变缓。

解决:先用示波器或者逻辑分析仪抓 STM32 的 RX 引脚波形,看波特率是否准确;然后用同一根杜邦线把 OpenMV 的 TX 和 STM32 的 RX 对接,在 STM32 端加一个 LED 指示收到有效帧;最后把串口线尽量缩短,不要飞线过长。

5.2 症状:编码器计数值方向不对,车越跑越偏

现象:直线行驶时车会跑偏,左右轮转速目标一样,但实际速度差很大,而且方向不固定。

原因:编码器 A/B 相接反了。STM32 的编码器模式对 A 相超前还是 B 相超前是敏感的——A 超前 B 是正计数,B 超前 A 是负计数。如果两个轮子的 A/B 相刚好接反,其中一个轮的计数值就是负数,速度反馈反了,PID 会输出错误的 PWM,车自然跑偏。

解决:把对调的 A/B 相换回来,或者在代码里取绝对值并增加方向判断。我习惯在初始化时把两个轮子都正转 20 圈,观察计数器是否为正值,如果为负就把某个编码器输入的 A、B 交换。这是硬件层面的问题,别试图在 PID 里反向补偿,那会让调试变得玄学化。

5.3 症状:PID 参数调大后车子震荡发散,甚至直接原地甩尾

现象:速度环 P 加到 20 以上,车子开始高频抖动,轮子发出异响;转向环 P 加到 0.8,车在目标线附近来回摆。

原因:速度环的反馈量是编码器计数值,采样周期 10ms,如果 P 太大,每个控制周期都会产生很大的 PWM 变化,电机机械响应跟不上 PWM 变化,就形成了极限环振荡。转向环也一样,它的反馈是视觉偏差,OpenMV 的帧率只有 12 帧左右,响应滞后严重,过大的 P 必然发散。

解决:先把速度环的 D 项加上,Kd=0.5能明显抑制振荡;然后分两步调——先只给一个轮子发固定 PWM,验证速度环稳定;再联调转向环,P 从 0.1 起步慢慢加。PID 调试的玄学很多时候是参数间相互影响,串级 PID 更是如此,记得一次只调一个环。

5.4 症状:车位识别框跳变,转向随之剧烈摆动

现象:OpenMV 的find_blobs有时会识别到反光的地面或者车体阴影,导致色块中心跳变几十像素,STM32 转向 PID 输入剧烈变化,车辆蛇形前进。

原因:阈值设置太宽,或者图像里有和车位胶带颜色相近的物体。另外 OpenMV 默认开启了自动增益和自动白平衡,亮度和色温变化会导致同一块胶带的 LAB 值波动,阈值边界被触发,出现误检。

解决:在代码里加一个色块面积阈值和矩形的宽高比过滤条件,例如色块宽度必须在图像宽度的 1/4 到 3/4 之间,宽高比在 1.5 到 4 之间,能过滤掉大部分无关物体。同时关闭自动增益和白平衡(就是 3.2 节代码里那两行),固定曝光时间。如果光线还是变化剧烈,就把曝光时间设置为固定值,例如sensor.set_auto_exposure(False)并手动设曝光 20ms。

6. 调试三板斧:串口回显、绘图工具、分模块验证

6.1 串口回显:先打通数据链路,再谈控制算法

自动泊车系统涉及两条数据链路,OpenMV→STM32 和目标速度→电机。我调试时第一步就是把 STM32 收到的原始串口数据回显到电脑上:STM32 把接收到的字节原样再通过另一个串口发出去,在电脑串口助手上看是否有连续的AA 01 XX XX XX XX帧。如果看不到,说明链路断了或格式不对,直接在这里排查,不用去调 PID。

接线时注意共地,OpenMV、STM32、电脑串口助手三者必须共地,不共地会出现数据偶发乱码。用 USB 给 STM32 供电时,如果 OpenMV 用电池独立供电,两者的 GND 也要连到一起,否则 UART 信号没有参考地,数据必乱。

6.2 用串口绘图工具看 PID 输出:让曲线代替感觉

调 PID 不能靠手感,我看的是曲线。STM32 通过串口把target_l和actual_l以文本形式发送到电脑,用串口绘图工具(比如 SerialPlot 或者 Arduino 串口绘图器)实时画两条曲线,目标速度和实际速度的贴合程度一目了然。如果实际速度曲线围绕目标值缓慢振荡,是 P 过大;如果响应太慢、始终跟不上目标,是 P 过小或积分太弱。

速度环稳定后,我用同样方法观察转向环:把 OpenMV 传回的deviation和 STM32 的steer_out画出来,看转向输出是否平滑。如果deviation曲线是方波状跳变,而steer_out也跟着跳,那就说明视觉端数据有抖动,问题不在 PID,在 OpenMV 的识别滤波。这时候回第 3 章加面积过滤条件就够了。

6.3 分模块验证:先从“盲开”开始,最后才连视觉

完整联调前,我会先让 STM32 执行一个写死的运动序列:直行 50cm、左转 90°、右转 90°、后退 30cm。这一步验证的是编码器测距和 PID 控制是否准确,不依赖任何外部输入。如果连固定动作都走不准,连上视觉只会更乱,车会飞出场地。

固定动作走准后,再让 OpenMV 单独做识别,把识别结果打印到 IDE 的串口终端,确认偏差值随车位置变化的方向和大小都是对的。最后才把两条链路接通。整个流程下来,如果还差一点,多半是标定问题——N20 电机的减速比标称 30 但实际是 29.86,编码器换算成厘米的系数 0.036 需要用游标卡尺实测轮子直径后重新算。

从那以后我每次做这种“视觉引导 + 运动控制”的项目,都强制先做一次固定动作盲测,再做视觉单测,最后才联调。这套顺序帮我避开了至少一半的玄学故障,希望帮到你。

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

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

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

立即咨询