超詳細OpenMV與STM32單片機通信 附完整源碼

1.前言(閑話)

最近在做電磁炮,發現題目需要用到顏色跟蹤,於是花瞭一點時間學瞭一下OpenMV,隻學習OpenMV是遠遠不夠的,還需要實現與單片機的通信,本以為很簡單,在CSDN上找瞭一些代碼,直接拿來修改粘貼,把代碼看明白瞭,這些隻花瞭幾個小時,本以為自己已經弄明白瞭二者之間的通信,但是在後期把OpenMV端數據傳輸到單片機的時候卻犯瞭難。我選擇使用OLED顯示傳輸的數據,在這裡調試瞭許久,中間遇到瞭許多之前的學習漏洞,特在此寫下博客記錄學習經歷。*


2.硬件連接

我所用到的材料如下: 四針IIC OLED,OpenMV(OV7725),STM32F103C8T6最小系統板,數據線N條(OpenMV的數據線隻能用官方自帶的,其他的基本都用不瞭),杜邦線若幹。

1.OpenMV端:由圖知UART_RX—P5 —— UART_TX—P4

OpenMV引腳說明圖


2.STM32端:USART_TX—PA9 —–USART_RX—PA10

STM32F10xC8T6

3.四針OLED IIC連接:SDA—PA2—–SCL—PA1 由於使用的是模擬IIC而不是硬件IIC,可以根據個人需要修改IO口來控制SDA線和SCL線,隻需要簡單修改一下代碼即可。
4.STM32的TX(RX)接OpenMV的RX(TX),OLED連接到STM32即可。


3.軟件代碼———OpenMV端

import sensor, image, time,math,pyb
from pyb import UART,LED
import json
import ustruct

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False) # must be turned off for color tracking
sensor.set_auto_whitebal(False) # must be turned off for color tracking
red_threshold_01=(10, 100, 127, 32, -43, 67)
clock = time.clock()

uart = UART(3,115200)   #定義串口3變量
uart.init(115200, bits=8, parity=None, stop=1) # init with given parameters

def find_max(blobs):    #定義尋找色塊面積最大的函數
    max_size=0
    for blob in blobs:
        if blob.pixels() > max_size:
            max_blob=blob
            max_size = blob.pixels()
    return max_blob


def sending_data(cx,cy,cw,ch):
    global uart;
    #frame=[0x2C,18,cx%0xff,int(cx/0xff),cy%0xff,int(cy/0xff),0x5B];
    #data = bytearray(frame)
    data = ustruct.pack("<bbhhhhb",      #格式為倆個字符倆個短整型(2字節)
                   0x2C,                      #幀頭1
                   0x12,                      #幀頭2
                   int(cx), # up sample by 4   #數據1
                   int(cy), # up sample by 4    #數據2
                   int(cw), # up sample by 4    #數據1
                   int(ch), # up sample by 4    #數據2
                   0x5B)
    uart.write(data);   #必須要傳入一個字節數組


while(True):
    clock.tick()
    img = sensor.snapshot()
    blobs = img.find_blobs([red_threshold_01])
    cx=0;cy=0;
    if blobs:
        	max_b = find_max(blobs)
            #如果找到瞭目標顏色
            cx=max_b[5]
            cy=max_b[6]
            cw=max_b[2]
            ch=max_b[3]
            img.draw_rectangle(max_b[0:4]) # rect
            img.draw_cross(max_b[5], max_b[6]) # cx, cy
            FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
            #sending_data(cx,cy,cw,ch)
            uart.write(FH)
            print(cx,cy,cw,ch)

bytearray([, , ,])組合uart.write()的作用與直接調用sending_data(cx,cy,cw,ch)作用是一樣的


4.軟件代碼———STM32端

工程總共包含如下文件:main.c、iic.c、iic.h、oled.c、oled.h、uart.c、uart.h。由於OLED的代碼存在版權問題,需要的可以郵箱私發。

/***** oled.h *****/

#ifndef __USART_H
#define __USART_H
#include "sys.h"
void USART1_Init(void);//串口1初始化並啟動
#endif

/***** oled.c *****/

#include "uart.h"
#include "oled.h"
#include "stdio.h"

static u8 Cx=0,Cy=0,Cw=0,Ch=0;

void USART1_Init(void)
{
	 	//USART1_TX:PA 9   
		//USART1_RX:PA10
		GPIO_InitTypeDef GPIO_InitStructure;     //串口端口配置結構體變量
		USART_InitTypeDef USART_InitStructure;   //串口參數配置結構體變量
		NVIC_InitTypeDef NVIC_InitStructure;     //串口中斷配置結構體變量

		RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //打開PA端口時鐘

    	//USART1_TX   PA9
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;          		 //PA9
   		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  		 //設定IO口的輸出速度為50MHz
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	   		 //復用推挽輸出
    	GPIO_Init(GPIOA, &GPIO_InitStructure);             	 	 //初始化PA9
    	//USART1_RX	  PA10
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;             //PA10
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //浮空輸入
    	GPIO_Init(GPIOA, &GPIO_InitStructure);                 //初始化PA10 

    	//USART1 NVIC 配置
    	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
		NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;  //搶占優先級0
		NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;		    //子優先級2
		NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			      					                 //IRQ通道使能
		NVIC_Init(&NVIC_InitStructure);	                          //根據指定的參數初始化VIC寄存器

    	//USART 初始化設置
		USART_InitStructure.USART_BaudRate = 115200;                  //串口波特率為115200
		USART_InitStructure.USART_WordLength = USART_WordLength_8b;   //字長為8位數據格式
		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);                     //初始化串口1

    	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能中斷
   		USART_Cmd(USART1, ENABLE);                     //使能串口1
	  	USART_ClearFlag(USART1, USART_FLAG_TC);        //清串口1發送標志
		
}

//USART1 全局中斷服務函數
void USART1_IRQHandler(void)			 
{
		u8 com_data; 
		u8 i;
		static u8 RxCounter1=0;
		static u16 RxBuffer1[10]={0};
		static u8 RxState = 0;	
		static u8 RxFlag1 = 0;

		if( USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET)  	   //接收中斷  
		{
				USART_ClearITPendingBit(USART1,USART_IT_RXNE);   //清除中斷標志
				com_data = USART_ReceiveData(USART1);
			
				if(RxState==0&&com_data==0x2C)  //0x2c幀頭
				{
					RxState=1;
					RxBuffer1[RxCounter1++]=com_data;OLED_Refresh();
				}
		
				else if(RxState==1&&com_data==0x12)  //0x12幀頭
				{
					RxState=2;
					RxBuffer1[RxCounter1++]=com_data;
				}
		
				else if(RxState==2)
				{
					RxBuffer1[RxCounter1++]=com_data;

					if(RxCounter1>=10||com_data == 0x5B)       //RxBuffer1接受滿瞭,接收數據結束
					{
						RxState=3;
						RxFlag1=1;
						Cx=RxBuffer1[RxCounter1-5];
						Cy=RxBuffer1[RxCounter1-4];
						Cw=RxBuffer1[RxCounter1-3];
						Ch=RxBuffer1[RxCounter1-2];

					}
				}
		
				else if(RxState==3)		//檢測是否接受到結束標志
				{
						if(RxBuffer1[RxCounter1-1] == 0x5B)
						{
									USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);//關閉DTSABLE中斷
									if(RxFlag1)
									{
									OLED_Refresh();
									OLED_ShowNum(0, 0,Cx,3,16,1);
									OLED_ShowNum(0,17,Cy,3,16,1);
									OLED_ShowNum(0,33,Cw,3,16,1);
									OLED_ShowNum(0,49,Ch,3,16,1);
									}
									RxFlag1 = 0;
									RxCounter1 = 0;
									RxState = 0;
									USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
						}
						else   //接收錯誤
						{
									RxState = 0;
									RxCounter1=0;
									for(i=0;i<10;i++)
									{
											RxBuffer1[i]=0x00;      //將存放數據數組清零
									}
						}
				} 
	
				else   //接收異常
				{
						RxState = 0;
						RxCounter1=0;
						for(i=0;i<10;i++)
						{
								RxBuffer1[i]=0x00;      //將存放數據數組清零
						}
				}

		}
		
}

解釋:OpenMV發送數據包給STM32,STM32利用中斷接收數據並把數據存放在RxBuffer1這個數組裡,並且在中斷中利用OLED顯示cx,cy,cw,ch四個坐標。在中斷中,有如下函數:

else if(RxState==2)
				{
					RxBuffer1[RxCounter1++]=com_data;

					if(RxCounter1>=10||com_data == 0x5B)       //RxBuffer1接受滿瞭,接收數據結束
					{
						RxState=3;
						RxFlag1=1;
						Cx=RxBuffer1[RxCounter-5];
						Cy=RxBuffer1[RxCounter-4];
						Cw=RxBuffer1[RxCounter-3];
						Ch=RxBuffer1[RxCounter1-2];

					}
				}

RxBuffer1是一個裝有接收OpenMV數據的數組,RxCounter1起著一個計數器的作用,當RxBuffer[RxCounter1-1]存放的數據為數據包的幀位時,說明已經接收成功整個數據包,此時RxBuffer[RxCounter1-2]存放ch坐標值,RxBuffer[RxCounter1-3]存放cw坐標值,RxBuffer[RxCounter1-4]存放cy坐標值,RxBuffer[RxCounter1-5]存放cx坐標值,此後在RxState=3過程中將這四個坐標顯示出來即可。
特別註意的是:STM32中斷每發生一次,隻會接收到一字節的數據,因此,進行七次才會接收完一整幀的數據包,這一點需要讀者仔細揣摩,結合上文中說的靜態變量關鍵字static,定義瞭:

u8 com_data; 
u8 i;
static u8 RxCounter1=0;
static u8 RxBuffer1[10]={0};
static u8 RxState = 0;	
static u8 RxFlag1 = 0;

請讀者仔細揣摩為什麼com_data(端口接收到的數據)、i定義的是動態的(auto),而RxBuffer1(裝接收到數據的靜態全局數組)、RxState(狀態標志變量)、RxFlag1(接受結束標志變量)定義的確實靜態的,這一點並不難理解。


5.利用PC端測試數據數據是否發送接收正常

在進行OpenMV與STM32的通信測試過程中,我使用瞭USB轉TTL模塊,將OpenMV(或STM32單片機)與PC端進行通信確保數據發出或者接收正常。
OpenMV&&PC
OpenMV_RX接模塊TX
OpenMV_TX接模塊RX
OpenMV_GND接模塊GND
然後打開OpenMV,在大循環while(True)中使用語句:

DATA=bytearray[(1,2,3,4,5)]
uart.write(DATA)

打開PC端串口助手,註意設置一樣的波特率、停止位、發送字節數等,查看串口助手是否接受到瞭數據。
STM32&&PC
STM32_RX接模塊TX
STM32_TX接模塊RX
STM32_GND接模塊GND
註意:不管是STM32與PC還是OpenMV與PC還是STM32與OpenMV通信,都要將二者的GND連接在一起。
在main.c中先調用stdio頭文件,大循環中使用如下語句:

while(1)
{
		printf("HelloWorld!");
}

打開串口助手查看是否接收到瞭數據。


6.學習補充 (代碼看不懂的時候可以來看一下)

補充1:static關鍵字(靜態變量)的使用

static 修飾全局函數和全局變量,隻能在本源文件使用。舉個例子,比如用以下語句static u8 RxBuffer[10] 定義瞭一個名為RxBuffer的靜態數組,數組元素類型為unsigned char型。在包含Rxbuffer的源文件中,Rxbuffer相當於一個全局變量,任意地方修改RxBuffer的值,RxBuffer都會隨之改變。而且包含RxBuffer的函數在多次運行後RxBuffer的值會一直保存(除非重新賦值)。在C語言學習中,利用static關鍵字求階乘是一個很好的例子:

#include“stdio.h”
long fun(int n);
void main()
{
    int i,n;
    printf("input the value of n:");
    scanf("%d",&n);
    for(i=1;i<=n;i++)
    {
        printf("%d! = %1d\n",i,fun(i));
    }
}
>long fun(int n)
{
    static long p=1; 
    p=p*n;
    return p;
}

效果為依次輸出n!(n=1,2,3…n)
這個例子中,第一次p的值為1,第二次p的值變成瞭p x n=1 x 2=2,這個值會一直保存,如果p沒有定義為靜態類型,那麼在第一次運算過後p的值會重新被賦值為1,這就是auto型(不聲明默認為auto型)與static型的最大區別。

總結:static關鍵字定義的變量是全局變量,在static所包含的函數多次運行時,該變量不會被多次初始化,隻會初始化一次。

補充2:extern關鍵字(外部變量)的使用

程序的編譯單位是源程序文件,一個源文件可以包含一個或若幹個函數。在函數內定義的變量是局部變量,而在函數之外定義的變量則稱為外部變量,外部變量也就是我們所講的全局變量。它的存儲方式為靜態存儲,其生存周期為整個程序的生存周期。全局變量可以為本文件中的其他函數所共用,它的有效范圍為從定義變量的位置開始到本源文件結束。
如果整個工程由多個源文件組成,在一個源文件中想引用另外一個源文件中已經定義的外部變量,同樣隻需在引用變量的文件中用 extern 關鍵字加以聲明即可。下面就來看一個多文件的示例:

/****max.c****/
#include <stdio.h>
/*外部變量聲明*/
extern int g_X ;
extern int g_Y ;
int max()
{
    return (g_X > g_Y ? g_X : g_Y);
}
/***main.c****/
#include <stdio.h>
/*定義兩個全局變量*/
int g_X=10;
int g_Y=20;
int max();
int main(void)
{
    int result;
    result = max();
    printf("the max value is %d\n",result);
    return 0;
}
運行結果為:
the max value is 20

對於多個文件的工程,都可以采用上面這種方法來操作。對於模塊化的程序文件,可在其文件中預先留好外部變量的接口,也就是隻采用 extern 聲明變量,而不定義變量,max.c 文件中的 g_X 與 g_Y 就是如此操作的。比如想要在主函數中調用usart.c中的變量x,usart.c中有著這樣的定義:static u8 x=0在usart.h中可以這樣寫:extern u8 x在main.c中包含usart.h頭文件,這樣在編譯的時候就會在main.c中調用x外部變量。

總結:extern關鍵字是外部變量,靜態類型的全局變量,可以在源文件中調用其他文件中的變量,在多文件工程中配合頭文件使用。

補充3:MicroPython一些庫函數的解釋

1.ustruct.pack函數:
import ustruct,在ustruct中

data = ustruct.pack("<bbhhhhb",      #格式為倆個字符倆個短整型(2字節)
                   0x2C,                      #幀頭1
                   0x12,                      #幀頭2
                   int(cx), # up sample by 4   #數據1
                   int(cy), # up sample by 4    #數據2
                   int(cw), # up sample by 4    #數據1
                   int(ch), # up sample by 4    #數據2
                   0x5B)

“”bbhhhhb”簡單來說就是要發送數據的聲明,bbhhhhb共七個,代表發送七個數據,對照下面的表,可以知道七個數據按時序發送為unsigner char、unsigned char、short、short、short、short、unsigned char。0x2c為數據幀的幀頭,即檢測到數據流的開始,但是一個幀頭可能會出現偶然性,因此設置兩個幀頭0x2c與0x12以便在中斷中檢測是否檢測到瞭幀頭以便存放有用數據。0x5b為幀尾,即數據幀結束的標志。

在這裡插入圖片描述

2.bytearray([ , , , ])函數:
用於把十六進制數據以字節形式存放到字節數組中,以便以數據幀的形式發送出去進行通信。

FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
uart,write(FH)

7.效果展示(可以先來看效果)

在這裡插入圖片描述 

從上到下依次為CX,CY,CW,CH


8.博客更新

1.有朋友反饋OpenMv端找不到色塊就會報錯,解決方案如下:

while(True):
    clock.tick()
    img = sensor.snapshot()
    blobs = img.find_blobs([red_threshold_01])
    cx=0;cy=0;
    if blobs:
        	max_b = find_max(blobs)
            #如果找到瞭目標顏色
            cx=max_b[5]
            cy=max_b[6]
            cw=max_b[2]
            ch=max_b[3]
            img.draw_rectangle(max_b[0:4]) # rect
            img.draw_cross(max_b[5], max_b[6]) # cx, cy
            FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
            #sending_data(cx,cy,cw,ch)
            uart.write(FH)
            print(cx,cy,cw,ch)

在以上代碼中,將max_b = find_max(blobs) 移到if blobs外即可


2.有朋友反饋OpenMV發送數據隻能發送一個字節,也就是說大於255的數據無法直接通過代碼完成,現在提供以下解決方案:在STM32端代碼中依次保存大於255數字的高八位和低八位最後在組合在一起即可。
2021/9/15更新 4字節與浮點數之間的轉換(參考)

#if 1 
int main()
{
	#if 0
	//字符型數據分成四個字節存放在數組中
	float m = 23.25;
	unsigned char *a;
	a = (unsigned char *)&m;
	printf("0x%x \n0x%x \n0x%x \n0x%x \n",a[0],a[1],a[2],a[3]);
	
	#endif
	
	#if 1
	//四個字節數據合成存放在數組中
	unsigned char a[]={0x00,0x00,0xba,0x41};
	float BYTE;
	BYTE = *(float *)&a;
	printf("%f\n",BYTE);
	#endif
}
#endif

上述代碼實現瞭將四個字節轉換為一個浮點數的功能,同時也實現瞭將一個浮點數拆分為四個字節功能。在Openmv傳數據時,隻能傳輸一個字節,大於255的數無法以一字節形式發送,因此可以在Openmv端將該數據拆分成兩個字節,分別發送給Stm32端,同時Stm32端對傳來的數據進行合成,合成並解析為對應的數據。
另一種解決方案:python傳數據的1/2,單片機在乘2即可。


9.參考鏈接

[1]extern外部變量參考鏈接
[2]星瞳科技OpenMV中文參考手冊官方
[3]MicroPython函數庫


10.完整版代碼鏈接

完整版代碼鏈接(點贊收藏免費哦)

到此這篇關於超詳細OpenMV與STM32單片機通信 附完整源碼的文章就介紹到這瞭,更多相關OpenMV與STM32單片機通信內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: