1.前言(闲话)

最近在做电磁炮,发现题目需要用到颜色跟踪,于是花了一点时间学了一下OpenMV,只学习OpenMV是远远不够的,还需要实现与单片机的通信。我选择使用OLED显示传输的数据,在这里调试了许久,中间遇到了许多之前的学习漏洞,特在此写下博客记录学习经历。*



2.硬件连接
我所用到的材料如下: 四针IIC OLED,OpenMV(OV7725),STM32F103C8T6最小系统板,数据线N条(OpenMV的数据线只能用官方自带的,其他的基本都用不了),杜邦线若干。
1.OpenMV端:由图知UART_RX—P5 ------ UART_TX—P4
b3eedd535b67416bb009aa1f84eb6f26~noop.image?_iz=58558&from=article.jpg


2.STM32端:USART_TX—PA9 -----USART_RX—PA10
e6784ecfa2e44ac086b0c3822643d691~noop.image?_iz=58558&from=article.jpg

3.四针OLED IIC连接:SDA—PA2-----SCL—PA1 由于使用的是模拟IIC而不是硬件IIC,可以根据个人需要修改IO口来控制SDA线和SCL线,只需要简单修改一下代码即可。
4.STM32的TX(RX)接OpenMV的RX(TX),OLED连接到STM32即可。


3.软件代码———OpenMV端
  1. import sensor, image, time,math,pyb
  2. from pyb import UART,LED
  3. import json
  4. import ustruct
  5. sensor.reset()
  6. sensor.set_pixformat(sensor.RGB565)
  7. sensor.set_framesize(sensor.QVGA)
  8. sensor.skip_frames(time = 2000)
  9. sensor.set_auto_gain(False)
  10. sensor.set_auto_whitebal(False)
  11. red_threshold_01=(10, 100, 127, 32, -43, 67)
  12. clock = time.clock()
  13. uart = UART(3,115200)   
  14. uart.init(115200, bits=8, parity=None, stop=1)
  15. def find_max(blobs):   
  16.     max_size=0
  17.     for blob in blobs:
  18.         if blob.pixels() > max_size:
  19.             max_blob=blob
  20.             max_size = blob.pixels()
  21.     return max_blob
  22. def sending_data(cx,cy,cw,ch):
  23.     global uart;
  24.    
  25.    
  26.     data = ustruct.pack("<bbhhhhb",      
  27.                    0x2C,                     
  28.                    0x12,                     
  29.                    int(cx),
  30.                    int(cy),
  31.                    int(cw),
  32.                    int(ch),
  33.                    0x5B)
  34.     uart.write(data);   
  35. while(True):
  36.     clock.tick()
  37.     img = sensor.snapshot()
  38.     blobs = img.find_blobs([red_threshold_01])
  39.     cx=0;cy=0;
  40.     if blobs:
  41.                 max_b = find_max(blobs)
  42.             
  43.             cx=max_b[5]
  44.             cy=max_b[6]
  45.             cw=max_b[2]
  46.             ch=max_b[3]
  47.             img.draw_rectangle(max_b[0:4])
  48.             img.draw_cross(max_b[5], max_b[6])
  49.             FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
  50.             
  51.             uart.write(FH)
  52.             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的代码存在版权问题,需要的可以邮箱私发。
  1.     /***** oled.h *****/
  2. void USART1_Init(void);//串口1初始化并启动
  3.     /***** oled.c *****/
  4. static u8 Cx=0,Cy=0,Cw=0,Ch=0;
  5. void USART1_Init(void)
  6. {
  7.                  //USART1_TX:PA 9   
  8.                 //USART1_RX:PA10
  9.                 GPIO_InitTypeDef GPIO_InitStructure;     //串口端口配置结构体变量
  10.                 USART_InitTypeDef USART_InitStructure;   //串口参数配置结构体变量
  11.                 NVIC_InitTypeDef NVIC_InitStructure;     //串口中断配置结构体变量
  12.                 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);        
  13.                 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //打开PA端口时钟
  14.             //USART1_TX   PA9
  15.             GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;                           //PA9
  16.                    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                   //设定IO口的输出速度为50MHz
  17.             GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;                            //复用推挽输出
  18.             GPIO_Init(GPIOA, &GPIO_InitStructure);                               //初始化PA9
  19.             //USART1_RX          PA10
  20.             GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;             //PA10
  21.             GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //浮空输入
  22.             GPIO_Init(GPIOA, &GPIO_InitStructure);                 //初始化PA10
  23.             //USART1 NVIC 配置
  24.             NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  25.                 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;  //抢占优先级0
  26.                 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;                    //子优先级2
  27.                 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                                                                                       //IRQ通道使能
  28.                 NVIC_Init(&NVIC_InitStructure);                                  //根据指定的参数初始化VIC寄存器
  29.             //USART 初始化设置
  30.                 USART_InitStructure.USART_BaudRate = 115200;                  //串口波特率为115200
  31.                 USART_InitStructure.USART_WordLength = USART_WordLength_8b;   //字长为8位数据格式
  32.                 USART_InitStructure.USART_StopBits = USART_StopBits_1;        //一个停止位
  33.                 USART_InitStructure.USART_Parity = USART_Parity_No;           //无奇偶校验位
  34.                 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;   //无硬件数据流控制
  35.                 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                          //收发模式
  36.             USART_Init(USART1, &USART_InitStructure);                     //初始化串口1
  37.             USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能中断
  38.                    USART_Cmd(USART1, ENABLE);                     //使能串口1
  39.                   USART_ClearFlag(USART1, USART_FLAG_TC);        //清串口1发送标志
  40.                
  41. }
  42. //USART1 全局中断服务函数
  43. void USART1_IRQHandler(void)                        
  44. {
  45.                 u8 com_data;
  46.                 u8 i;
  47.                 static u8 RxCounter1=0;
  48.                 static u16 RxBuffer1[10]={0};
  49.                 static u8 RxState = 0;        
  50.                 static u8 RxFlag1 = 0;
  51.                 if( USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET)             //接收中断  
  52.                 {
  53.                                 USART_ClearITPendingBit(USART1,USART_IT_RXNE);   //清除中断标志
  54.                                 com_data = USART_ReceiveData(USART1);
  55.                         
  56.                                 if(RxState==0&&com_data==0x2C)  //0x2c帧头
  57.                                 {
  58.                                         RxState=1;
  59.                                         RxBuffer1[RxCounter1++]=com_data;OLED_Refresh();
  60.                                 }
  61.                
  62.                                 else if(RxState==1&&com_data==0x12)  //0x12帧头
  63.                                 {
  64.                                         RxState=2;
  65.                                         RxBuffer1[RxCounter1++]=com_data;
  66.                                 }
  67.                
  68.                                 else if(RxState==2)
  69.                                 {
  70.                                         RxBuffer1[RxCounter1++]=com_data;
  71.                                         if(RxCounter1>=10||com_data == 0x5B)       //RxBuffer1接受满了,接收数据结束
  72.                                         {
  73.                                                 RxState=3;
  74.                                                 RxFlag1=1;
  75.                                                 Cx=RxBuffer1[RxCounter1-5];
  76.                                                 Cy=RxBuffer1[RxCounter1-4];
  77.                                                 Cw=RxBuffer1[RxCounter1-3];
  78.                                                 Ch=RxBuffer1[RxCounter1-2];
  79.                                         }
  80.                                 }
  81.                
  82.                                 else if(RxState==3)                //检测是否接受到结束标志
  83.                                 {
  84.                                                 if(RxBuffer1[RxCounter1-1] == 0x5B)
  85.                                                 {
  86.                                                                         USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);//关闭DTSABLE中断
  87.                                                                         if(RxFlag1)
  88.                                                                         {
  89.                                                                         OLED_Refresh();
  90.                                                                         OLED_ShowNum(0, 0,Cx,3,16,1);
  91.                                                                         OLED_ShowNum(0,17,Cy,3,16,1);
  92.                                                                         OLED_ShowNum(0,33,Cw,3,16,1);
  93.                                                                         OLED_ShowNum(0,49,Ch,3,16,1);
  94.                                                                         }
  95.                                                                         RxFlag1 = 0;
  96.                                                                         RxCounter1 = 0;
  97.                                                                         RxState = 0;
  98.                                                                         USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
  99.                                                 }
  100.                                                 else   //接收错误
  101.                                                 {
  102.                                                                         RxState = 0;
  103.                                                                         RxCounter1=0;
  104.                                                                         for(i=0;i<10;i++)
  105.                                                                         {
  106.                                                                                         RxBuffer1[i]=0x00;      //将存放数据数组清零
  107.                                                                         }
  108.                                                 }
  109.                                 }
  110.         
  111.                                 else   //接收异常
  112.                                 {
  113.                                                 RxState = 0;
  114.                                                 RxCounter1=0;
  115.                                                 for(i=0;i<10;i++)
  116.                                                 {
  117.                                                                 RxBuffer1[i]=0x00;      //将存放数据数组清零
  118.                                                 }
  119.                                 }
  120.                 }
  121.                
  122. }
解释:OpenMV发送数据包给STM32,STM32利用中断接收数据并把数据存放在RxBuffer1这个数组里,并且在中断中利用OLED显示cx,cy,cw,ch四个坐标。在中断中,有如下函数:
  1. else if(RxState==2)
  2.                                 {
  3.                                         RxBuffer1[RxCounter1++]=com_data;
  4.                                         if(RxCounter1>=10||com_data == 0x5B)       //RxBuffer1接受满了,接收数据结束
  5.                                         {
  6.                                                 RxState=3;
  7.                                                 RxFlag1=1;
  8.                                                 Cx=RxBuffer1[RxCounter-5];
  9.                                                 Cy=RxBuffer1[RxCounter-4];
  10.                                                 Cw=RxBuffer1[RxCounter-3];
  11.                                                 Ch=RxBuffer1[RxCounter1-2];
  12.                                         }
  13.                                 }
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,定义了:
  1. u8 com_data;
  2. u8 i;
  3. static u8 RxCounter1=0;
  4. static u8 RxBuffer1[10]={0};
  5. static u8 RxState = 0;        
  6. 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)中使用语句:
  1. DATA=bytearray[(1,2,3,4,5)]
  2. 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头文件,大循环中使用如下语句:
  1. while(1)
  2. {
  3.                 printf("HelloWorld!");
  4. }
打开串口助手查看是否接收到了数据。


6.学习补充 (代码看不懂的时候可以来看一下)
补充1:static关键字(静态变量)的使用
static 修饰全局函数和全局变量,只能在本源文件使用。举个例子,比如用以下语句static u8 RxBuffer[10] 定义了一个名为RxBuffer的静态数组,数组元素类型为unsigned char型。在包含Rxbuffer的源文件中,Rxbuffer相当于一个全局变量,任意地方修改RxBuffer的值,RxBuffer都会随之改变。而且包含RxBuffer的函数在多次运行后RxBuffer的值会一直保存(除非重新赋值)。在C语言学习中,利用static关键字求阶乘是一个很好的例子:
  1. long fun(int n);
  2. void main()
  3. {
  4.     int i,n;
  5.     printf("input the value of n:");
  6.     scanf("%d",&n);
  7.     for(i=1;i<=n;i++)
  8.     {
  9.         printf("%d! = %1d\n",i,fun(i));
  10.     }
  11. }
  12. >long fun(int n)
  13. {
  14.     static long p=1;
  15.     p=p*n;
  16.     return p;
  17. }
效果为依次输出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 关键字加以声明即可。下面就来看一个多文件的示例:
  1. #include <stdio.h>
  2. extern int g_X ;
  3. extern int g_Y ;
  4. int max()
  5. {
  6.     return (g_X > g_Y ? g_X : g_Y);
  7. }
  8. #include <stdio.h>
  9. int g_X=10;
  10. int g_Y=20;
  11. int max();
  12. int main(void)
  13. {
  14.     int result;
  15.     result = max();
  16.     printf("the max value is %d\n",result);
  17.     return 0;
  18. }
  19. 运行结果为:
  20. 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中
  1. data = ustruct.pack("<bbhhhhb",      #格式为俩个字符俩个短整型(2字节)
  2.                    0x2C,                      #帧头1
  3.                    0x12,                      #帧头2
  4.                    int(cx), # up sample by 4   #数据1
  5.                    int(cy), # up sample by 4    #数据2
  6.                    int(cw), # up sample by 4    #数据1
  7.                    int(ch), # up sample by 4    #数据2
  8.                    0x5B)
""bbhhhhb"简单来说就是要发送数据的声明,bbhhhhb共七个,代表发送七个数据,对照下面的表,可以知道七个数据按时序发送为unsigner char、unsigned char、short、short、short、short、unsigned char。0x2c为数据帧的帧头,即检测到数据流的开始,但是一个帧头可能会出现偶然性,因此设置两个帧头0x2c与0x12以便在中断中检测是否检测到了帧头以便存放有用数据。0x5b为帧尾,即数据帧结束的标志。
3050c1a063be4347bba057505f4d316e~noop.image?_iz=58558&from=article.jpg 2.bytearray([ , , , ])函数:
用于把十六进制数据以字节形式存放到字节数组中,以便以数据帧的形式发送出去进行通信。
  1. FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
  2. uart,write(FH)
  3. 12

7.效果展示(可以先来看效果)
2bc0a2f594db473a80472cefc186e998~noop.image?_iz=58558&from=article.jpg

从上到下依次为CX,CY,CW,CH


8.博客更新
1.有朋友反馈OpenMv端找不到色块就会报错,解决方案如下:
  1. while(True):
  2.     clock.tick()
  3.     img = sensor.snapshot()
  4.     blobs = img.find_blobs([red_threshold_01])
  5.     cx=0;cy=0;
  6.     if blobs:
  7.                 max_b = find_max(blobs)
  8.             #如果找到了目标颜色
  9.             cx=max_b[5]
  10.             cy=max_b[6]
  11.             cw=max_b[2]
  12.             ch=max_b[3]
  13.             img.draw_rectangle(max_b[0:4]) # rect
  14.             img.draw_cross(max_b[5], max_b[6]) # cx, cy
  15.             FH = bytearray([0x2C,0x12,cx,cy,cw,ch,0x5B])
  16.             #sending_data(cx,cy,cw,ch)
  17.             uart.write(FH)
  18.             print(cx,cy,cw,ch)
在以上代码中,将max_b = find_max(blobs) 移到if blobs外即可

2.有朋友反馈OpenMV发送数据只能发送一个字节,也就是说大于255的数据无法直接通过代码完成,现在提供以下解决方案:在STM32端代码中依次保存大于255数字的高八位和低八位最后在组合在一起即可。
2021/9/15更新 4字节与浮点数之间的转换(参考)
  1. #if 1
  2. int main()
  3. {
  4.         #if 0
  5.         
  6.         float m = 23.25;
  7.         unsigned char *a;
  8.         a = (unsigned char *)&m;
  9.         printf("0x%x \n0x%x \n0x%x \n0x%x \n",a[0],a[1],a[2],a[3]);
  10.         
  11.         #endif
  12.         
  13.         #if 1
  14.         
  15.         unsigned char a[]={0x00,0x00,0xba,0x41};
  16.         float BYTE;
  17.         BYTE = *(float *)&a;
  18.         printf("%f\n",BYTE);
  19.         #endif
  20. }
  21. #endif
上述代码实现了将四个字节转换为一个浮点数的功能,同时也实现了将一个浮点数拆分为四个字节功能。在Openmv传数据时,只能传输一个字节,大于255的数无法以一字节形式发送,因此可以在Openmv端将该数据拆分成两个字节,分别发送给Stm32端,同时Stm32端对传来的数据进行合成,合成并解析为对应的数据。
另一种解决方案:python传数据的1/2,单片机在乘2即可。



9.参考链接[1]extern外部变量参考链接
[2]星瞳科技OpenMV中文参考手册官方
[3]MicroPython函数库


10.完整版代码链接完整版代码链接(点赞收藏免费哦)
免费啦
链接:
https://pan.baidu.com/s/1rCocKyECcyssLqFs3xWlvA
提取码:hsg6