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

零、导读

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

  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的输出以及设置输出窗口的相应寄存器的配置。

这里我给出了三个输出窗口的配置,分别是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再到上位机和云端小程序等,(我就是伪全栈工程师)。大学在我看来还是要什么都去碰一碰,毕竟学习的道路是越来越专精的,在高考的时候已经从众多行业中选择了电子,到了后面的研究生阶段还将要继续细分,因此什么都玩一玩在之后的学习上还是能给我不少帮助的。

头像

SNinfinite

立志成为Double E的程序猿

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注