前言:本文为手把手教学 OpenMV 与 STM32 的数据通信项目教程,本教程使用 STM32F103C8T6 与 OpenMV 进行操作。 OpenMV 是非常强大的计算机视觉实现工具,自身提供了非常多的视觉项目案例,编程与使用门槛极低。为了进一步增强作品的功能与创意性,往往需要将 OpenMV 的视觉与 STM32 的控制融合,本篇博客将为读者朋友教学使用 UART 串口构建两者的快速数据通信。希望本篇博客能给读者朋友的工程项目或科研生活给予些许帮助,敬礼Respect!!!(篇末代码开源!)

实验硬件:OpenMV;STM32F103C8T6;0.96寸OLED;杜邦线若干

项目实物图:

项目效果图:

引脚连接:

OpenMV 与 STM32 引脚:

OpenMV.Rx -> STM32.PA9

OpenMV.Tx -> STM32.PA10

STM32 与 0.96寸OLED 引脚:

STM32.PA6 -> OLED.SCL

STM32.PA7 -> OLED.SDA

STM32.VCC -> OLED.VCC

STM32.GND -> OLED.GND

一、OpenMV概述

1.1 OpenMV

OpenMV 是一个开源、功能强大的机器视觉模块。通常以 STM32F767/STM32H743/STM32F427 为 CPU 核心,集成 OV7725 等欧姆龙系列摄像头芯片,在小巧的硬件模块上,用C语言高效地实现了核心机器视觉算法,并提供 MicroPython 编程接口,拥有专属的编程平台 OpenMV IDE 程序。创客或电赛选手往往忠爱使用 OpenMV 为自己的产品和发明增加有特色的竞争力。作者补充:实话实说目前 OpenMV 在计算机视觉上的帮助确实很大,可是考虑到如今的 OpenMV 的售价是非常不合适的!

OpenMV 的整体设计小巧,使用门槛低,使得它拥有很强的 "视觉DIY” 属性。 OpenMV 还拥有丰富的外设资源,例如:UART、I2C、SPI、PWM、ADC、DAC以及GPIO等接口,方便扩展外围功能。USB接口用于连接电脑上的集成开发环境 OpenMV IDE,协助完成编程、调试和更新固件等工作。TF卡槽支持大容量的TF卡,可以用于存放程序和保存照片等。

作者推荐 OpenMV 学习网站:

官方网站:Download – OpenMV

OpenMV中国官方代理(星瞳科技):序言 · OpenMV中文入门教程

1.2 OpenMV项目

中国 OpenMV 官方代理是星瞳科技,星瞳科技在其官网提供了超多详细且丰富的 OpenMV 使用案例,例如:特征点检测、测距、扫描识别、寻找色块、模板匹配、颜色形状识别与人脸识别等。

上述图片中的案例都是可以借助 OpenMV 进行实现的,当然考虑到 STM32F7/STM32H7 等系列 CPU 算力的上限,可能输出图像像素以及 FPS 并不是特别优秀的。有能力和专研精神的读者朋友可以尝试高级的计算机视觉开发工具,例如:Jeston Nano、K210、K510、RK3568、RK3588与树莓派4/5B系列等(部分产品的性能与算力非常有竞争力)!

补充提醒:本项目中使用 OpenMV 的数字识别作为案例,进行与 STM32 之间的数据通信!

二、博客项目概述

2.1 OpenMV的Mnist数字识别

OpenMV 提供了超级多的计算机视觉的案例,作者选择常用的 mnist 数字识别项目作为 OpenMV 终端处理的事件(电赛送药小车题目与之类似),该案例可以直接通过星瞳科技官网进行获取(老旧版本的 OpenMV 可能需要升级固件才能使用该案例):

案例地址:Mnist数字识别 · OpenMV中文入门教程

作者手上的 OpenMV 为 OpenMV3 R1,CPU 的处理性能非常一般。官方在 OpenMV4 H7 Plus上面运行大概每秒 45 帧,在 OpenMV4 H7上面运行大概每秒 25 帧左右。mnist 数字识别案例使用了 CNN 卷积神经网络进行识别,例程利用 mnis t数字数据集,自行训练神经网络得到手写数字识别神经网络模型,性能和准确率很高(可以直接使用案例的权重文件即可)。

★运行目录前,将官网提供的 mnist 数字识别的 trained.tflite 文件下载到电脑,并复制到 OpenMV 的存储中。

mnist数字识别代码:

# This code run in OpenMV4 H7 or OpenMV4 H7 Plus

import sensor, image, time, os, tf

sensor.reset() # Reset and initialize the sensor.

sensor.set_pixformat(sensor.GRAYSCALE) # Set pixel format to RGB565 (or GRAYSCALE)

sensor.set_framesize(sensor.QVGA) # Set frame size to QVGA (320x240)

sensor.set_windowing((240, 240)) # Set 240x240 window.

sensor.skip_frames(time=2000) # Let the camera adjust.

clock = time.clock()

while(True):

clock.tick()

img = sensor.snapshot().binary([(0,64)])

for obj in tf.classify("trained.tflite", img, min_scale=1.0, scale_mul=0.5, x_overlap=0.0, y_overlap=0.0):

output = obj.output()

number = output.index(max(output))

print(number)

print(clock.fps(), "fps")

案例测试:

2.2 项目整体说明

OpenMV 集成了非常多的库函数,常用的数据通信使用 UART 串口,本篇博客就以 UART 通信为例。

本项目利用 OpenMV 的数字识别案例进行数字识别,将识别到的数字信息通过 OpenMV的 UART 串口发送至 STM32F103C8T6。而 STM32F103C8T6 通过 I2C 协议将 OpenMV 传输过来的数字信息显示在 0.96 寸的 OLED 屏幕上。该项目的整体实现还是非常简单,特别是计算机视觉的数字识别部分,OpenMV 直接封装为案例,极大地方便研发人员的后续使用。当然,本篇博客最核心部分是 OpenMV 与 STM32 的通信部分的处理,包含数据包的处理编程!

三、传输数据包协议

3.1 数据包通信概述

传输完全体数据包可以包含:帧头、地址信息、数据类型、数据长度、数据块、校验码、帧尾。正常情况下,考虑到传输速率问题不会使用完全体数据包。大多数情况下,工程师仅使用简版数据包:帧头、数据字节长度与帧尾!

传输数据包的过程包含 2 个部分:(1) 数据包编码,上文所说的数据包组成;(2) 数据包解析,下文所说的数据包解析;

传输数据过程中的数据包解析通常有 2 种方式:(1)、中断内部解析;(2)、中断外部解析;

第一种方法:中断服务函数内部直接解析使用,该方法适用于数据帧简单,数据复杂程度低的情况。可以满足中断函数的快进快出,该方法可以使整个项目代码框架简洁,方便后期纠错改正!!!

第二种方法:中断服务函数外部解析使用,该方法适用于数据帧繁杂,数据复杂程度高的情况。该情况下,往往无法满足中断服务函数的快进快出,容易卡死在中断内部。这种情况下,工程师可以在中断中只接收数据,随后通过 extern 全局变量将数据在外部进行解析处理。实际工程中,该方法使用可能性高,希望读者朋友可以完全掌握该技能!!!

本篇博客项目使用中断内部解析数据包的方法,该方法也是作者电赛常用手段之一(部分情况下解析完的数据可能需要数据融合或是滤波处理,该情况使不适合在中断服务函数中解析的)

3.2 数据包传输(HEX方式)

数据包传输方式是机器设备间通信最常见的方法,数据包传输方式一般分为 3 种:(1) 固定包长,含帧头帧尾;(2) 可变包长,含帧头帧尾;(3) 可变包长,含数据字节长度及帧头帧尾;详情如下图所示:

作者补充说明:上图中的帧头为 0xFE,帧尾为 0xEF;这里的帧头和帧尾是可以自定义的,但通常情况下会选择帧头为 0xFE,帧尾为 0xEF,这是为什么呢?

答:通常帧头和帧尾的设计需要避免与通信过程中的数据具有相似性,不然容易导致误把通信数据当初帧头帧尾进行处理,从而解析出错误的数据!当然,复杂的数据包帧头也可以不局限于 1 个字节,读者朋友可以根据自己实际情况设计。作者项目使用直接使用了帧头为 0xFE,帧尾为 0xEF 的数据包进行传输!

四、CubeMX配置

1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug设置成Serial Wire(否则可能导致芯片自锁);

3、USART1配置:设置UART1串口;波特率:115200;开启UART串口中断;

4、I2C配置:设置I2C1与 0.96 寸OLED进行通信;

5、时钟树配置

6、工程配置

五、代码与解析

5.1 OpenMV数据发生端代码

5.1.1 OpenMV的串口数据传输

星瞳科技官网提供了 OpenMV 的串口 UART 的使用案例,升级到最新版固件就可以直接运行。作者使用 CH340 芯片将串口数据上传至电脑终端进行测试(读者朋友搞工程的时候,也建议按部就班的搭建和完善代码流程)。

OpenMV 串口通信代码:

# This code run in OpenMV4 H7 or OpenMV4 H7 Plus

import sensor, image, time, os, tf

from pyb import UART

sensor.reset() # Reset and initialize the sensor.

sensor.set_pixformat(sensor.GRAYSCALE) # Set pixel format to RGB565 (or GRAYSCALE)

sensor.set_framesize(sensor.QVGA) # Set frame size to QVGA (320x240)

sensor.set_windowing((240, 240)) # Set 240x240 window.

sensor.skip_frames(time=2000) # Let the camera adjust.

#OpenMV串口UART传输数据

uart = UART(3, 115200) # 实例化一个串口3,波特率为115200,必须与STM32接收端保持一致

clock = time.clock()

while(True):

clock.tick()

img = sensor.snapshot().binary([(0,64)])

for obj in tf.classify("trained.tflite", img, min_scale=1.0, scale_mul=0.5, x_overlap=0.0, y_overlap=0.0):

output = obj.output()

number = output.index(max(output))

print(number)

print(clock.fps(), "fps")

uart.write("Hello World!\r")

5.1.2 OpenMV发送端完整代码

在上述官方提供的 OpenMV 的 2 个例程代码的基础上结合项目实际情况进行编写代码。OpenMV 只能传输十六进制的数据给 STM32,否则 STM32 将收不到数据,就是单片机和 OpenMV 都能正常和电脑通信,但是两者结合就不能正常通信。十六进制数据的实现主要通过 bytearray() 这个函数,代码格式如下:OUT_DATA =bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])

代码解析:通过定义 Sending_Data() 函数,进行 OpenMV 端的数据发送。在 mnist 数字识别的 while 函数的 for 循环中将识别到的 number 数据包持续 Sending_Data() 发送到 STM32 开发板上。

mnist.py代码:

# This code run in OpenMV4 H7 or OpenMV4 H7 Plus

import sensor, image, time, os, tf

from pyb import UART

sensor.reset() # Reset and initialize the sensor.

sensor.set_pixformat(sensor.GRAYSCALE) # Set pixel format to RGB565 (or GRAYSCALE)

sensor.set_framesize(sensor.QVGA) # Set frame size to QVGA (320x240)

sensor.set_windowing((240, 240)) # Set 240x240 window.

sensor.skip_frames(time=2000) # Let the camera adjust.

#OpenMV串口UART传输数据

uart = UART(3, 115200) # 实例化一个串口3,波特率为115200,必须与STM32接收端保持一致

#定义数据包发送函数

def Sending_Data(Num):

global uart;

OutData = bytearray([0xFE,0xBC,Num,0xEF]) #构建发送数据的数据包

uart.write(OutData); #必须要传入一个字节数组

clock = time.clock()

while(True):

clock.tick()

img = sensor.snapshot().binary([(0,64)])

for obj in tf.classify("trained.tflite", img, min_scale=1.0, scale_mul=0.5, x_overlap=0.0, y_overlap=0.0):

output = obj.output()

number = output.index(max(output))

Sending_Data(number)

print(number)

print(clock.fps(), "fps")

mnist数字识别数据传输:

5.2 STM32数据接收端代码

5.2.1 0.96寸OLED代码

本篇博客项目中使用 0.96 寸 OLED 将 OpenMV 识别的 mnist 数字结果进行输出,0.96 寸的 OLED 驱动代码可以参考作者的另一篇博客。考虑到博客篇幅有限,0.96 寸 OLED 驱动就不详细赘述了,希望读者朋友可以自行掌握!

博客地址:http://t.csdnimg.cn/gDcev

5.2.2 STM32接收端完整代码

代码解析:本篇项目代码中 STM32 接收端关键操作都是依赖于 HAL_UART_RxCpltCallback() 函数实现的。OpenMV 与 STM32 数据传输过程中的解码在中断回调函数中直接通过 OpenMV_Data_Receive() 函数实现。USART1_RXbuff 变量为 USART1 开启后持续传输的数据,将该变量放入 OpenMV_Data_Receive()  进行解码。

★核心函数 OpenMV_Data_Receive() 解析:

OpenMV 与 STM32 数据传输稍微复杂点的其实就是 STM32 接收端的解码过程,常规情况下 OpenMV 发送端的数据是一组数据包。这组数据包的组成是程序员自己定义的,比如作者 OpenMV 端的数据包格式为:0xFE,0xBC,Num,0xEF。其中,0xFE,0xBC 为帧头,Num 为需要解码出的真正数据,0xEF 为帧尾。

STM32 接收端需要根据 OpenMV 发送端的数据包格式进行解码,HAL_UART_Receive_IT() 函数稳定将接收到的数据赋值 USART1_RXbuff ,通过 OpenMV_Data_Receive() 函数进行解码。根据上述 OpenMV 发送端的代码,可以得出需要首先解码帧头的 0xFE 与 0xBC,OpenMV_Data_Receive() 函数中定义 RxBuffer[4] 数组来接收每一帧的数据(作者每一帧数据有 4 个字节数据,读者朋友可以根据实际情况设置数组大小),设置 RxState 状态位来递进判断是否正确接收到目标数据。在成功接收到 2 个帧头数据之后,通过 OLED_ShowNum() 函数将 OpenMV 识别出的数字显示出来。

 关键点:串口接收中断回调函数

/* USER CODE BEGIN PTD */

uint8_t USART1_RXbuff; //中断数据接收缓冲区

/* USER CODE END PTD */

HAL_UART_Receive_IT(&huart1,(void *)&USART1_RXbuff,1); /* 开启串口中断接收 */

/* USER CODE BEGIN 4 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

{

uint16_t tempt;

if(huart->Instance==USART1)

{

tempt=USART1_RXbuff;

OpenMV_Data_Receive(tempt);

}

HAL_UART_Receive_IT(&huart1,(void *)&USART1_RXbuff,1); //再次开启中断接收

}

/* USER CODE END 4 */

openmv.h:

#ifndef __OPENMV_H

#define __OPENMV_H

#include "stm32f1xx.h"

void OpenMV_Data_Receive(int16_t OpenMV_Data); /* STM32接收端处理OpenMV传输的数据 */

#endif

openmv.c:

/********************************* (C) COPYRIGHT **********************************

* File Name : openmv.c

* Author : 混分巨兽龙某某

* Version : V1.0.0

* Data : 2023/11/03

* Contact : QQ:1178305328

* Description : OpenMV and STM32 Communication Files

***********************************************************************************/

#include "openmv.h"

#include "usart.h"

#include "stdio.h"

#include "oled.h"

static uint8_t Number = 0;

/* STM32接收端处理OpenMV传输的数据 */

void OpenMV_Data_Receive(int16_t OpenMV_Data)

{

/* 计数变量 */

static uint8_t RxCounter=0; //计数变量

/* 数据接收数组 */

static uint16_t RxBuffer[4]={0};

/* 数据传输状态位 */

static uint8_t RxState = 0;

/* 判断数据是否为有效数据,解码 */

if(RxState == 0 && OpenMV_Data == 0xFE) //0xFE帧头

{

RxState = 1; //状态位改变

RxBuffer[RxCounter++] = OpenMV_Data; //将数据放入接收数组

}

else if(RxState == 1 && OpenMV_Data == 0xBC) //0xBC帧头

{

RxState = 2; //状态位改变

RxBuffer[RxCounter++] = OpenMV_Data; //将数据放入接收数组

}

else if(RxState == 2) //读取目标数据(根据实际情况处理)

{

RxBuffer[RxCounter++] = OpenMV_Data; //将数据放入接收数组

if(RxCounter>=3||OpenMV_Data == 0xEF)

{

RxState = 3; //状态位改变

Number = RxBuffer[RxCounter-1];

/* OLED显示目标数字 */

OLED_ShowNum(65,4,Number,3,16);

}

}

else if(RxState == 3) //检测是否接收到标志位

{

if(RxBuffer[RxCounter-1] == 0xEF)

{

/* 计数和状态位归零 */

RxCounter = 0;

RxState = 0;

}

else //接收错误

{

/* 计数和状态位归零 */

RxCounter = 0;

RxState = 0;

/* 清空存放数据的数组 */

for(int i = 0;i < 4; i++)

{

RxBuffer[i] = 0x00;

}

}

}

else //整体的接收异常

{

/* 计数和状态位归零 */

RxCounter = 0;

RxState = 0;

/* 清空存放数据的数组 */

for(int i = 0;i < 4; i++)

{

RxBuffer[i] = 0x00;

}

}

}

 项目:

六、项目视频

OpenMV与STM32数据传输演示视频

七、代码开源

代码地址: 基于OpenMV与STM32的数据传输项目代码资源-CSDN文库

如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

相关链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: