基于OpenMV与STM32的数据通信项目(代码开源)

发布于:2024-05-14 ⋅ 阅读:(87) ⋅ 点赞:(0)

前言:本文为手把手教学 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 直接封装为案例,极大地方便研发人员的后续使用。当然,本篇博客最核心部分是 OpenMVSTM32 的通信部分的处理,包含数据包的处理编程!

三、传输数据包协议

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发送端完整代码

在上述官方提供的 OpenMV2 个例程代码的基础上结合项目实际情况进行编写代码。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.96OLED 将 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() 解析:

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

STM32 接收端需要根据 OpenMV 发送端的数据包格式进行解码,HAL_UART_Receive_IT() 函数稳定将接收到的数据赋值 USART1_RXbuff ,通过 OpenMV_Data_Receive() 函数进行解码。根据上述 OpenMV 发送端的代码,可以得出需要首先解码帧头的 0xFE 与 0xBCOpenMV_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文库

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