ESP8266+STM32F407+OV7670实现图片传输

2021年9月17日 22点热度 0条评论 来源: 溪午X
   声明:由于ESP8266与STM32之间采用串口进行通讯,导致传输速率较低,一到两秒才可以传输一帧图片,因此无法实现实时的图像显示。
  本文虽然以串口通讯的方式进行数据传输,但是建议想要实现实时视频显示的朋友:想要提高无线传输的速率,必须放弃串口通讯的方式,采用如SPI或SDIO的通讯方式作为STM32单片机与ESP8266(或其他无线模块)进行通讯。

正文开始

  1. 硬件结构

  本文使用到的硬件设备主要为STM32F407单片机、ESP8266无线WIFI模块、OV7670摄像头、LCD显示屏幕以及计算机。整体结构如下图所示:

  上图中的LCD显示屏幕只是为了将OV7670采集到的图片在下位机进行显示,与图片传输没有什么关系,并且若增加LCD显示图像功能的话,会进一步影响图片传输的速率。因此不需要在下位机显示的朋友可以不用LCD显示屏幕,直接将图片数据通过ESP8266传输到上位机进行显示。

工作方式:

   此处对上图硬件结构中各个模块是如何工作的进行简单的介绍,首先OV7670摄像机模块在单片机的驱动下,采集图像,然后单片机通过串口将采集到的图像传输给ESP8266无线WIFI模块,该模块已经提前设置为无线透传模式(后文会介绍无线透传模式的设置方式),在透传模式下,ESP8266会将单片机串口发送来的所有数据,以无线的方式自动发送给与其连接好的上位机软件,上位机软件通过对接收到的图像数据进行处理,就能够显示出OV7670采集到的图片,本文使用到的上位机软件为C#语言编写,通过TCP/IP通讯方式与ESP8266进行通讯,并可以将传输到的数据处理成图片进行显示,后文会对上位机软件进行详细介绍。

  2. STM32驱动OV7670采集图片

  本文没有使用DCMI接口来驱动摄像机,而是使用IO口,实现OV7670图像数据的采集,而且其采集速率可以达到每秒24帧以上。以下为OV7670的IO口驱动代码:

/初始化GPIO
u8 OV7670_Init(void)
{ 
	u8 temp;
	u16 i=0;	
	
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOB\
			      |RCC_AHB1Periph_GPIOC|RCC_AHB1Periph_GPIOE\
			      |RCC_AHB1Periph_GPIOG, ENABLE);//时钟使能
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6;//D1 2 3 4 5 6 7数据线
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; 
	GPIO_Init(GPIOE, &GPIO_InitStructure);      //IO口初始化
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;   //D0数据线
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//VSYNC 中断输入线
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	/*output*/
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_6;//RRST及PCLK
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 	
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //WRST 
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_15;//WEN及OE
	GPIO_Init(GPIOG, &GPIO_InitStructure);
	
	SCCB_Init();   //SCCB(与IIC类似)IO口初始化
  
	if(SCCB_WR_Reg(0x12,0x80))return 1;	//复位SCCB 
	delay_ms(50); 
/
	//读取产品ID
	//读取产品型号
 	temp=SCCB_RD_Reg(0x0b);   
	if(temp!=0x73)return 2;  
 	temp=SCCB_RD_Reg(0x0a);   
	if(temp!=0x76)return 2;
	
/
	//初始化序列 (该部分代码非常关键,也是整个底层驱动的核心。由厂家提供)
	for(i=0;i<sizeof(ov7670_init_reg_tbl)/sizeof(ov7670_init_reg_tbl[0]);i++)
	{ 
	   	SCCB_WR_Reg(ov7670_init_reg_tbl[i][0],ov7670_init_reg_tbl[i][1]);
  }
  return 0x00; 	//ok
}

  根据OV7670的驱动代码,正确连接接线,搭配LCD显示屏幕,就可以显示摄像机采集到的图像,速率大于24帧每秒,下图为LCD摄像头视频显示。
在使用杜邦线进行接线的时候,一定注意要将数据线和信号线分开各自单独捆起来,否则信号之间会发生干扰,导致花屏现象。如果使用排针插口,则不存在这个问题。

  3. ESP8266无线透传模式设置

  一般情况下,在网上购买的ESP8266模块内置AT固件库,用户使用串口助手通过简单的AT指令就可以将ESP8266设置为不同的模式(总共有3种模式)。但是,如果用户购买的模块没有内置固件库,则需要用户自己烧写固件。烧写固件库很简单,网上由很多教程,本文重点不在此,因此不做过多介绍。

ESP8266透传模式设置步骤:
  1. 设置为客户端模式:AT+CWMODE=1 设置工作模式
  2. 查询是否设置成功:AT+CWMODE? 查询工作模式
  3. 加入AP: AT+CWJAP=“XXXX”,“XXXX”,上述指令中,使用者应该根据实际情况,填写所要连接的热点或者WIFI的名称以及密码
  4. 重启ESP8266: AT+RST 重启
  5. 获取本模块IP看一下是否连接上了: AT+CIFSR
  6. 设置为透传模式:AT+CIPMODE=1
  7. 连接到你自己的服务器(可以用网络调试助手进行测试): AT+CIPSTART=“TCP”,“IP地址”,端口
  8. 进入透传:AT+CIPSEND

   ESP8266模块按照上述步骤与PC连接好并设置为透传模式之后,就可以与PC进行通讯。本文使用STM32采集OV7670的图像,然后利用串口将采集到的图像数据传输给ESP8266,ESP8266会自动将串口传输来的数据发送给PC端的上位机软件,在上位机中就可以实现图片的显示。

  4. STM32主函数图像数据更新代码

  以下代码为STM32主程序,包括OV7670图像帧的刷新以及数据发送。

//OV7670图像刷新函数
void camera_refresh(void)
{ 
	u32 j;
 	u16 color;
	u8 re[640]={ 0};
	u16 flag=0;
	
	if(ov_sta)
	{ 
		LCD_Scan_Dir(U2D_L2R);		//LCD扫描方式

	  if(lcddev.id==0X1963)LCD_Set_Window((lcddev.width-240)/2,(lcddev.height-320)/2,240,320);//½«ÏÔʾÇøÓòÉèÖõ½ÆÁÄ»ÖÐÑë
		else if(lcddev.id==0X5510||lcddev.id==0X5310)LCD_Set_Window((lcddev.width-320)/2,(lcddev.height-240)/2,320,240);//½«ÏÔʾÇøÓòÉèÖõ½ÆÁÄ»ÖÐÑë
		
		LCD_WriteRAM_Prepare();     
		
		OV7670_RRST=0;				//开始复位读指针
		OV7670_RCK_L;
		OV7670_RCK_H;
		OV7670_RCK_L;
		OV7670_RRST=1;				//复位读指针结束
		OV7670_RCK_H;

		for(j=0;j<76800;j++)   //根据分辨率,读取数据。
		{ 
			OV7670_RCK_L;
            color=OV7670_DATA; 
            re[flag++]=color;  
			
            OV7670_RCK_H; 
            color<<=8; 
            OV7670_RCK_L;
            color|=OV7670_DATA; 
			re[flag++]=color; 
			
            OV7670_RCK_H; 
			
			LCD->LCD_RAM=color;
			if(flag==640)     
			{ 
				Send_data_3(re); //使用串口3将数据传输给ESP8266,每次发送640个字节。
				flag=0;
			}
		}  
 		ov_sta=0;					//清除帧中断标志。
		ov_frame++; 
		LCD_Display_Dir(0);
	} 
}


//LCD相关模式参数
const u8*LMODE_TBL[5]={ "Auto","Sunny","Cloudy","Office","Home"};
const u8*EFFECTS_TBL[7]={ "Normal","Negative","B&W","Redish","Greenish","Bluish","Antique"};	//7ÖÖÌØЧ 

int main()
{ 
	VO7670_flag=0;
	SysTick_Init(168);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //ÖжÏÓÅÏȼ¶·Ö×é ·Ö2×é
	LED_Init();
	KEY_Init();

	USART3_Init(115200);
	USART1_Init(115200);
	
	LCD_Init();           //LCD初始化函数
	POINT_COLOR=RED;      //画笔颜色设置
	
	LCD_ShowString(30, 70, 200, 16, 16, "OV7670 Test");
	
	while(OV7670_Init())
	{ 
		LCD_ShowString(30, 230, 200, 16, 16, "OV7670 Error!");
		delay_ms(200);
        LCD_Fill(30,230,239,246,WHITE);
        delay_ms(200);
	}
	
	LCD_ShowString(30, 230, 200, 16, 16, "OV7670 OK!");
	delay_ms(1500);
	
	OV7670_Light_Mode(0);
	OV7670_Color_Saturation(2);
	OV7670_Brightness(2);
	OV7670_Contrast(2);
 	OV7670_Special_Effects(0);
	
    TIM4_Init(20000,7199); 
    EXTI8_Init();

    OV7670_Window_Set(12,176,240,320);
    OV7670_CS=0;
    LCD_Clear(BLACK);
	
	while(1)
	{ 
		if(VO7670_flag==1)  //该参数是有上位机控制。可以去掉
		{ 
			camera_refresh(); //更新显示图像或无线传输图像数据
			VO7670_flag=0;
		}
	}

  5. 上位机软件

   上位机主要用于与ESP8266进行无线连接,并将ESP8266发送来的图片数据重新编码为图片并进行显示,本文使用C#语言编写上位机软件,通过TCP/IP协议与ESP8266进行通讯,软件界面如下图所示。
   软件界面中间黑色区域为图像显示区域,下方白色TextBox为数据发送区域,左侧TextBox为数据接收显示区域(程序设置为图片数据不显示)。由于下位机单片机采集到的图片数据格式为RGB565,因此上位机内部需要将接收到的RGB565格式的图像数据转化为RGB24,才可以正常将接收到的图片数据显示为图片。

   该上位机主要功能包括TCO/IP协议以及图像数据解码,具体代码如下所示:

private void Start_Click(object sender, EventArgs e)
        { 
            panel1.BackColor = System.Drawing.Color.LightSkyBlue;
            panel4.BackColor = System.Drawing.Color.LightSkyBlue;

            panel3.BackColor = System.Drawing.Color.LightSteelBlue;
            //1.创建服务器端用于监听的SOCKET
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //2.绑定IP和端口
            IPAddress ip = IPAddress.Parse(textBox_IP.Text);
            IPEndPoint ipandport = new IPEndPoint(ip, int.Parse(textBox_Port.Text));
            serverSocket.Bind(ipandport);

            //3.开启监听
            serverSocket.Listen(10);

            //使用线程池技术,服务器端接受客户端的连接
            ThreadPool.QueueUserWorkItem(new WaitCallback(AcceptClientConnect), serverSocket);
        }

        /// <summary>
        /// 接受客户端的连接
        /// </summary>
        /// <param name="socket"></param>
        public void AcceptClientConnect(object socket)
        { 
            //传递过来的服务器端SOCKET
            Socket serverSocket = socket as Socket;
            this.AppendToMessage("服务器开始接受客户端的连接请求。");

            //4.服务器端开始接受客户端的连接
            //此处服务器端应该一直循环,用来一直接收可能的客户端。
            while (true)
            { 
                //创建代理Socket,用来与客户端进行通讯。
                Socket proxsocket = serverSocket.Accept();
                //将代理socker放到窗体级别的集合中
                proxSocketList.Add(proxsocket);
                AppendToMessage(string.Format("客户端:{0}已经连接上。", proxsocket.RemoteEndPoint.ToString()));
                //ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveClientMessage),proxsocket);
                Thread MyThread = new Thread(new ParameterizedThreadStart(ReceiveClientMessage));
                MyThread.IsBackground = true;
                MyThread.Start(proxsocket);
                Thread.Sleep(100);
            }
        }

     
        /// <summary>
        /// 接收客户端发送的消息
        /// </summary>
        /// <param name="proxsocket"></param>
        public void ReceiveClientMessage(object socket)
        { 
            int flag= 0;
            //服务器端与客户端之间用于通讯的socket
            Socket proxSocket = socket as Socket;

            //开辟用来存储客户端发送来的数据的控件
            byte[] dataClient = new byte[640*240];

            proxSocket.ReceiveTimeout = 100 * 2;            //设置接收数据时的阻塞时间为15秒钟
            

            //接收客户端数据
            while (true)    //总共有240列数据,每列数据为640个字节,两个字节组成一个像素点。
            { 
                try
                { 
                    proxSocket.Receive(dataClient, 0, dataClient.Length, SocketFlags.None);
                }
                catch (SocketException e)
                { 
                    if (e.ErrorCode == 10060)
                    { 
                        hang = 0;
                        lie = 0;
                        flag = 0;
                        continue;
                    }
                    else
                    { 
                        //客户端非正常退出。
                        proxSocketList.Remove(proxSocket);
                        AppendToMessage(string.Format("客户端:{0}非正常退出。", proxSocket.RemoteEndPoint.ToString()));
                        return; //线程终结
                    }
                }
                
                //绘制一列数据
                if(flag<240)
                { 
                    Paint_bmp(dataClient);
                    flag++;
                }
                else
                { 
                    hang = 0;
                    lie = 0;
                    flag = 0;
                }
            }
        }

        /// <summary>
        /// 根据接收的数组绘制一副图像
        /// 注意:byte[] Data数据中的数据不一定为一副图像的完整数据
        /// 若数据不完整,下次调用时将自动完成绘制
        /// </summary>
        /// <param name="Data"></param>
        void Paint_bmp(byte[] Data)
        { 
            int i = 0;

            //foreach (byte color in Data)
            for(i=0;i<Data.Length;i++)
            { 
                if (isheight)                           //判断是否为高位
                { 
                    isheight = false;
                    heightdate = Data[i];
                }
                else
                { 
                    isheight = true;                    //若为低8位,则转化颜色,并写入bmp
                    Color c = RGB565ToRGB24(heightdate, Data[i]);
                    Write_Color(c);
                }
            }
        }

        /// <summary>
        /// 将一个16位的RGB565格式颜色转化成RGB24格式颜色,并返回Color类
        /// </summary>
        /// <param name="RGB565_H"></param>
        /// <param name="RGB565_L"></param>
        /// <returns></returns>
        Color RGB565ToRGB24(int RGB565_H, int RGB565_L)
        { 
            int RGB565_MASK_RED = 0xF800;
            int RGB565_MASK_GREEN = 0x07E0;
            int RGB565_MASK_BLUE = 0x001F;
            int RGB565;
            int R, G, B;
            RGB565_H <<= 8;
            RGB565 = RGB565_H | RGB565_L;
            R = (RGB565 & RGB565_MASK_RED) >> 11;
            G = (RGB565 & RGB565_MASK_GREEN) >> 5;
            B = (RGB565 & RGB565_MASK_BLUE);
            R <<= 3;
            G <<= 2;
            B <<= 3;
            return Color.FromArgb(R, G, B);
        }
        /// <summary>
        /// 在bmp中写入一个像素点的颜色,并自动将指针向下一个是像素点移动
        /// 当指针移动到像素点的最后一个像素时,将返回图像起点
        /// </summary>
        /// <param name="c"></param>
        void Write_Color(Color c)
        { 
            if(c.R==0&&c.B==0&c.G==0)
            { 
                return;
            }
            //使用委托在子线程操作主线程界面。
            this.Invoke((EventHandler)(delegate {  bmp.SetPixel(hang, lie, c); }));
            if (lie < bmp_height - 1)
            { 
                lie++;
            }
            else
            { 
                lie = 0;
                this.BeginInvoke((EventHandler)(delegate {  pictureBox1.Image = bmp; }));
                if (hang < bmp_width - 1)
                { 
                    hang++;
                }
                else
                { 
                    hang = 0;
                }
            }
        }

   最终的显示结果如下图所示:

   上图完成了最终的图像数据无线传输,并在上位机中完成了图像的解码,其中图像显示区域右侧黑色区域是因为图像显示区域设置的比真实图片的分辨率略大。后续可以进行改进。此外,由于单片机每发送一次数据,除原本的数据之后,貌似好有多余的几个字节的数据,导致图片显示出现些许差错,后续也可以进行改进。

上位机源代码:https://download.csdn.net/download/qq_37855507/16496001

    原文作者:溪午X
    原文地址: https://blog.csdn.net/qq_37855507/article/details/105831054
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。