本文利用STM32F407单片机、OV2640摄像机模块以及ESP8266 WIFI模块,并基于C#编写的TCP上位机服务,来实现图像的无线传输。
本文受启发于博客:ESP8266+STM32F407+OV7670实现图片传输,在此感谢该文作者。与该文不同的是,本文采用的摄像机模块是0V2640,传输的数据是压缩之后的jpeg格式的图像数据,而不是像上文博主那样,将RGB565数据直接传输到上位机。此外,本文存在和上文博主同样的问题,即采用串口传输方式,数据传输速率过低,实际应用中,发现3秒左右才能够传出一帧320*240的图片。本文接下来对整个图传功能中使用到的模块一一进行详细介绍。
在整个图传项目中,使用到的最核心的模块就是OV2640模块, 关于该模块的介绍以及详细的驱动,可以参考文章:STM32F4驱动OV2640摄像头,该文详细介绍了OV2640模块、DMCI接口及整个OV2640的驱动。此处进行简要介绍:
STM32F4驱动OV2640采用的是DCMI接口及DMA直接存储器访问,对于DMA的相关介绍可以参考文章:STM32:DMA。整个驱动流程是:STM32单片机通过DCMI接口,获取OV2640摄像头采集到的图像数据,并通过提前配置好的DMA数据流,将数据传输到LCD或内部数组。如下图所示,是通过DMA配置,将DCMI采集到的图像数据传输到LCD显示屏的效果:
关于LCD显示屏的介绍及详细驱动,可以参考博文: STM32: LCD显示。这样,首先通过OV2640及LCD屏幕,将摄像头模块采集到的数据显示在屏幕上。
该部分的源码: OV2640驱动,虽然我们将OV2640采集到的图像成功显示在了LCD屏幕之上,但是,我们最终的目的是将图像数据利用ESP8266模块通过WIFI传输到上位机或网页中,因此,我们还需要ESP8266模块。
ESP8266是比较常见的WIFI模块,该模块有三种不同的工作模式,即softAP 模式,station 模式,softAP + station 共存模式。(SoftAP:即无线接入点,是一个无线网络的中心节点,通常使用的无线路由器就是一个无线接入点;Station:即无线终端,是一个无线网络的终端端。)
本文我们将ESP8266作为客户端,并将其传输模式设置为透传,让其连接位于电脑的TCP上位机服务器,然后将OV2640采集的图像数据通过单片机传输给ESP8266模块,并采用WIFI发送给上位机。ESP8266的配置过程如下:
此处要注意,本文的ESP8266是通过AT指令进行配置的,因此要保证ESP8266已经烧录了AT固件库,一般网上买的ESP8266模块默认会烧录AT固件库,如若没有,可以自行进行烧录。
基本的模块介绍已经完成,通过模块介绍,我们也可以发现,无线图传模块的工作原理是:首先我们利用STM32和OV2640模块,采集图像数据,然后,我们配置好ESP8266,让其连接上位机服务器,然后,我们通过串口,将STM32采集到的图像数据传输给ESP8266,由于ESP8266配置的是透传模式,因此其会将STM32通过串口发送过来的数据原封不动的通过WIFI发送给上位机,上位机在将这些图像数据解析为图像显示出来就可以了,如下图所示:
上文中,我们介绍OV2640模块时,将图像显示在LCD屏幕上,但是,我们真正的目的,是将数据通过ESP8266模块传输给上位机。其实这两者本质上是一样的,一个是将数据通过DMA传输给LCD屏幕,而另外一个则是将数据传输给串口(因为ESP8266和STM32是通过串口连接的)。
核心传输代码如下:
//处理JPEG数据
//当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
void jpeg_data_process(void)
{
if(ov2640_mode)//只有在JPEG格式下,才需要做处理.
{
if(jpeg_data_ok==0) //jpeg数据还未采集完?
{
DMA_Cmd(DMA2_Stream1, DISABLE);//停止当前传输
while (DMA_GetCmdStatus(DMA2_Stream1) != DISABLE){}// 等待DMA2_Stream1可配置
jpeg_data_len=jpeg_buf_size-DMA_GetCurrDataCounter(DMA2_Stream1);// 得到此次数据传输的长度
jpeg_data_ok = 1; // 数据已经采集完成,等待被处理
}
if(jpeg_data_ok==2) //上一次的jpeg数据已经被处理了
{
DMA2_Stream1->NDTR=jpeg_buf_size;
DMA_SetCurrDataCounter(DMA2_Stream1,jpeg_buf_size);//传输长度为jpeg_buf_size*4字节
DMA_Cmd(DMA2_Stream1, ENABLE); //重新传输
jpeg_data_ok=0; //标记数据未采集
}
}
}
//jpeg模式
void jpeg_test(void)
{
u32 i,jpgstart,jpglen;
u8 headok=0;
u8 *p;
u8 effect=0,saturation=2,contrast=2;
u8 size=3;
u8 msgbuf[15]; //消息缓存区
//uart4初始化
LTE_uart3_init(115200);
LCD_Clear(WHITE);
POINT_COLOR=RED;
LCD_DisplayString(30,60,24,"ALIENTEK STM32F4");
LCD_DisplayString(30,90,24,"OV2640 JPEG Mode");
OV2640_JPEG_Mode(); // JPEG模式
My_DCMI_Init(); //DCMI配置
DCMI_DMA_Init((u32)&jpeg_buf,jpeg_buf_size,DMA_MemoryDataSize_Word,DMA_MemoryInc_Enable);//DCMI配置 输出到数组
OV2640_OutSize_Set(jpeg_img_size_tbl[size][0], jpeg_img_size_tbl[size][1]);
DCMI_Start(); //启动传输
delay_ms(500);
while(1)
{
if(jpeg_data_ok==1) // 已经采集完成一帧图像,开始处理数据
{
p=(u8*)jpeg_buf;
LCD_DisplayString(30,150,24,"Sending JPEG data...");
jpglen=0; //设置jpg文件大小为0
headok=0; //清除jpg头标记
for(i=0; i
上述核心代码其实是两个函数,函数jpeg_data_process()在帧中断中被调用,也就是说,每次采集完一帧图像,该函数都会被调用。在该函数中,主要是修改标志jpeg_data_ok 为1,以便另外一个函数jpeg_test()可以对这一帧数据进行处理,所谓的处理在此处其实就是将数据通过串口发送给ESP8266,ESP8266模块会将数据发送给上位机,并由上位机解析然后显示。
ESP8266在拿到STM32通过串口传输的图像数据之后,会将其发送给上位机,那么ESP8266和上位机之间是如何通讯的呢?是通过TCP/IP协议。
本文使用C#语言,基于TCP/IP协议,写了一个简单的上位机服务,该服务接受ESP8266的连接,并将其发送过来的数据编码为图像进行显示。由于软件写的很简单,因此很多功能并没有进行扩充实现,如其只支持一个设备的图传,后续可以进行升级改进。软件界面如下所示:
该软件的实现思路非常简单,因此其功能也比较粗狂,bug较多。
软件整体包含两条主要的线程以及一个数据缓冲容器(生产者消费者模式)。在服务器启动之后,软件开启监听服务,监听ESP8266的连接请求,并开启一个消费线程,用于从缓冲容器中获取图像数据(此时应该没有数据),在服务与ESP8266建立连接之后,就开始接收来自ESP8266的图像数据,此时,创建一个数据生产线程,将服务器接收到的数据存放到缓冲容器中,这样,这两个线程一个专门负责将接收到的数据放到缓冲容器中,一个专门负责从缓冲容器中获取数据并将其解码为图像在界面显示。
上位机核心代码:
///
/// 接收客户端发送的图像数据,并放到缓冲容器中。
///
///
public void ReceiveClientMessage(object socket)
{
int imageCount = 0;
//服务器端与客户端之间用于通讯的socket
Socket proxSocket = socket as Socket;
//开辟用来存储客户端发送来的数据的控件
byte[] dataClient = new byte[640 * 240];
proxSocket.ReceiveTimeout = 1000 * 300; // 设置接收数据时的阻塞时间为15秒钟
//接收客户端数据
while (serverStart)
{
StringBuilder imageData = new StringBuilder();
try
{
while (imageCount < 5)
{
int countRev = proxSocket.Receive(dataClient, 0, dataClient.Length, SocketFlags.None);
string revDate = StringUtils.ToHexStrFromByte(dataClient, countRev);
if (revDate.Length>0)
{
imageData.Append(revDate);
imageCount++;
AppendToMessage(revDate);
}
}
jpegQueue.Add(imageData.ToString());
imageCount = 0;
}
catch (SocketException e)
{
if (e.ErrorCode == 10060)
{
continue;
}
else
{
//客户端非正常退出。
AppendToMessage(string.Format("客户端:{0}非正常退出。", proxSocket.RemoteEndPoint.ToString()));
return; //线程终结
}
}
}
}
///
/// 从容器中获取图形数据,并解析为图像显示
///
private void createImg()
{
string imgData = "";
while (serverStart)
{
string imgListData = jpegQueue.Take();
imgData = imgData + imgListData;
while (imgData.IndexOf("FF D8")!=-1)
{
int startIndex = imgData.IndexOf("FF D8");
int endIndex = imgData.IndexOf("FF D9");
if (endIndex != -1)
{
string jpeg = imgData.Substring(startIndex,endIndex+5-startIndex);
imgData = imgData.Substring(endIndex+5);
Bitmap imageBitmap = StringUtils.GetJpegImage(jpeg.Replace(" ", ""));
if (imageBitmap != null)
{
Image img = Image.FromHbitmap(imageBitmap.GetHbitmap());
if (this.JpgImage.InvokeRequired)
{
JpgImage.Invoke(new Action(s =>
{
JpgImage.Image = s;
}), img);
}
else
{
this.JpgImage.Image = img;
}
}
else
{
continue;
}
}
else
{
break;
}
}
}
}
图像数据的解析也非常简单,根据JPEG图像的特点,其开头为FFD8,结尾为FFD9,我们通过查找这两个标志位,其中间的数据就是整帧的图像数据,我们将其这些数据转化位Bitmap位图进行显示就可以了。显示效果如下图:
上图中可以看出,OV2640采集到的图像数据,最终经过ESP8266发送到了上位机,并成功显示出来,只是受限于串口速率,大概3秒才会传输完一帧数据,在实际使用时,也可以发现,上位机数据缓冲容器时常都是空的,数据生产线程受限于串口速率,导致数据的生产远远小于数据的消耗,表现出来就是几秒才会刷新一帧数据。
本文主要基于STM32、OV2640以及ESP8266完成图像的网络传输,本文受启发并参考了博文:ESP8266+STM32F407+OV7670实现图片传输,在此对该文作者表示深深的感谢。
本文下位机图像数据采集以及上位机图像数据解析源码如下:
无线图传下位机源码:https://download.csdn.net/download/sssxlxwbwz/85251144
无线图传上位机源码: https://download.csdn.net/download/sssxlxwbwz/85251105