二分类数据集 XShell 程序设计 二叉树排序 控制跳转 bootstrap 自动化部署 post javafx mongoose boost threejs requirejs Way.js vue最新版本 vue样式 react脚手架 nginx学习视频 rxjava线程切换 nginx默认端口号 matlab对数函数 kb转mb excel带格式复制粘贴 Navicat pythonlist python取随机数 简单python脚本实例 java中的多态 java入门编程 java教材 java结构 java读取文件内容 java时间转换 java代码 linux目录 linux服务器登录 java游戏开发 typemonkey php递归 逗号的作用
当前位置: 首页 > 学习教程  > 编程语言

用20块的摄像头(不带fifo的OV7670)做WiFi实时传图小车

2020/8/31 14:36:55 文章标签:

博客地址 https://infinite-zh.com/archives/409

零、导读

 

在这篇文章中,你将看到如下内容:

 

  1. OV7670 QVGA配置的注意事项;
  2. 将FPGA做成一个FIFO驱动不带FIFO的OV7670;
  3. libjpeg阉割库的使用;
  4. ESP8266传图给上位机的简单实现。
  5. ESP8266传图给OneNET,微信小程序的方案

 

一、项目背景与吐槽

 

该项目是大三下的嵌入式短学期,因为一些的原因做的不太一样,正常做的是一个STM32F103的WiFi上位机的项目,但为了玩地快乐我做了一个FPGA控制的小车,并且还兼备了WIFI传图,上位机/微信小程序显示与控制地功能。

 

我在老师发布项目之后的三天内,使用STM32F1战舰开发板进行了测试(毕竟一下子玩的太大,怕失手),确定了初步方案,当时感觉是捡了一个大便宜,做了一个简单的任务,但当我看到FPGA的RTL图的时候,我自闭了。

 

还有许多小模块没全部展现出来,特别是在sdram ip中,害,自闭

 

要往上面这个FPGA中动手脚是比较困难的。特别是在我对整个sdram存储的运用还不太熟练的情况下。

 

吐槽:本来没打算做微信小程序的,结果在最后的实验验收要求上看到了安卓手机控制。阿这,这是我没想到的,只得硬着头皮做完。。。

 

二、项目设计方案

 

回归正题,这次的任务的要求如下:

 

基于FPGA小车、STM32最小系统、摄像头、无线模块等常用电子模块,实现一个远程可视的遥控小车。

 

对此我的设计方案如下:

 

系统框图

 

系统设计方案

 

由系统框图所示,整个系统设计方案分成三个部分,分别是FPGA部分,STM32部分以及上位机/云端部分。

 

1、FPGA部分

 

使用黑金的AX301来进行设计,FPGA在系统中的作用是获取图像、存储图像、传输图像以及控制小车。

 

  1. 获取图像
    • 使用OV7670摄像头来进行图像获取,其中对摄像头的配置,采用QVGA、RGB565的格式输出,输出窗口设置为256*128的大小
  2. 图像存储
    • 使用SRAM来进行图像的存储,同时采用乒乓操作,一边获取图像一边传输图像;
  3. 图像的传输
    • 为了加快图像的传输速率,采用流水线,将RGB565的图像转换成灰度图,然后通过SPI作为从机进行图像的传输;
  4. 小车控制
    • 根据获得的控制指令,输出1KHZ的PWM波驱动电桥控制电机从而控制小车,其中PWM的占空比可调,在进行转弯时为了增加驱动能力,适当提高PWM的占空比。

 

2、STM32部分

 

使用自制的STM32F407最小系统板,STM32在系统中的作用是与FPGA进行交互,对图像数据进行处理,与上位机/云端进行交互,是连接云端与底层的通道。

 

  1. 与FPGA进行交互
    • STM32使用硬件SPI作为主机,使用21M的速率与FPGA进行双向传输,即获取FPGA中存储的图像数据的同时,传达小车的方向控制指令。
  2. 对图像数据进行处理
    • 根据计算,256*128的图像即使是作为灰度图仍然后32KB的大小,相对于ESP8266的115200的波特率来说图像还是太大,因此需要对图像进行jpeg压缩,在jpeg的压缩方面,借助了libjpeg的库。实测灰度图压缩为jpeg后图像大小压缩至1KB左右。大大加快了传输的速率。
  3. 与上位机/云端进行交互
    • STM32驱动ESP8266一方面可以与PC的上位机进行交互,传输图片到上位机上并获取来自上位机的小车控制指令,另一方面可以与OneNET进行交互,通过http协议post图像数据到OneNET上,并能通过get获取OneNET上的小车控制指令。

 

3、上位机/云端部分

 

  • 这一部分是最后的显示与控制终端,其中上位机部分采用C#进行编写,云端部分借助OneNET这个物联网开发平台存储数据,并通过微信小程序获得图像数据以及传达控制指令。

 

三、项目具体实现与难点攻克

 

1、有关FPGA的方案设计

 

借助黑金的OV7670_sdram的例程,进行修改。

 

1、OV7670 QVGA的配置

 

根据OV7670的datasheet,就会发现OV7670有上百个寄存器(阿这),一一地去配置显然过于复杂,不过我使用的程序是黑金官方的一个例程,黑金官方已经给了OV7670的寄存器的配置,而我们要做的是修改他的寄存器配置,因为一般来说FPGA驱动OV7670都是为了VGA的传输,所以基本都是配置了VGA的模式,但VGA模式对于这个项目来说是在是太大了,我们需要将他修改到QVGA模式,并且设置输出的窗口,这里借鉴了STM32驱动OV7670时的配置(详细可以参考这个http://www.51hei.com/bbs/dpj-92272-1.html),以下的代码给出了QVGA的输出以及设置输出窗口的相应寄存器的配置。

 

 

此图像的alt属性为空;文件名为image-3-1024x299.png

这里我给出了三个输出窗口的配置,分别是128*64、320*240和256*128

 

但需要注意的时,将OV7670配置成QVGA时,根据datasheet给出的波形图。

 

 

我们可以看到他的输出速率是VGA模式的一半,表现在应用中,我发线他的PCLK输出变为原来的一半,即原来我是25M的PCLK,但我设置位QVGA时,他的输出变成了12.5M。

 

这里必须安利一波singalTap,我本来没有注意这一点,然后他的输出数据一支不对,然后我就将OV7670的全部引脚全部放到singalTap上,然后就发现了他的PCLK变为原来的一半,这时候我才想到datasheet这个时序图的含义......他的这个时钟是会影响到后续的fifo等一系列操作。

 

2、修改保存的图像的大小

 

要改变图像大小,只需修改sdram_vga_top.v的最大地址大小

 

NOTE:这里的地址大小最好是与wr_length和rd_length的整数倍,否则在显示图像的时候会出现图像会进行移动。

 

注意:我在上面记录了OV7670的配置时给了320*240的图像大小,但我最后并没有使用,一方面是因为图像大了传输过慢,另一方面是因为建议图像的大小是他的wr_length以及rd_length的整数倍,否则显示图像的时候可能会出现图像的移动。

 

3、FPGA与STM32的SPI通信

 

为了实现高速率的传输,我设置了SPI通信为21M,需要注意的是一下几点:

 

  1. 为了保证SPI和STM32的通信,一定要让STM32与FPGA共地!!!
  2. SPI模块的时钟
    • 在FPGA这里经过测试发现SPI模块的时钟建议倍频到200M,一开始我设置了100M的时钟,发现SPI通信会发生错位,有时候还会紊乱,提高时钟的频率可以解决这个问题。
  3. SPI模块的CS
    • 一般SPI模块都会设计一个CS,然后又由于STM32的硬件SPI只有3个输出脚,我自己定义了一个CS,但实际的使用效果不佳,我查看SingalTap的时候发现,好几次SPI传输的时候这个CS脚会突然来一个高电平,导致我的数据出现问题,预计原因是STM32与FPGA之间的地还是不稳,因此我最后没有用32控制CS,而是引出来进行手动复位......

 

4、将FPGA做成一个FIFO

 

因为原来的程序中,摄像头的数据是存储在sdram中,使用了乒乓操作,一边读取一边存储,并且设计是考虑到VGA的时序,基本读取与存储是可以很好的同步的进行,但在我这个项目中,我对图像数据的处理明显时远远慢于OV7670图像数据的输出,因此我需要将FPGA做成一个保存数据的FIFO(吐槽一下:此处点题,20块的摄像头不带fifo就是这么真实,但凡他有一个fifo我可以简便不少.....)

 

为了达到fifo的效果,我从sdban_switch入手修改了他的交换bank的条件

 

case(state_write)
3'd0:begin 
	if(frame_write_done  && frame_read_done) //to be sure data with the same image has been wrote
		state_write <= 3'd1;
	else 
		state_write <= 3'd0;
	end
3'd1:begin wr_load <= 1'b0; state_write <= 3'd2; end
3'd2:begin wr_load <= 1'b1; state_write <= 3'd3; end
3'd3:begin wr_load <= 1'b0; state_write <= 3'd4; end
3'd4:begin
	if(bank_switch_flag)
		state_write <= 3'd5;
	else
		state_write <= 3'd4;
	end
3'd5:begin
	if(frame_write_done  && frame_read_done) //to be sure data with the same image has been wrote
		begin
		wr_bank <= ~wr_bank;
		state_write <= 3'd0;
		end
	else
		begin
		wr_bank <= wr_bank;
		state_write <= 3'd4;
		end
	end
default:;

 

如上所示,在读取完并且写入完毕后才交换bank,以此来配合较慢的读取,但是相应的最终会导致显示的图像有延迟,延迟取决于传输时间、压缩时间等一系列因素。

 

不过,其中的神来之笔是我将读取的时钟更换了,不再由pll锁相环提供,而是由SPI模块的读取完毕的信号来作为时钟,通过rtl图可以清楚的看到这一点

 

 

这条时钟一方面是作为sdram读取fifo的时钟,另一方面是作为RGB565转换灰度图的时钟,因为这个转换是完全流水线结构,所以他的只需要经过3个时钟周期的时滞就可以8位流量的输出灰度值。

 

5、小车的驱动

 

小车的驱动主要是依靠PWM来进行驱动,但需要注意的是,转弯时的摩擦力比较大,需要更强的驱动能力,因此在收到转弯指令的时候可以适当提高PWM的占空比,提高驱动能力。

 

2、有关STM32的设计

 

1、libjpeg库的使用

 

这个库是我在CSDN找的,这是链接https://blog.csdn.net/forwardjia/article/details/82179191

 

但他是RGB的压缩,我为了更进一步的压缩,把他修改成了灰度图的jpeg压缩,具体可以看我的代码,或者直接使用。

 

这位大佬移植了libjpeg,不过貌似是一个比较旧的版本......压缩质量使用的还是浮点数,不过不影响使用。

 

值得一提的是,如果去查看STM32Cube_FW_F4可以在其的Projects->STM32F429-Discovery->Application下可以看到LibJPEG,这个是官方的移植,不过他把整个LibJPEG给移植过来了,整个工程太大了,所以还是选择使用这个阉割版。。。。。。

 

2、STM32与上位机通信

 

在这里我设置了ESP8266作Client,上位机作为Server,ESP8266通过配置连接WIFI,然后访问我电脑的IP,就可以与上位机建立socket进行图片的传输。

 

STM32与上位机机通信最好是要有自定的协议,要有帧头,校验位等,但这个数据量实在有点大,我就只设置了一个传输数据包大小的帧头,然后就粗暴的上传了,不过效果还蛮好的说

 

效果如下:

 

全损画质

 

3、STM32与OneNET进行交互

 

STM32对OneNET进行交互,OneNET官方给了许多的协议,例如MQTT,EDP和HTTP等,不过对于我来说,最快的还是直接使用API,用get获取数据点,用post上传图片。

 

为了顺利的与OneNET进行交互,建议先使用网络调试助手进行调试,调试方面可以参考这个链接:https://blog.csdn.net/qq_42401265/article/details/101053604

 

对于图像传输,这是我上传的数据

 

POST http://api.heclouds.com/bindata?device_id=607169499&datastream_id=image HTTP/1.1
api-key:your api-key
Host:api.heclouds.com
Content-Length:3493

 

后面加上图像数据,图像数据可以是用上面链接给的一个小软件。

 

在传输的时候需要注意的是,在传输末尾要加上两个换行符,这个很关键,没有这两个换行符数据上传就会有很多的问题。另外OneNET的Api-key建议直接使用Master-api-key

 

在STM32的代码上,如下所示

 

u8 *p;
int t =0;
p=mymalloc(SRAMIN,32);							//申请32字节内存
sprintf((char*)p,"Content-Length:%d\r\n\r\n",pt_buf);
u3_printf("POST http://api.heclouds.com/bindata?device_id=607169499&datastream_id=image&desc=testfile HTTP/1.1\r\n");
u3_printf("api-key: your api key\r\n");
u3_printf("Host:api.heclouds.com\r\n");
u3_printf(p);
for(t = 0;t<pt_buf;t++)
{
	UART3_Send_Data(JPG_enc_buf[t]);		
}
myfree(SRAMIN,p);

 

同理,获取OneNET的数据点

 

USART3_RX_STA=0;
u3_printf("GET http://api.heclouds.com/devices/607169499/datastreams/direction HTTP/1.1\r\n");
u3_printf("api-key:your api key\r\n");
u3_printf("Host:api.heclouds.com\r\n\r\n");
delay_ms(100);
	USART3_RX_STA=0;
char *c1=strstr(USART3_RX_BUF, "current_value");

return *(c1+16);

 

这里对于收到的数据可以使用cJSON进行解析,但需要注意的是,收到的数据还有一个HTTP的头,需要将他除去,如下所示

 

HTTP/1.1 200 OK
Date: Wed, 01 Jul 2020 15:23:14 GMT
Content-Type: application/json
Content-Length: 133
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

 

因此为了方便使用,我直接使用了strstr函数,通过查找current_value来获取数据点的数据。

 

4、上位机的编写

 

上位机的编写我是用的是C#来进行编写,下列展示一下主要的代码

 

void TSReceive()
{
    aimSocket = mySocket.Accept();//服务端监听到的Socket为服务端发送数据的目标socket
    infoLabel.Text = "连接成功";
    byte[] buffer = new byte[4];
    while (true)
    {
        try
        {
            aimSocket.Receive(buffer, buffer.Length, SocketFlags.None);
            int contentLen = BitConverter.ToInt32(buffer, 0);
            int size = 0;
            MemoryStream ms = new MemoryStream();
            while (size < contentLen)
            {
                //分多次接收,每次接收256个字节,
                byte[] bits = new byte[256];
                int r = aimSocket.Receive(bits, bits.Length, SocketFlags.None);//接收到监听的Socket的数据
                if (r == 0)
                {
                    MessageBox.Show("连接断开");
                    break;
                }
                ms.Write(bits, 0, r);
                size += r;
            }
            Image img = Image.FromStream(ms);
            picBox.Image = null;
            picBox.Image = img;

        }
        catch
        { }
    }
}

 

上述代码是TCP Server的线程,在该线程中,通过监听socket,获取数据然后将他显示到Image控件上。

 

整体上位机布局

 

在控制指令方面,我设置了一个帧头,byte[] CtrBuf = new byte[2] { 0xA3, 0x20 };

 

虽然实际上并没有起到什么作用.......

 

5、微信小程序的编写

 

微信小程序方面,下图展示的获取图像数据并展示的代码。

 

 

图像的转换是主要靠OneNET获取文件的api,获取图像的二进制流文件,然后将其转换成base64的类型,就可以显示图像了,这里主要要注意两点:

 

  1. 在request的函数中加一个responseType: 'arraybuffer', 这样接收到的数据就是arraybuffer的形式,就可通过arrayBufferToBase64转换成base64的形式,然后加上 data:image/jpg;base64, 就可以直接显示图像了。
  2. 使用OneNET获取文件的api的时候要使用master-api-key,否则可能会报错,我当时报的错是 errno:3 auth failed, 为此我还问了OneNET客服呢。。。。。

 

四、成果展示

 

1、上位机接受图像

 

 

2、手机端微信小程序

 

界面做的简陋了一点。。。。。

 

五、缺陷与提高部分

 

本次实验最大的缺陷是延迟过高,一方面是因为受限于Jpeg压缩的效率太低,另一方面则是因为数据量太大,传输的速率太慢,在这里我有一些改进与提高的想法,但因为时间的限制就没有付诸于实践。

 

1、SPI传输在STM32那边可以使用DMA进行传输,将数据直接存储到定义的存储空间中,但这会引起另一个问题,就是DMA对于SPI双向传输该如何进行处理。毕竟不仅要接收数据,还需要传送方向指令。

 

2、DMA传输还可以用在串口wifi上传图像上,但相对于第一条,这一条的实现难度可能更大一些。

 

3、氪金,没错,花钱换一个摄像头,换一个能直接jpeg形式输出的摄像头,例如ov2640,这样就可以省去jpeg压缩的时间,大大提高效率,(但凡有个好的摄像头,这个东西也不至于这么复杂)

 

4、增加协议。包括STM32与FPGA通过SPI传输的协议,以及STM32与上位机的传输协议。如果是用http传输,STM32与OneNET的协议倒不是非常必要。STM32与FPGA通过SPI的协议可以保证数据传输的稳定与可靠,如果可以最好与CS脚联合设计。STM32与上位机的协议也可以保证出错的概率降低。(目前虽然差错出现的概率不大,但还是会有。。。。。)

 

六、评价

 

总的来说,这一个项目虽然不大,但他的涉及面广,从FPGA到STM32再到上位机和云端小程序等,(我就是伪全栈工程师)。大学在我看来还是要什么都去碰一碰,毕竟学习的道路是越来越专精的,在高考的时候已经从众多行业中选择了电子,到了后面的研究生阶段还将要继续细分,因此什么都玩一玩在之后的学习上还是能给我不少帮助的。

 


本文链接: http://www.dtmao.cc/news_show_150260.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?