OpenMV与STM32物料搬运机器人:视觉识别到动作执行全实现

发布时间:2026/9/9 2:58:59
OpenMV与STM32物料搬运机器人:视觉识别到动作执行全实现 简介这套代码包面向工程训练比赛中的物料搬运任务将OpenMV视觉模块与STM32F103C8T6微控制器相结合全程使用C语言编写控制程序。OpenMV端完成图像采集、颜色或形状识别、物料定位等视觉任务再通过串口将坐标和状态信息发送给STM32STM32端则解析数据并控制底盘寻线、机械臂抓取与释放实现完整的搬运流程。代码中包含了图像预处理、特征提取、目标检测、串口通信协议、电机驱动与避障逻辑等关键模块并预留了参数修改接口方便将工程迁移到其他单片机平台。资源共287个文件除C源码和头文件外还包含Keil工程配置文件、编译生成的中间文件、链接脚本、Hex固件、链接映射文件与列表文件等完整保留了从源程序到最终固件的构建信息可以帮助读者了解工程中各模块的依赖关系便于二次开发。压缩包大小为6.62MB结构紧凑清晰已有1638人学习下载适合嵌入式竞赛备赛、毕业设计或机器人入门实践者深入学习参考也可以作为视觉识别与运动控制相结合的课程设计案例。OpenMV STM32F103C8T6 物料搬运机器人从视觉识别到动作执行的完整实现做嵌入式这几年有个项目几乎每次实训都会被拎出来当“入门到中级跳板”就是用 OpenMV 做视觉识别、STM32F103C8T6 做运动控制的物料搬运机器人。我在实际带项目时也发现真正把视觉端、通信端、运动控制端三块串起来跑通的人其实不多。大多数卡点不在某个单独模块而是卡在 OpenMV 与 STM32 之间的数据协议上或者卡在舵机抖动、串口丢帧、阈值漂移这些“看起来小、实际上致命”的细节上。这篇文章就把我做这个项目时的完整思路、核心代码、调参心得和踩坑记录都摊开讲清楚给准备从零做一遍的同学当参考。这个项目能做什么简单说就是让 OpenMV 通过摄像头识别待搬运物料通常是颜色块或特定形状算出物料在画面中的坐标和颜色信息然后通过串口把结果发给 STM32F103C8T6再由 STM32 驱动舵机机械臂或小车底盘完成抓取、转移、放置的动作。适合刚接触嵌入式视觉方向、学过 STM32 基础外设、想动手做一个完整视觉控制闭环的同学。如果你已经点过 LED、玩过 PWM、调过串口那这个项目的技术跨度刚刚好。1. 整体方案与硬件架构设计1.1 系统分工为什么是 OpenMV 做眼睛、STM32 做手脚先聊一下选型逻辑。物料搬运机器人本质上就是一个“输入—决策—输出”的闭环系统输入是视觉信息决策是目标位置判断输出是电机和舵机的运动。你可以用一块高性能开发板全包比如树莓派加 USB 摄像头但实际在工业成本和实时性场景里这种方案并不够“嵌入式”。更合理的做法是把任务按硬件能力拆分。OpenMV 的强项是图像处理。它内部运行 MicroPython封装好了find_blobs、find_lines、find_april_tags这些视觉 API你不需要懂底层图像算法几行代码就能识别色块并获得中心坐标、面积、颜色类别。它不适合做高实时性运动控制毕竟 Python 解释执行和单核 CPU 摆在那。STM32F103C8T6 的强项恰恰是底层控制。它有丰富定时器、多路 PWM、硬件串口、大量 GPIO处理舵机角度控制、直流电机调速、IO 逻辑切换是拿手好戏而且响应确定性远好于跑 Python 的 MCU。更关键的是器件成本低、资料多板子坏了换一块继续焊也不心疼。所以这套方案的灵魂就是四个字各司其职。OpenMV 负责把“看到的东西”翻译成结构化数据STM32 负责把这些数据转成“物理动作”。眼睛和手脚分开上层视觉逻辑和底层控制逻辑解耦调试时可以独立验证每一端这也是工程上很典型的“模块化设计”思路。1.2 硬件选型与物料清单这个项目用到的硬件不算多我这里给一份可以照抄的清单并多说一句选型理由方便你在淘宝下单时不走弯路。模块推荐型号数量选型理由视觉模块OpenMV Cam H7 Plus 或 M71H7 运算更快色块识别更流畅预算紧 M7 也够用主控板STM32F103C8T6 最小系统板1引脚够用、资料多、自己做板也容易画舵机SG90小臂 / MG996R大臂2~4SG90 便宜够轻载MG996R 适合需要力矩的关节电机驱动TB6612FNG 或 L298N1TB6612 压降小效率高对电池友好底盘电机直流减速电机带霍尔编码器更好2~4轮式底盘最省事编码器能后续做闭环调速摄像头支架金属/3D打印云台架1稳定视角减少抖动导致识别跳动供电7.4V 2S 锂电池 降压模块1舵机瞬间电流大需要独立供电避坑物料红/绿/蓝小方块或乒乓球若干颜色通道可区分便于验证识别效果这里有个重点STM32 和 OpenMV 的串口通信必须共地否则数据就是乱码。很多人买回板子后直接 TX-RX 交叉接上就跑了结果发现收到的全是 0xFF就是没接 GND。共地这事我会在后面排查章节再强调一次因为真的大把人栽在这。另外一个提醒STM32F103C8T6 是 48 脚的 LQFP 封装引脚比 C6T6、ZE 少很多选型前一定查好原理图确认你需要的 PWM 通道和串口引脚能用。网上有很多现成的最小系统板原理图做硬件设计时可以直接参考重点看 USART1PA9/PA10、定时器 PWM 输出脚比如 TIM2 的 PA0~PA3、TIM3 的 PB0/PB1/PB4/PB5是否被其他功能占用。2. 视觉识别与串口通信协议设计2.1 OpenMV 端色块识别与坐标提取这一节的目的是让 OpenMV 识别一个目标色块并把它的中心坐标、颜色 ID 打包发送出去。先看核心代码我用的 OpenMV 开发环境是 OpenMV IDE约 3.3.0 以上版本都试过没问题。import sensor, image, time from machine import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) # 使用 RGB565 图像格式 sensor.set_framesize(sensor.QVGA) # 320x240识别速度与精度折中 sensor.skip_frames(time2000) # 等感光元件稳定 sensor.set_auto_gain(False) # 关闭自动增益防止环境亮度突变影响颜色 sensor.set_auto_whitebal(False) # 同样关闭白平衡保证颜色一致性 # 颜色阈值格式为 LAB 空间的 L Min/Max, A Min/Max, B Min/Max # 红色、绿色、蓝色三个颜色通道具体数值用 IDE 工具调好后填进来 thresholds [(30, 100, 30, 80, 30, 80), # red (40, 80, -50, -20, 20, 60), # green (20, 70, -20, 20, -60, -30)] # blue uart UART(3, 115200, timeout_char1000) # 使用 UART3波特率 115200 color_id 0set_auto_gain(False)和set_auto_whitebal(False)这两行非常关键。如果不关摄像头会在光线稍有变化时自动调整曝光和色偏导致同一个色块的 RGB 值漂移阈值识别就可能突然失明。这就是为什么很多人白天调试好好的换到晚上开灯环境下就识别不了。接下来是主循环对每一帧图像做二值化提取色块筛选最大色块若面积足够就认为是目标并发送坐标。while True: img sensor.snapshot() max_area 0 best_blob None best_id 0 for i, threshold in enumerate(thresholds): blobs img.find_blobs([threshold], pixels_threshold100, area_threshold100, mergeTrue) for b in blobs: if b.area() max_area: max_area b.area() best_blob b best_id i if best_blob and max_area 1500: # 发送区域面积超过 1500 像素才认为有效过滤掉噪点 x best_blob.cx() # 色块中心 X y best_blob.cy() # 色块中心 Y color_id best_id 1 # 1红 2绿 3蓝 send_data(x, y, color_id) else: # 没找到目标时发送 0x00 表示空避免 STM32 拿旧数据重复动作 send_data(0, 0, 0) time.sleep_ms(50)pixels_threshold100和area_threshold100是 OpenMVfind_blobsAPI 里过滤噪点的内置参数100 属于较为保守的设置识别小幅值的空包时比较稳。实际应用中如果物料比较小可以把像素阈值降到 50否则小目标会被过滤掉。2.2 通信协议设计帧头、帧尾、校验位把视觉信息从 OpenMV 传到 STM32这一步看起来简单实际上协议设计好坏直接决定整个系统的稳定性。如果你只是简单地把坐标数值一个一个直接发没有帧头帧尾接收端会面临一个经典问题字节错位。举个实际的例子。OpenMV 连续发送两个包假设第一个包数据结尾的某个字节是 0xAA正好和第二个包的帧头 0xAA 撞在一起STM32 收到后就可能把第 2 个包的最后几个字节误当成新包的帧头导致所有数据偏移一位之后全部解析错误。这个问题在串口通信中极其常见所以一定要用帧头 帧尾 校验的结构。我自己用的协议格式非常简单但很可靠参考了很多工业总线协议的做法后精简下来的帧头1帧头2颜色IDX高8位X低8位Y高8位Y低8位校验和帧尾0xAA0x550x01~0x03datadatadatadataSUM0x0D 0x0A协议长度固定 10 字节X 和 Y 坐标用 16 位拆成高低两个字节发送这样可以兼容 320x240 的图像分辨率最大值都小于 65535。颜色 ID 为 1、2、3分别对应红、绿、蓝0 表示没找到物料。校验和就是把除了帧尾以外所有字节加起来取低 8 位。OpenMV 发送端的 Python 代码实现如下def send_data(x, y, color_id): data bytearray([0xAA, 0x55, color_id, (x 8) 0xFF, x 0xFF, (y 8) 0xFF, y 0xFF, 0x00, 0x0D, 0x0A]) # 校验和除帧尾外所有字节累加取低8位 checksum 0 for i in range(7): checksum data[i] data[7] checksum 0xFF uart.write(data)校验和的作用是防止某个字节在传输过程中被干扰特别是电机启动瞬间容易产生电磁干扰可能导致串口数据出错。STM32 端收到后先算校验、再解析如果校验不通过直接丢弃整个包这样可以避免执行错误的坐标导致机械臂往错误方向跑。2.3 识别不到目标时的处理策略很多新手容易忽略这一块当 OpenMV 没找到目标色块时程序应该发送什么有人直接不发送有人继续发送上一次的坐标这两种做法都有隐患。不发送的后果是STM32 会一直用缓冲区的旧数据机械臂可能会反复执行上一次搬运动作导致重复抓取或空转。继续发旧数据的后果类似更隐蔽因为你可能根本不知道数据其实是过期的。正确做法是发送一个空包颜色 ID 为 0X/Y 坐标也清 0。STM32 收到颜色 ID 为 0 的包后就知道当前没有目标自动让机械臂回到待机位。这样整个系统状态非常清晰也方便通过串口助手上位机实时观察 OpenMV 到底有没有看到东西。我实际调式时发现这个设计对排查“是视觉端没找到、还是控制端不执行”这类问题特别有效加了这个逻辑后几乎不用盲猜。3. STM32 端代码实现与运动控制3.1 工程搭建CubeMX 初始化串口和定时器STM32 这端我用的是标准库当然你们也可以用 HAL 库思路完全一样。如果你是从 CubeMX 生成的工程开始需要配置好几个外设USART1PA9TX、PA10RX波特率 1152008 数据位、1 停止位、无校验使能接收中断。定时器 TIM2输出比较模式四路 PWM频率 50Hz用于控制四个舵机。50Hz 对应周期 20ms舵机标准控制周期。定时器 TIM3如果需要直流电机调速用来产生电机 PWM。几个普通 GPIO控制电机方向、机械臂电磁阀或开关。CubeMX 生成工程后记得在main.c中开启串口接收中断HAL_UART_Receive_IT(huart1, rx_buf[0], 1); // 单字节中断接收不管是用标准库还是 HAL 库核心逻辑完全一致串口收到一个字节就丢进我们自己的接收缓冲区由状态机逐字节解析。3.2 串口中断接收与状态机解析这里我分享一下串口接收的解析方式我用的是状态机 缓冲区的组合相比直接用HAL_UART_Receive等固定长度这种方式可以应对任何长度的数据包而且不会阻塞主循环。先定义几个全局变量#define BUFFER_SIZE 128 uint8_t uart_buf[BUFFER_SIZE]; uint8_t uart_cnt 0; uint8_t proto_state 0; // 0找头1; 1找头2; 2收数据; 3校验; 4帧尾 uint8_t frame[10]; uint8_t frame_index 0;串口中断回调函数中把收到的字节送入解析状态机void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { parse_byte(rx_buf[0]); HAL_UART_Receive_IT(huart1, rx_buf[0], 1); } } void parse_byte(uint8_t data) { switch (proto_state) { case 0: if (data 0xAA) proto_state 2; // 继续找0x55 break; case 1: if (data 0x55) { frame_index 2; proto_state 2; } else if (data 0xAA) { /* 保持 */ } else proto_state 0; break; case 2: frame[frame_index] data; if (frame_index 8) proto_state 3; break; case 3: frame[7] data; // 校验字节 if (check_sum(frame)) proto_state 4; else proto_state 0; break; case 4: if (data 0x0D frame[8] 0x0D) { // 处理完整帧数据 handle_frame(frame); } else if (data 0x0A) { proto_state 0; } break; } }注意这段代码里的状态流转是“收满 8 字节后接着收到 0x0D 0x0A”才算一帧完整结束。实际上我的协议里帧尾是两个字节 0x0D 0x0A所以完整状态机会再多几个状态。上面的写法是为了展示核心逻辑你们实现时按 10 字节协议的完整状态机来写或者干脆换一种更简单的思路。我这里提供一种更实际的做法由于帧长固定 10 字节而且帧头是 0xAA 0x55完全可以做一个带缓冲的“滑窗查找”串口收到任意字节先存缓冲区然后每次检查缓冲区末尾 10 个字节是否满足帧头、帧尾和校验要求。这种方法写起来更简单也更好维护适合项目快速跑通。void parse_stream(uint8_t data) { static uint8_t buffer[16]; static uint8_t idx 0; buffer[idx] data; idx % 16; if (idx 10) { uint8_t start (idx 6) % 16; // 环形缓冲取最老字节的位置 if (buffer[start] 0xAA buffer[(start1)%16] 0x55) { uint8_t frame[10]; for (int i 0; i 10; i) frame[i] buffer[(start i) % 16]; if (check_sum(frame) frame[9] 0x0A frame[8] 0x0D) { handle_frame(frame); } } } }环形缓冲区的好处是不用频繁搬移数据代码也简洁。不过这里有个细节如果数据长度不是 10 的整数倍旧数据仍然保留在缓冲区中有可能会造成重复解析。实际测试时只要保证 OpenMV 发数据包是连续 10 字节一个包中间不用 SLEEP 或留间隙过大问题不大。3.3 舵机控制PWM 脉冲宽度与角度的换算STM32 最终执行动作是通过舵机。舵机的控制原理不复杂它内部有一个参考电路产生周期 20ms、宽度 0.5ms~2.5ms 的脉冲信号舵机输出轴的角度会根据脉冲的高电平时间线性变化。0.5ms 对应 0 度1.5ms 对应 90 度中位2.5ms 对应 180 度。如果配置 TIM2 的 PWM 频率为 50Hz即周期 20msARR 值通常设成 199如果计数频率为 10kHz那占空比每变化 5 就对应 0.1ms 脉冲变化大约 7.2 度。实算一下0.5ms 对应的比较值就是 251.5ms 对应 752.5ms 对应 125。所以在代码中控制舵机角度就是改比较寄存器的值// 以 TIM2 通道1 为例角度范围 0~180 void set_servo_angle(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle) { if (angle 180) angle 180; uint32_t compare 25 (uint32_t)(100.0f * (float)angle / 180.0f); __HAL_TIM_SET_COMPARE(htim, channel, compare); }这里的核心换算逻辑是角度 0 时比较值为 250.5ms角度 180 时比较值为 1252.5ms角度每增加 1 度比较值增量就是 100/180 ≈ 0.556。这样就能把视觉识别到的物体坐标映射成舵机角度带动机械臂完成抓取路径。3.4 核心执行逻辑从坐标到动作STM32 收到完整帧后handle_frame()负责解析出颜色 ID、X、Y 坐标并作出动作决策。最简单可靠的搬运逻辑是按物料在画面中的位置分区把画面分成左、中、右三区物料在左区则让机械臂往左摆在右区则往右摆然后执行抓取、抬升、平移、放下、复位。void handle_frame(uint8_t *frame) { uint8_t color_id frame[2]; uint16_t x (frame[3] 8) | frame[4]; uint16_t y (frame[5] 8) | frame[6]; if (color_id 0) { set_servo_angle(htim2, TIM_CHANNEL_1, 90); // 回到中位待机 return; } uint8_t angle 0; if (x 107) // 画面左边 angle 45; else if (x 213) // 画面右边 angle 135; else // 中间 angle 90; set_servo_angle(htim2, TIM_CHANNEL_1, angle); // 水平旋转关节 HAL_Delay(300); if (color_id 1) // 红色物料抓取后放到指定区 { set_servo_angle(htim2, TIM_CHANNEL_2, 30); // 前伸抓取 HAL_Delay(500); set_servo_angle(htim2, TIM_CHANNEL_2, 150); // 抬升 HAL_Delay(500); // 移到红色物料投放区省略舵机组合动作 } else if (color_id 2) // 绿色物料 { // 另一套动作 } else if (color_id 3) // 蓝色物料 { // 另一套动作 } }这种“分区 固定角度”的执行策略在实训项目中非常实用因为它把视觉坐标映射简化为有限的几个执行位不需要复杂的路径规划可靠性和可调试性都大幅提高。如果你想更高级可以把目标 X 坐标线性映射为舵机角度的连续量比如angle 30 x * 120 / 320这样机械臂会随着物料位置连续变化视觉效果更丝滑但调试时也会更费劲。4. 常见问题排查与避坑技巧实录4.1 通信乱码、丢帧、数据错位这是整个项目里出现频率最高的问题十个人里有六个人卡在这。最常见的几个原因按概率排序第一没共地。OpenMV 的 GND 和 STM32 的 GND 必须接在一起否则串口电平没有参考点收到的数据一定是乱码。检查方法很简单用示波器量 TX 线或者直接串口助手看是否有稳定波形。第二波特率不匹配。OpenMV 端和 STM32 端的波特率必须完全一致不要一边设 115200、一边设 9600 还怪代码写错。推荐 115200兼顾速度和稳定性。第三串口中断优先级太低。STM32 的串口接收中断优先级如果被设置得很低外部中断或定时器中断频繁抢占时可能丢字节。把HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)设成最高优先级即可。第四电机/舵机大电流造成的电磁干扰。舵机启动瞬间电流能到 1A 以上如果电源滤波没做好会严重干扰串口数据。常规解决办法是舵机供电和逻辑供电分开我在硬件架构章节里特意强调了独立供电原因就在这。4.2 识别不稳定、阈值漂移、误识别OpenMV 在光照稳定的实验室环境下识别色块非常可靠但一换环境就容易出幺蛾子。最常见的坑是忘关自动增益和白平衡导致阈值在运行中悄悄变化。所以我建议在初始化阶段强制关闭 auto gain 和 auto whitebal然后固定曝光时间sensor.set_auto_exposure(False, exposure_us20000)还有一个坑是阈值选择太宽导致把背景物误识别为物料。建议用 OpenMV IDE 自带的“阈值编辑器”工具它能实时显示摄像头看到的画面和每个颜色通道的直方图你可以用鼠标框选目标区域直接导出 LAB 空间的最优阈值区间。我一般会多选几个不同的光照条件取一个交集作为最终阈值这样鲁棒性更好。如果你需要在不同场景下切换阈值可以把多组阈值存成类似数据集的方式运行时根据环境选择加载。OpenMV 支持 TF 卡可以用json或文本文件存阈值需要时读入项目大了以后这是一个很好的扩展方向。4.3 舵机抖动、机械臂动作不流畅很多同学反馈舵机装上机械臂后一直抖或者动作一顿一顿。原因通常是两个电源电流不够或者 PWM 频率不对。舵机尤其是 MG996R启动瞬间电流很大如果直接用 STM32 板载的 3.3V 或 USB 供电电压会被拉低导致舵机控制信号失真表现为抖动、无力、甚至复位。解决方案是给舵机单独接 5V~6V 电源且电源输出电流至少 2A同时在舵机电源引脚并联一个大电容470uF 以上做缓冲。PWM 频率问题则要看舵机型号有些舵机对 50Hz 特别敏感频率偏高或偏低都会造成抖动。标准舵机用 50Hz20ms 周期就行我用的 SG90 和 MG996R 在 50Hz 下都很稳。如果你是数字舵机可能要用更高的频率具体看舵机规格书。4.4 问题排查速查表整理一个排查表项目跑不通的时候按表里顺序检查比自己瞎试快得多。现象可能原因解决方法串口收到乱码未共地 / 波特率不一致检查 GND 连接统一波特率接收数据偶尔丢一包中断优先级低 / 电源干扰提高串口中断优先级舵机独立供电OpenMV 识别不到目标光照变化 / 阈值太严格关自动曝光、白平衡重新用阈值编辑器调参画面有目标但 STM32 不动串口没收到 / 解析协议不匹配用 USB 转串口监听 OpenMV 发出数据对比协议格式舵机抖动或无力电源供电不足加独立 5V/2A 电源和电容机械臂执行动作偏差大坐标映射错误 / 舵机安装角度偏移校准舵机 0 度位置调整角度偏移量程序跑飞或卡死串口数据长度不对 / 状态机无法恢复给状态机加超时复位任意字节错位 3 秒后自动归零4.5 几个值得收藏的经验技巧最后分享几个我用过后觉得特别好用的小技巧。调试串口时先用 USB 转 TTL 模块做中线检查。在 OpenMV 和 STM32 还没对接之前分别用串口助手测试先让 OpenMV 把数据发给电脑看格式是否正确再用电脑模拟 OpenMV 发同样的数据给 STM32看解析是否正常。这样一次只排一个环节的问题而不是两边同时怀疑。这个步骤能节省至少一半调试时间。善用板载 LED 做指示。STM32 板载 LED 可以用来指示“帧头收到”“校验成功”“动作执行中”等状态。我在调试时把 LED 状态和串口状态绑定问题肉眼就能看出是卡在协议哪个阶段不用每次都接逻辑分析仪。给机械臂做一个物理限位。舵机如果角度超出机械结构允许范围轻则效率下降重则烧舵机。建议在代码里做角度上下限保护并在机械结构上加装限位块。安全第一这句话在嵌入式里真的不是套话。如果想偷懒可以用 Arduino 框架开发 STM32。把 STM32F103C8T6 当作 Arduino 开发板来写代码用Serial和Servo库代码量会少很多。我之前给懒得配 CubeMX 的同学试过能跑通但受库封装限制做复杂状态机时不如直接用寄存器或 HAL 方便所以建议把标准库或 HAL 库作为首选。就我个人经验来说这类视觉搬运机器人项目做得顺不顺利很大程度不是取决于代码多高级而是取决于系统各部分之间“接口”设计得好不好。通信协议定了串口调试通了视觉识别逻辑用固定曝光和阈值调整稳定下来剩下的动作执行其实就是把舵机角度编排好。你最应该花时间打磨的是数据的流转链路从像素坐标到串口帧、从串口帧到控制指令、从控制指令到真实的物理位移每一层都要有明确的职责和可观测性。只要这一条链路通了后续想加颜色分拣、加传送带跟随、加远程监控都只是在现有框架上做增量而已。本文还有配套的精品资源点击获取