【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0

1)实验平台:正点原子领航者ZYNQ开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-301505-1-1.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:994244016
5)关注正点原子公众号,获取最新资料更新
在这里插入图片描述

第三十五章 基于OV7725的PL以太网视频传输实验

OV7725是OmniVision(豪威科技)公司生产的一颗CMOS图像传感器,该传感器功耗低、可靠性高以及采集速率快,主要应用在玩具、安防监控、电脑多媒体等领域。本章我们将使用ZYNQ开发板实现对OV7725的数字图像采集,并通过开发板上的PL以太网接口发送给上位机实时显示。
本章分为以下几个章节:
35.1 简介
35.2 实验任务
35.3 硬件设计
35.4 程序设计
35.5 下载验证
35.1 简介
OV7725简介
OV7725是一款1/4英寸单芯片图像传感器,其感光阵列达到640*480,能实现最快60fps分辨率的图像采集。传感器内部集成了图像处理的功能,包括自动曝光控制(AEC)、自动增益控制(AGC)和自动白平衡(AWB)等。同时传感器具有较高的感光灵敏度,适合低照度的应用,下图为OV7725的功能框图。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第1张图片

图 7.5.13.1 OV7725功能框图
由上图可知,感光阵列(image array)在XCLK时钟的驱动下进行图像采样,输出640*480阵列的模拟数据;接着模拟信号处理器在时序发生器(video timing generator)的控制下对模拟数据进行算法处理(analog processing);模拟数据处理完成后分成G(绿色)和R/B(红色/蓝色)两路通道经过AD转换器后转换成数字信号,并且通过DSP进行相关图像处理,最终输出所配置格式的10位视频数据流。模拟信号处理以及DSP等都可以通过寄存器(registers)来配置,配置寄存器的接口就是SCCB接口,该接口协议兼容IIC协议。
SCCB(Serial Camera Control Bus,串行摄像头控制总线)是由OV(OmniVision的简称)公司定义和发展的三线式串行总线,该总线控制着摄像头大部分的功能,包括图像数据格式、分辨率以及图像处理参数等。OV公司为了减少传感器引脚的封装,现在SCCB总线大多采用两线式接口总线。
OV7725使用的是两线式接口总线,该接口总线包括SIO_C串行时钟输入线和SIO_D串行双向数据线,分别相当于IIC协议的SCL信号线和SDA信号线。我们在前面提到过SCCB协议兼容IIC协议,是因为SCCB协议和IIC协议非常相似,有关IIC协议的详细介绍请大家参考“EEPROM读写实验”章节。
SCCB的写传输协议如下图所示:
在这里插入图片描述

图 7.5.13.2 SCCB写传输协议
上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写 1:读),OV7725的器件地址为7’h21,所以在写传输协议中,ID Address(W) = 8’h42(器件地址左移1位,低位补0);Sub-address为8位寄存器地址,在OV7725的数据手册中定义了0x00~0xAC共173个寄存器,有些寄存器是可改写的,有些是只读的,只有可改写的寄存器才能正确写入;Write Data为8位写数据,每一个寄存器地址对应8位的配置数据。上图中的第9位X表示Don’t Care(不必关心位),该位是由从机(此处指OV7725)发出应答信号来响应主机表示当前ID Address、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机(此处指FPGA)可不用判断此处是否有应答,直接默认当前传输完成即可。
我们可以发现,SCCB和IIC写传输协议是极为相似的,只是在SCCB写传输协议中,第9位为不必关心位,而IIC写传输协议为应答位。SCCB的读传输协议和IIC有些差异,在IIC读传输协议中,写完寄存器地址后会有restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,发起总线停止信号,下图为SCCB的读传输协议。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第2张图片

图 7.5.13.3 SCCB读传输协议
由上图可知,SCCB读传输协议分为两个部分。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针指向虚写操作中寄存器地址的位置,当然虚写操作也可以通过前面介绍的写传输协议来完成。第二部分是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据,注意ID Address(R) = 8’h43(器件地址左移1位,低位补1)。上图中的NA位由主机(这里指FPGA)产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
在OV7725正常工作前,必须先对传感器进行初始化,即通过配置寄存器使其工作在预期的工作模式,以及得到较好画质的图像。因为SCCB的写传输协议和IIC几乎相同,因此我们可以直接使用IIC的驱动程序来配置摄像头。当然这么多寄存器也并非都需要配置,很多寄存器可以采用默认的值。OV公司提供了OV7725的软件使用手册(OV7725 Software Application Note,位于开发板所随附的资料“7_硬件资料/6_OV7725资料/OV7725 Software Application Note.pdf”),如果某些寄存器不知道如何配置可以参考此手册,下表是本程序用到的关键寄存器的配置说明。
表 35.1.1 OV7725关键寄存器配置说明
地址
(HEX) 寄存器 默认值
(HEX) 详细说明
0x0C COM3 0x10 Bit[7]:保留
Bit[6]:水平镜像开关
Bit[5]:交换RGB输出模式B/R位置
Bit[4]:交换YUV输出模式Y/UV位置
Bit[3]:交换MSB/LSB位置
Bit[2]:电源休眠期间输出时钟三态选择
0:三态
1:非三态
Bit[1]:电源休眠期间输出数据三态选择
0:三态
1:非三态
Bit[0]:彩条测试使能
0x0D COM4 0x41 Bit[7:6]:PLL频率控制
00:旁路PLL(直通)
01: PLL 4x
02: PLL 6x
03: PLL 8x
0x11 CLKRC 0x00 Bit[6]:选择是否直接使用外部时钟
Bit[5:0]:内部PLL配置
F(internal clk) = F(input clk) * PLL multiplier/[(CLKRC[5:0]+1)* 2]
0x12 COM7 0x00 Bit[7]:SCCB寄存器复位
0:保持不变
1:复位所有的寄存器
Bit[6]:0:VGA分辨率输出
1:QVGA分辨率输出
Bit[5]:BT.656协议开关
Bit[4]:Sensor RAW
Bit[3:2]:RGB输出格式控制
00:GRB4:2:2
01:RGB565
10:RGB555
11: RGB444
Bit[1:0]:输出格式控制
00:YUV
01:Processed Bayer RAW
10:RGB
11:Bayer RAW
0x13 COM8 0xCF Bit[2]:选择是否开启自动增益AGC功能
Bit[1]:选择是否开启自动白平衡AWB功能
Bit[0]:选择是否开启自动曝光AEC功能
0x15 COM10 0x00 Bit[7]:反转输出图像数据
Bit[6]:切换HREF到HSYNC信号
Bit[5]:PCLK输出选项
0:PCLK时钟有效
1:在行无有效信号时PCLK无效
Bit[4]:翻转PCLK
Bit[3]:翻转HREF
Bit[1]:翻转VSYNC
bit[0]:数据输出范围选择
0:10bit图像数据输出
1:高8bit图像数据输出
0x9B BRIGHT 0x00 亮度值补偿,可以通过此值提高像素的亮度
OV7725的寄存器较多,对于其它寄存器的描述可以参OV7725的数据手册。
下图为OV7725的一些特性。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第3张图片

图 7.5.13.4 OV7725的特性
从上图可以看出,OV7725的输入时钟频率的范围是10Mhz~48Mhz;SCCB总线的SIO_C的时钟频率最大为400KHz;配置寄存器软件复位(寄存器地址0x12 Bit[7]位)和硬件复位(cam_rst_n引脚)后需要等待最大1ms才能配置其它寄存器;每次配置完寄存器后,需要最大300ms时间的延迟,也就是10帧图像输出的时间才能输出稳定的视频流。
OV7725支持多种不同分辨率图像的输出,包括VGA(640480)、QVGA(320240)以及CIF(一种常用的标准化图像格式,分辨率为352288)到4030等任意尺寸。可通过寄存器地址0x12(COM7)、0x17(HSTART)、0x18(HSIZE)、0x19(VSTRT)、0x1A(VSIZE)、0x32(HREF)、0x29(HoutSize)、0x2C(VOutSize)、0x2A(EXHCH)来配置输出图像的分辨率。
OV7725支持多种不同的数据像素格式,包括YUV(亮度参量和色度参量分开表示的像素格式)、RGB(其中RGB格式包含RGB565、RGB555等)以及8位的RAW(原始图像数据)和10位的RAW,通过寄存器地址0x12(COM7)配置不同的数据像素格式。
一般通过原始像素数据(如RGB565或者RGB888格式)来作为显示的数据会更加方便,因此我们将OV7725摄像头输出的图像像素数据配置成RGB565格式。本次实验采用OV7725支持的最大分辨率640*480,下图为摄像头输出的VGA帧模式时序图。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第4张图片

图 7.5.13.5 VGA帧模式输出时序图
在介绍时序图之前先了解几个基本的概念。
VSYNC:场同步信号,由摄像头输出,用于标志一帧数据的开始与结束。上图中VSYNC的高电平作为一帧的同步信号,在低电平时输出的数据有效。需要注意的是场同步信号是可以通过设置寄存器0x15 Bit[1]位进行取反的,即低电平同步高电平有效,本次实验使用的是和上图一致的默认设置;
HREF/HSYNC:行同步信号,由摄像头输出,用于标志一行数据的开始与结束。上图中的HREF和HSYNC是由同一引脚输出的,只是数据的同步方式不一样。本次实验使用的是HREF格式输出,当HREF为高电平时,图像输出有效,可以通过寄存器0x15 Bit[6]进行配置。本次实验使用的是HREF格式输出;
D[9:0]:数据信号,由摄像头输出,在RGB格式输出中,只有高8位D[9:2]是有效的;
tPCLK:一个像素时钟周期;
tp:单个数据周期,这里需要注意的是上图中左下角红框标注的部分,在RGB模式中,tp代表两个tPCLK(像素时钟)。以RGB565数据格式为例,RGB565采用16bit数据表示一个像素点,而OV7725在一个像素周期(tPCLK)内只能传输8bit数据,因此需要两个时钟周期才能输出一个RGB565数据;
tLine:摄像头输出一行数据的时间,共784个tp,包含640tp个高电平和144tp个低电平,其中640tp为有效像素数据输出的时间。以RGB565数据格式为例,640tp实际上是6402=1280个tPCLK;
由图 7.5.13.5可知,VSYNC的上升沿作为一帧的开始,高电平同步脉冲的时间为4
tLine,紧接着等待20tLine时间后,HREF开始拉高,此时输出有效数据;HREF由640tp个高电平和144tp个低电平构成;输出480行数据之后等待6tLine时间一帧数据传输结束。所以输出一帧图像的时间实际上是tFrame =(4 + 20 + 480 + 6)tLine = 510tLine。
由此我们可以计算出摄像头的输出帧率,以PCLK=25Mhz(周期为40ns)为例,计算出OV7725输出一帧图像所需的时间如下:
一帧图像输出时间:tFrame = 510
tLine = 510784tp = 5107842tPCLK = 79968040ns = 31.9872ms;
摄像头输出帧率:1000ms/31.9872ms ≈ 31Hz。
如果把像素时钟频率提高到摄像头的最大时钟频率48Mhz,通过上述计算方法,摄像头的输出帧率约为60Hz。
下图为OV7725输出RGB565格式的时序图:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第5张图片

图 7.5.13.6 RGB565模式时序图
上图中的PCLK为OV7725输出的像素时钟,HREF为行同步信号,D[9:2]为8位像素数据。OV7725最大可以输出10位数据,在RGB565输出模式中,只有高8位是有效的。像素数据在HREF为高电平时有效,第一次输出的数据为RGB565数据的高8位,第二次输出的数据为RGB565数据的低8位,first byte和second byte组成一个16位RGB565数据。由上图可知,数据是在像素时钟的下降沿改变的,为了在数据最稳定的时刻采集图像数据,所以我们需要在像素时钟的上升沿采集数据。
图像传输简介
随着图像技术、监控技术的发展,通信的数据量越来越大,这无疑对数据传输系统的实时性、稳定性和高效性都提出了苛刻的要求。对于大量数据的高速传输,一般使用以太网或者USB传输方案,而以太网相比于USB,有着传输距离更远的优势。为了能够满足视频在高帧率、高分辨率下实时传输,传统的百兆以太网已不能满足需求,此时需要通过千兆以太网进行传输。本章将使用开发板上的千兆以太网接口传输视频,并通过上位机实时显示。
以太网实时传输图像采用的传输层协议有TCP和UDP两种。TCP协议能为两个端点间的数据传输提供相对可靠的保障,这种保障是通过一个握手机制实现的。当数据发送给接收者时,接收者要检查数据的正确性,当接收者接收到正确数据后给发送者一个确认报文信号,发送者只有接收到接收者的确认报文信号后才能发送下一个数据块。如果没有接收到确认报文,这个数据块就必须要重新发送。尽管这种机制对传输数据来说是非常合理的,但当用它在以太网视频实时传输时就会引发很多问题。首先就是延迟问题,在传输信道丢包率较高时,TCP的传输质量下滑严重,重传拥塞导致视频延时非常大,失去实时互通的意义。而UDP相比于TCP能提供更高的吞吐量和较低的延迟,非常适合低延时的视频传输场合。
UDP性能的提高是以不能保障数据完整性为代价的,它不能对所传数据提供担保,有时会出现数据丢包的现象。为了降低丢包对视频显示带来的影响,我们为每帧图像添加一个帧头,用于标志一帧图像的开始。上位机解析到图像帧头之后,接下来将接收到的像素数据重新放到图像显示区域的起始位置,保证了在视频传输过程中,即使出现丢包的现象,视频也能恢复到正常显示的画面。
35.2 实验任务
本节实验任务是使用领航者ZYNQ开发板及OV7725摄像头实现图像采集,并通过开发板上的PL以太网接口发送给上位机实时显示。
35.3 硬件设计
领航者Zynq开发板上有一个摄像头扩展接口,该接口可以用来连接OV7725/OV5640等摄像头模块。由于SCCB接口通信需要接上拉电阻,因此,将CMOS_SCL信号和CMOS_SDA信号连接上拉电阻,原理图如图 7.5.13.1所示:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第6张图片

图 7.5.13.1 摄像头扩展接口原理图
ATK-OV7725是正点原子推出的一款高性能30W像素高清摄像头模块。该模块通过2*9排针(2.54mm间距)同外部连接,我们将摄像头的排针直接插在开发板上的摄像头接口即可,模块外观如图 7.5.13.2所示:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第7张图片

图 7.5.13.2 ATK-OV7725摄像头模块实物图
我们在前面说过,OV7725在RGB565模式中只有高8位数据是有效的即D[9:2],而我们的摄像头排针上数据引脚的个数是8位。实际上,摄像头排针上的8位数据连接的就是OV7725传感器的D[9:2],所以我们直接使用摄像头排针上的8位数据引脚即可。
需要注意的是,由图 7.5.13.1可知,摄像头扩展口的第18个引脚定义为CMOS_PWDN,而我们的OV7725摄像头模块的PWDN引脚固定为低电平,也就是一直处于正常工作模式。OV7725摄像头模块的第18个引脚定义为SGM_CTRL,这个引脚是摄像头驱动时钟的选择引脚。OV7725摄像头模块内部自带晶振的,当SGM_CTRL引脚为低电平时,选择使用摄像头的外部时钟,也就是FPGA需要输出时钟给摄像头;当SGM_CTRL引脚为高电平时,选择使用摄像头的晶振提供时钟。本次实验将SGM_CTRL引脚驱动为高电平,这样就不用为摄像头提供驱动时钟,即不用在CMOS_XCLK引脚上输出时钟。
由于PL以太网引脚数目较多,且在前面相应的章节中已经给出它们的管脚列表,这里只列出摄像头相关管脚分配, 如下表所示:
表 35.3.1 OV7725摄像头管脚分配
信号名 方向 管脚 端口说明 IO电平
cam_pclk input W14 cmos 数据像素时钟 LVCMOS33
cam_vsync input U12 cmos 场同步信号 LVCMOS33
cam_href input T12 cmos 行同步信号 LVCMOS33
cam_rst_n output P14 cmos 复位信号 LVCMOS33
cam_sgm_ctrl output V15 cmos 时钟选择信号 LVCMOS33
cam_data[0] input R14 cmos 数据 LVCMOS33
cam_data[1] input U13 cmos 数据 LVCMOS33
cam_data[2] input V13 cmos 数据 LVCMOS33
cam_data[3] input U15 cmos 数据 LVCMOS33
cam_data[4] input U14 cmos 数据 LVCMOS33
cam_data[5] input W13 cmos 数据 LVCMOS33
cam_data[6] input V12 cmos 数据 LVCMOS33
cam_data[7] input Y14 cmos 数据 LVCMOS33
emio_sccb_tri_io[0] output T10 cmos SCCB_SCL线 LVCMOS33
emio_sccb_tri_io[1] inout T11 cmos SCCB_SDA线 LVCMOS33
相关的管脚约束如下所示:
#系统时钟和复位

create_clock -period 20.000 -name sys_clk [get_ports sys_clk]
create_clock -period 8.000 -name eth_rxc [get_ports eth_rxc]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]

set_property -dict {PACKAGE_PIN G15 IOSTANDARD LVCMOS33} [get_ports eth_rst_n]
set_property -dict {PACKAGE_PIN K17 IOSTANDARD LVCMOS33} [get_ports eth_rxc]
set_property -dict {PACKAGE_PIN E17 IOSTANDARD LVCMOS33} [get_ports eth_rx_ctl]
set_property -dict {PACKAGE_PIN B19 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[0]}]
set_property -dict {PACKAGE_PIN A20 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[1]}]
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[2]}]
set_property -dict {PACKAGE_PIN H16 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[3]}]

set_property -dict {PACKAGE_PIN B20 IOSTANDARD LVCMOS33} [get_ports eth_txc]
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports eth_tx_ctl]
set_property -dict {PACKAGE_PIN D18 IOSTANDARD LVCMOS33} [get_ports {eth_txd[0]}]
set_property -dict {PACKAGE_PIN C20 IOSTANDARD LVCMOS33} [get_ports {eth_txd[1]}]
set_property -dict {PACKAGE_PIN D19 IOSTANDARD LVCMOS33} [get_ports {eth_txd[2]}]
set_property -dict {PACKAGE_PIN D20 IOSTANDARD LVCMOS33} [get_ports {eth_txd[3]}]

#CAMERA
create_clock -period 40.000 -name cmos_pclk [get_ports cam_pclk]
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets cam_pclk_IBUF]
set_property -dict {PACKAGE_PIN W14 IOSTANDARD LVCMOS33} [get_ports cam_pclk]
set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports cam_rst_n]
set_property -dict {PACKAGE_PIN V15 IOSTANDARD LVCMOS33} [get_ports cam_sgm_ctrl]
set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[0]}]
set_property -dict {PACKAGE_PIN U13 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[1]}]
set_property -dict {PACKAGE_PIN V13 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[2]}]
set_property -dict {PACKAGE_PIN U15 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[3]}]
set_property -dict {PACKAGE_PIN U14 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[4]}]
set_property -dict {PACKAGE_PIN W13 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[5]}]
set_property -dict {PACKAGE_PIN V12 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[6]}]
set_property -dict {PACKAGE_PIN Y14 IOSTANDARD LVCMOS33 IOB TRUE} [get_ports {cam_data[7]}]
set_property -dict {PACKAGE_PIN U12 IOSTANDARD LVCMOS33} [get_ports cam_vsync]
set_property -dict {PACKAGE_PIN T12 IOSTANDARD LVCMOS33} [get_ports cam_href]
set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports cam_scl]
set_property -dict {PACKAGE_PIN T11 IOSTANDARD LVCMOS33} [get_ports cam_sda]

35.4 程序设计
OV7725在VGA(分辨率为640480)帧模式下,以RGB565格式输出最高帧率可达60Hz,每秒钟输出的数据量达到6064048016bit = 294912000bit = 281.25Mbit。我们FPGA开发板上的PHY芯片类型为千兆以太网,理论上最大传输速率为1000Mbit/s,加上帧头、CRC校验以及帧间隙带来的额外开销,实际上能达到的最大传输速率比理论上最大传输速率低。尽管实际传输速率低于1000Mbit/s,但实时传输OV7725摄像头的图像完全没有压力,可以满足带宽要求,因此本次实验不需要通过片外存储器缓存图像,仅将图像数据先经过FIFO进行缓存,然后通过以太网接口进行发送即可。
根据实验任务,我们可以大致规划出系统的控制流程:时钟模块用于为IIC驱动模块、以太网顶层模块和开始传输控制模块提供驱动时钟。I2C驱动模块和I2C配置模块用于初始化OV7725图像传感器;摄像头采集模块负责采集摄像头图像数据,并且把图像数据连接至图像数据封装模块,图像数据封装模块将输入的图像数据进行位拼接,并添加图像的帧头和行场分辨率;以太网顶层模块实现以太网数据的收发;开始传输控制模块控制以太网顶层模块开始/停止发送数据。
OV7725的以太网视频传输系统框图如下图所示:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第8张图片

图 7.5.13.1 系统框图
顶层模块的原理图如下图所示:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第9张图片

图 7.5.13.2 顶层模块原理图
FPGA顶层模块(ov7725_udp_pc)例化了以下七个模块:时钟模块(clk_wiz_0)、I2C驱动模块(i2c_dri)、I2C配置模块(i2c_ov7725_rgb565_cfg)、摄像头图像采集模块(cmos_capture_data)、开始传输控制模块(start_transfer_ctrl)、图像数据封装模块(img_data_pkt)和以太网顶层模块模块(eth_top)。
时钟模块(clk_wiz_0):时钟IP核模块通过调用MMCM IP核来实现,总共输出2个时钟,频率分别为50Mhz和200Mhz时钟。50Mhz时钟作为I2C驱动模块的驱动时钟;200Mhz时钟作为IDELAYCTRL源语的参考时钟。
I2C驱动模块(i2c_dri):I2C驱动模块负责驱动OV7725 SCCB接口总线,用户可根据该模块提供的用户接口可以很方便的对OV7725的寄存器进行配置,该模块和“EEPROM读写实验”章节中用到的I2C驱动模块为同一个模块,有关该模块的详细介绍请大家参考“EEPROM读写实验”章节。
I2C配置模块(i2c_ov7725_rgb565_cfg):I2C配置模块的驱动时钟是由I2C驱动模块输出的时钟提供的,这样方便了I2C驱动模块和I2C配置模块之间的数据交互。该模块寄存需要配置的寄存器地址、数据以及控制初始化的开始与结束,同时该模块输出OV7725的寄存器地址和数据以及控制I2C驱动模块开始执行的控制信号,直接连接到I2C驱动模块的用户接口,从而完成对OV7725传感器的初始化。
摄像头图像采集模块(cmos_capture_data):摄像头采集模块在像素时钟的驱动下将传感器输出的场同步信号、行同步信号以及8位数据转换成16位数据信号,完成对OV7725传感器图像的采集。
开始传输控制模块(start_transfer_ctrl):该模块解析以太网顶层模块接收到的数据,如果收到1个字节的ASCII码“1”,则表示以太网开始传输图像数据;如果收到1个字节的ASCII码“0”,则表示以太网停止传输图像数据。
图像数据封装模块(img_data_pkt):图像数据封装模块负责将输入16位的图像数据,拼接成32位数据,以及添加图像数据的帧头和行场分辨率。该模块控制着以太网发送模块发送的字节数,单次发送一行图像数据的字节数,模块内部例化了一个异步FIFO模块,用于缓存待发送的图像数据。
以太网顶层模块(eth_top):以太网顶层模块实现以太网通信的收发功能,有关该模块的详细介绍请大家参考“以太网UDP测试实验”章节。
顶层模块部分代码如下:

1   module ov7725_udp_pc(
2       input              sys_clk     , //系统时钟  
3       input              sys_rst_n   , //系统复位信号,低电平有效 
4       //以太网接口
5       input              eth_rxc     , //RGMII接收数据时钟
6       input              eth_rx_ctl  , //RGMII输入数据有效信号
7       input       [3:0]  eth_rxd     , //RGMII输入数据
8       output             eth_txc     , //RGMII发送数据时钟    
9       output             eth_tx_ctl  , //RGMII输出数据有效信号
10      output      [3:0]  eth_txd     , //RGMII输出数据          
11      output             eth_rst_n   , //以太网芯片复位信号,低电平有效    
12  
13      //摄像头接口
14      input              cam_pclk    , //cmos 数据像素时钟
15      input              cam_vsync   , //cmos 场同步信号
16      input              cam_href    , //cmos 行同步信号
17      input     [7:0]    cam_data    , //cmos 数据
18      output             cam_rst_n   , //cmos 复位信号,低电平有效
19      output             cam_sgm_ctrl, //cmos 时钟选择信号, 1:使用摄像头自带的晶振
20      output             cam_scl     , //cmos SCCB_SCL线
21      inout              cam_sda       //cmos SCCB_SDA线    
22  );
23  
24  //parameter define
25  //开发板MAC地址 00-11-22-33-44-55
26  parameter  BOARD_MAC = 48'h00_11_22_33_44_55;     
27  //开发板IP地址 192.168.1.10
28  parameter  BOARD_IP  = {8'd192,8'd168,8'd1,8'd10};  
29  //目的MAC地址 ff_ff_ff_ff_ff_ff
30  parameter  DES_MAC   = 48'hff_ff_ff_ff_ff_ff;    
31  //目的IP地址 192.168.1.102     
32  parameter  DES_IP    = {8'd192,8'd168,8'd1,8'd102};
33  
34  parameter  SLAVE_ADDR = 7'h21         ;  //OV7725的器件地址7'h21
35  parameter  BIT_CTRL   = 1'b0          ;  //OV7725的字节地址为8位  0:8位 1:16位
36  parameter  CLK_FREQ   = 26'd50_000_000;  //i2c_dri模块的驱动时钟频率
37  parameter  I2C_FREQ   = 18'd250_000   ;  //I2C的SCL时钟频率,不超过400KHz

在代码的第24至32行定义了四个参量:开发板MAC地址BOARD_MAC,开发板IP地址 BOARD_IP,目的MAC地址DES_MAC(这里指PC MAC地址),目的IP地址 DES_IP(PC IP地址)。开发板的MAC地址和IP地址是我们随意定义的,只要不和目的MAC 地址和目的IP地址一样就可以,否则会产生地址冲突。目的MAC地址这里写的是公共MAC 地址(48’hff_ff_ff_ff_ff_ff),也可以修改成电脑网口的MAC地址,DES_IP是对应电脑以太网的IP地址,这里定义的四个参数是向下传递的,需要修改MAC地址或者IP地址时直接在这里修改即可,而不用在以太网顶层模块里面修改。
在代码的第34行定义了OV7725的器件地址,其器件地址为7’h21;第35行定义了寄存器地址的位宽,BIT_CTRL=0表示地址位宽为8位,BIT_CTRL=1表示地址位宽为16位。因为OV7725的地址位宽为8位,所以BIT_CTRL设置为0。第36行和第37行分别定义了i2c_dri模块的驱动时钟频率和I2C的SCL时钟频率。

70  assign  rst_n = sys_rst_n & locked;
71  //不对摄像头硬件复位,固定高电平
72  assign  cam_rst_n = 1'b1;
73  //cmos 时钟选择信号, 1:使用摄像头自带的晶振
74  assign  cam_sgm_ctrl = 1'b1;
75  
76  //例化MMCM
77  clk_wiz_0 u_clk_wiz_0
78     (
79      .clk_out1    (clk_50m),  
80      .clk_out2    (clk_200m), 
81      .reset       (~sys_rst_n),  
82      .locked      (locked),       
83      .clk_in1     (sys_clk)
84      );      
85  
86  //I2C配置模块    
87  i2c_ov7725_rgb565_cfg u_i2c_cfg(
88      .clk           (i2c_dri_clk),
89      .rst_n         (rst_n),
90      .i2c_done      (i2c_done),
91      .i2c_exec      (i2c_exec),
92      .i2c_data      (i2c_data),
93      .init_done     (cam_init_done)
94      );    
95  
96  //I2C驱动模块
97  i2c_dri 
98     #(
99      .SLAVE_ADDR  (SLAVE_ADDR),               //参数传递
100     .CLK_FREQ    (CLK_FREQ  ),              
101     .I2C_FREQ    (I2C_FREQ  )                
102     ) 
103    u_i2c_dri(
104     .clk         (clk_50m   ),   
105     .rst_n       (rst_n     ),   
106     //i2c interface
107     .i2c_exec    (i2c_exec  ),   
108     .bit_ctrl    (BIT_CTRL  ),   
109     .i2c_rh_wl   (1'b0),                     //固定为0,只用到了IIC驱动的写操作   
110     .i2c_addr    (i2c_data[15:8]),   
111     .i2c_data_w  (i2c_data[7:0]),   
112     .i2c_data_r  (),   
113     .i2c_done    (i2c_done  ),   
114     .scl         (cam_scl   ),   
115     .sda         (cam_sda   ),   
116     //user interface
117     .dri_clk     (i2c_dri_clk)               //I2C操作时钟
118 );
119 
120 //摄像头数据采集模块
121 cmos_capture_data u_cmos_capture_data(
122 
123     .rst_n              (rst_n & cam_init_done),
124     .cam_pclk           (cam_pclk),   
125     .cam_vsync          (cam_vsync),
126     .cam_href           (cam_href),
127     .cam_data           (cam_data),           
128     .cmos_frame_vsync   (cmos_frame_vsync),
129     .cmos_frame_href    (),
130     .cmos_frame_valid   (img_data_en),     
131     .cmos_frame_data    (img_data)             
132     );

OV7725摄像头配置模块和IIC驱动模块实现对OV7725摄像头的初始化,在初始化完成后拉高cam_init_done信号,此时开始通过摄像头数据采集模块接收摄像头输出的图像数据(如程序中第123行代码所示),将输入的8位数据转换成16位RGB565数据。

134 //开始传输控制模块   
135 start_transfer_ctrl u_start_transfer_ctrl(
136     .clk                (eth_rx_clk),
137     .rst_n              (rst_n),
138     .udp_rec_pkt_done   (udp_rec_pkt_done),
139     .udp_rec_en         (udp_rec_en      ),
140     .udp_rec_data       (udp_rec_data    ),
141     .udp_rec_byte_num   (udp_rec_byte_num),
142 
143     .transfer_flag      (transfer_flag)      //图像开始传输标志,1:开始传输 0:停止传输
144     );       
145      
146 //图像封装模块     
147 img_data_pkt u_img_data_pkt(    
148     .rst_n              (rst_n),              
149    
150     .cam_pclk           (cam_pclk),
151     .img_vsync          (cmos_frame_vsync),
152     .img_data_en        (img_data_en),
153     .img_data           (img_data),
154     .transfer_flag      (transfer_flag),            
155     .eth_tx_clk         (eth_tx_clk     ),
156     .udp_tx_req         (udp_tx_req     ),
157     .udp_tx_done        (udp_tx_done    ),
158     .udp_tx_start_en    (udp_tx_start_en),
159     .udp_tx_data        (udp_tx_data    ),
160     .udp_tx_byte_num    (udp_tx_byte_num)
161     );  
162 
163 //以太网顶层模块    
164 eth_top  #(
165     .BOARD_MAC     (BOARD_MAC),              //参数例化
166     .BOARD_IP      (BOARD_IP ),          
167     .DES_MAC       (DES_MAC  ),          
168     .DES_IP        (DES_IP   )          
169     )          
170     u_eth_top(          
171     .sys_rst_n       (rst_n     ),           //系统复位信号,低电平有效           
172     .clk_200m        (clk_200m), 
173     //以太网RGMII接口             
174     .eth_rxc         (eth_rxc   ),           //RGMII接收数据时钟
175     .eth_rx_ctl      (eth_rx_ctl),           //RGMII输入数据有效信号
176     .eth_rxd         (eth_rxd   ),           //RGMII输入数据
177     .eth_txc         (eth_txc   ),           //RGMII发送数据时钟    
178     .eth_tx_ctl      (eth_tx_ctl),           //RGMII输出数据有效信号
179     .eth_txd         (eth_txd   ),           //RGMII输出数据          
180     .eth_rst_n       (eth_rst_n ),           //以太网芯片复位信号,低电平有效 
181 
182     .gmii_rx_clk     (eth_rx_clk),
183     .gmii_tx_clk     (eth_tx_clk),       
184     .udp_tx_start_en (udp_tx_start_en),
185     .tx_data         (udp_tx_data),
186     .tx_byte_num     (udp_tx_byte_num),
187     .udp_tx_done     (udp_tx_done),
188     .tx_req          (udp_tx_req ),
189     .rec_pkt_done    (udp_rec_pkt_done),
190     .rec_en          (udp_rec_en      ),
191     .rec_data        (udp_rec_data    ),
192     .rec_byte_num    (udp_rec_byte_num)
193     );
194 
195 endmodule

在代码的第135行至144行例化了开始传输控制模块,由该模块端口可知,输入端口信号为以太网接收到的数据,而输出信号为图像开始传输标志(transfer_flag)。transfer_flag信号用于控制以太网发送图像数据的开始和停止,连接至图像数据封装模块。
在代码的第147行至161行例化了图像数据封装模块,该模块输入的端口信号为摄像头图像数据,而输出的udp_tx_start_en(以太网开始发送信号)和udp_tx_byte_num(发送的字节数)连接至以太网顶层模块的以太网发送控制端口,从而控制以太网的UDP发送模块开始发送图像数据。
I2C配置模块寄存需要配置的寄存器地址、数据以及控制初始化的开始与结束,代码如下所示:

1   module i2c_ov7725_rgb565_cfg(  
2       input                clk      ,  //时钟信号
3       input                rst_n    ,  //复位信号,低电平有效
4       
5       input                i2c_done ,  //I2C寄存器配置完成信号
6       output  reg          i2c_exec ,  //I2C触发执行信号   
7       output  reg  [15:0]  i2c_data ,  //I2C要配置的地址与数据(高8位地址,低8位数据)
8       output  reg          init_done   //初始化完成信号
9       );
10  
11  //parameter define
12  parameter  REG_NUM = 7'd70   ;       //总共需要配置的寄存器个数
13  
14  //reg define
15  reg    [9:0]   start_init_cnt;       //等待延时计数器
16  reg    [6:0]   init_reg_cnt  ;       //寄存器配置个数计数器
17  
18  //*****************************************************
19  //**                    main code
20  //*****************************************************
21  
22  //cam_scl配置成250khz,输入的clk为1Mhz,周期为1us,1023*1us = 1.023ms
23  //寄存器延时配置
24  always @(posedge clk or negedge rst_n) begin
25      if(!rst_n)
26          start_init_cnt <= 10'b0;    
27      else if((init_reg_cnt == 7'd1) && i2c_done)
28          start_init_cnt <= 10'b0;
29      else if(start_init_cnt < 10'd1023) begin
30          start_init_cnt <= start_init_cnt + 1'b1;                    
31      end
32  end
33  
34  //寄存器配置个数计数    
35  always @(posedge clk or negedge rst_n) begin
36      if(!rst_n)
37          init_reg_cnt <= 7'd0;
38      else if(i2c_exec)   
39          init_reg_cnt <= init_reg_cnt + 7'b1;
40  end         
41  
42  //i2c触发执行信号   
43  always @(posedge clk or negedge rst_n) begin
44      if(!rst_n)
45          i2c_exec <= 1'b0;
46      else if(start_init_cnt == 10'd1022)
47          i2c_exec <= 1'b1;
48      //只有刚上电和配置第一个寄存器增加延时
49      else if(i2c_done && (init_reg_cnt != 7'd1) && (init_reg_cnt < REG_NUM))
50          i2c_exec <= 1'b1;
51      else
52          i2c_exec <= 1'b0;    
53  end 
54  
55  //初始化完成信号
56  always @(posedge clk or negedge rst_n) begin
57      if(!rst_n)
58          init_done <= 1'b0;
59      else if((init_reg_cnt == REG_NUM) && i2c_done)  
60          init_done <= 1'b1;  
61  end        
62     
63  //配置寄存器地址与数据
64  always @(posedge clk or negedge rst_n) begin
65      if(!rst_n)
66          i2c_data <= 16'b0;
67      else begin
68          case(init_reg_cnt)
69              //先对寄存器进行软件复位,使寄存器恢复初始值
70              //寄存器软件复位后,需要延时1ms才能配置其它寄存器
71              7'd0  : i2c_data <= {8'h12, 8'h80}; //COM7 BIT[7]:复位所有的寄存器
72              7'd1  : i2c_data <= {8'h3d, 8'h03}; //COM12 模拟过程直流补偿
73              7'd2  : i2c_data <= {8'h15, 8'h00}; //COM10 href/vsync/pclk/data信号控制
74              7'd3  : i2c_data <= {8'h17, 8'h23}; //HSTART 水平起始位置
75              7'd4  : i2c_data <= {8'h18, 8'ha0}; //HSIZE 水平尺寸
76              7'd5  : i2c_data <= {8'h19, 8'h07}; //VSTRT 垂直起始位置
77              7'd6  : i2c_data <= {8'h1a, 8'hf0}; //VSIZE 垂直尺寸            
78              7'd7  : i2c_data <= {8'h32, 8'h00}; //HREF 图像开始和尺寸控制,控制低位
79              7'd8  : i2c_data <= {8'h29, 8'ha0}; //HOutSize 水平输出尺寸
80              7'd9  : i2c_data <= {8'h2a, 8'h00}; //EXHCH 虚拟像素MSB
81              7'd10 : i2c_data <= {8'h2b, 8'h00}; //EXHCL 虚拟像素LSB
82              7'd11 : i2c_data <= {8'h2c, 8'hf0}; //VOutSize 垂直输出尺寸
83              7'd12 : i2c_data <= {8'h0d, 8'h41}; //COM4 PLL倍频设置(multiplier)
84                                                  //Bit[7:6]:  0:1x 1:4x 2:6x 3:8x
85              7'd13 : i2c_data <= {8'h11, 8'h00}; //CLKRC 内部时钟配置 
86                                        //Freq= input_clk * multiplier/[(CLKRC[5:0]+1)*2]
87              7'd14 : i2c_data <= {8'h12, 8'h06}; //COM7 输出VGA RGB565格式                                     
88              7'd15 : i2c_data <= {8'h0c, 8'h10}; //COM3 Bit[0]: 0:图像数据 1:彩条测试
89              //DSP 控制
90              7'd16 : i2c_data <= {8'h42, 8'h7f}; //TGT_B 黑电平校准蓝色通道目标值
91              7'd17 : i2c_data <= {8'h4d, 8'h09}; //FixGain 模拟增益放大器
92              7'd18 : i2c_data <= {8'h63, 8'hf0}; //AWB_Ctrl0 自动白平衡控制字节0
93              7'd19 : i2c_data <= {8'h64, 8'hff}; //DSP_Ctrl1 DSP控制字节1
94              7'd20 : i2c_data <= {8'h65, 8'h00}; //DSP_Ctrl2 DSP控制字节2
95              7'd21 : i2c_data <= {8'h66, 8'h00}; //DSP_Ctrl3 DSP控制字节3
96              7'd22 : i2c_data <= {8'h67, 8'h00}; //DSP_Ctrl4 DSP控制字节4    
97              //AGC AEC AWB        
98              //COM8 Bit[2]:自动增益使能 Bit[1]:自动白平衡使能 Bit[0]:自动曝光功能
99              7'd23 : i2c_data <= {8'h13, 8'hff}; //COM8 
100             7'd24 : i2c_data <= {8'h0f, 8'hc5}; //COM6
101             7'd25 : i2c_data <= {8'h14, 8'h11};  
102             7'd26 : i2c_data <= {8'h22, 8'h98}; 
103             7'd27 : i2c_data <= {8'h23, 8'h03};  
104             7'd28 : i2c_data <= {8'h24, 8'h40}; 
105             7'd29 : i2c_data <= {8'h25, 8'h30};  
106             7'd30: i2c_data <= {8'h26, 8'ha1};      
107             7'd31: i2c_data <= {8'h6b, 8'haa}; 
108             7'd32: i2c_data <= {8'h13, 8'hff};  
109             //matrix sharpness brightness contrast UV
110             7'd33 : i2c_data <= {8'h90, 8'h0a}; //EDGE1 边缘增强控制1
111             //DNSOff 降噪阈值下限,仅在自动模式下有效
112             7'd34 : i2c_data <= {8'h91, 8'h01}; //DNSOff 
113             7'd35 : i2c_data <= {8'h92, 8'h01}; //EDGE2 锐度(边缘增强)强度上限
114             7'd36 : i2c_data <= {8'h93, 8'h01}; //EDGE3 锐度(边缘增强)强度下限
115             7'd37 : i2c_data <= {8'h94, 8'h5f}; //MTX1 矩阵系数1
116             7'd38 : i2c_data <= {8'h95, 8'h53}; //MTX1 矩阵系数2
117             7'd39 : i2c_data <= {8'h96, 8'h11}; //MTX1 矩阵系数3
118             7'd40 : i2c_data <= {8'h97, 8'h1a}; //MTX1 矩阵系数4
119             7'd41 : i2c_data <= {8'h98, 8'h3d}; //MTX1 矩阵系数5
120             7'd42 : i2c_data <= {8'h99, 8'h5a}; //MTX1 矩阵系数6
121             7'd43 : i2c_data <= {8'h9a, 8'h1e}; //MTX_Ctrl 矩阵控制
122             7'd44 : i2c_data <= {8'h9b, 8'h3f}; //BRIGHT 亮度
123             7'd45 : i2c_data <= {8'h9c, 8'h25}; //CNST 对比度            
124             7'd46 : i2c_data <= {8'h9e, 8'h81}; 
125             7'd47 : i2c_data <= {8'ha6, 8'h06}; //SDE 特殊数字效果控制
126             7'd48 : i2c_data <= {8'ha7, 8'h65}; //USAT "U"饱和增益
127             7'd49 : i2c_data <= {8'ha8, 8'h65}; //VSAT "V"饱和增益            
128             7'd50 : i2c_data <= {8'ha9, 8'h80}; //VSAT "V"饱和增益   
129             7'd51 : i2c_data <= {8'haa, 8'h80}; //VSAT "V"饱和增益
130             //伽马控制 
131             7'd52 : i2c_data <= {8'h7e, 8'h0c}; 
132             7'd53 : i2c_data <= {8'h7f, 8'h16}; 
133             7'd54 : i2c_data <= {8'h80, 8'h2a}; 
134             7'd55 : i2c_data <= {8'h81, 8'h4e}; 
135             7'd56 : i2c_data <= {8'h82, 8'h61}; 
136             7'd57 : i2c_data <= {8'h83, 8'h6f}; 
137             7'd58 : i2c_data <= {8'h84, 8'h7b}; 
138             7'd59 : i2c_data <= {8'h85, 8'h86};   
139             7'd60 : i2c_data <= {8'h86, 8'h8e}; 
140             7'd61 : i2c_data <= {8'h87, 8'h97}; 
141             7'd62 : i2c_data <= {8'h88, 8'ha4}; 
142             7'd63 : i2c_data <= {8'h89, 8'haf}; 
143             7'd64 : i2c_data <= {8'h8a, 8'hc5}; 
144             7'd65 : i2c_data <= {8'h8b, 8'hd7}; 
145             7'd66 : i2c_data <= {8'h8c, 8'he8}; 
146             7'd67 : i2c_data <= {8'h8d, 8'h20}; 
147             
148             7'd68 : i2c_data <= {8'h0e, 8'h65}; //COM5
149             7'd69 : i2c_data <= {8'h09, 8'h00}; //COM2  Bit[1:0] 输出电流驱动能力
150             //只读存储器,防止在case中没有列举的情况,之前的寄存器被重复改写
151             default:i2c_data <= {8'h1C, 8'h7F}; //MIDH 制造商ID 高8位
152         endcase
153     end
154 end
155 
156 endmodule

在代码的第12行定义了总共需要配置的寄存器的个数,如果增加或者删减了寄存器的配置,需要修改此参数。
图像传感器刚开始上电时电压有可能不够稳定,所以程序中的23行至32行定义了一个延时计数器(start_init_cnt)等待传感器工作在稳定的状态。当计数器计数到预设值之后,开始第一次配置传感器即软件复位,目的是让所有的寄存器复位到默认的状态。从前面介绍的OV7725的特性可知,软件复位需要等待1ms的时间才能配置其它的寄存器,因此发送完软件复位命令后,延时计数器清零,并重新开始计数。当计数器计数到预设值之后,紧接着配置剩下的寄存器。只有软件复位命令需要1ms的等待时间,其它寄存器不需要等待时间,直接按照程序中定义的顺序发送即可。
在程序的83至86行,说明了关于摄像头输出时钟的寄存器配置,摄像头的地址0x0d配置成0x41,表示PLL倍频设置设为了4倍频,摄像头的地址0x11配置成0x00,而摄像头的输入时钟为12M,所以根据第86行的公式可得到摄像头的输出时钟为24M。
CMOS图像数据采集模块的代码如下所示:

1   module cmos_capture_data(
2       input                 rst_n            ,  //复位信号    
3       //摄像头接口                           
4       input                 cam_pclk         ,  //cmos 数据像素时钟
5       input                 cam_vsync        ,  //cmos 场同步信号
6       input                 cam_href         ,  //cmos 行同步信号
7       input  [7:0]          cam_data         ,                      
8       //用户接口                              
9       output                cmos_frame_vsync ,  //帧有效信号    
10      output                cmos_frame_href  ,  //行有效信号
11      output                cmos_frame_valid ,  //数据有效使能信号
12      output       [15:0]   cmos_frame_data     //有效数据        
13      );
14  
15  //寄存器全部配置完成后,先等待10帧数据
16  //待寄存器配置生效后再开始采集图像
17  parameter  WAIT_FRAME = 4'd10    ;            //寄存器数据稳定等待的帧个数            
18                                   
19  //reg define                     
20  reg             cam_vsync_d0     ;
21  reg             cam_vsync_d1     ;
22  reg             cam_href_d0      ;
23  reg             cam_href_d1      ;
24  reg    [3:0]    cmos_ps_cnt      ;            //等待帧数稳定计数器
25  reg    [7:0]    cam_data_d0      ;            
26  reg    [15:0]   cmos_data_t      ;            //用于8位转16位的临时寄存器
27  reg             byte_flag        ;            //16位RGB数据转换完成的标志信号
28  reg             byte_flag_d0     ;
29  reg             frame_val_flag   ;            //帧有效的标志 
30  
31  wire            pos_vsync        ;            //采输入场同步信号的上升沿
32  
33  //*****************************************************
34  //**                    main code
35  //*****************************************************
36  
37  //采输入场同步信号的上升沿
38  assign pos_vsync = (~cam_vsync_d1) & cam_vsync_d0; 
39  
40  //输出帧有效信号
41  assign  cmos_frame_vsync = frame_val_flag  ?  cam_vsync_d1  :  1'b0; 
42  
43  //输出行有效信号
44  assign  cmos_frame_href = frame_val_flag  ?  cam_href_d1   :  1'b0; 
45  
46  //输出数据使能有效信号
47  assign  cmos_frame_valid = frame_val_flag  ?  byte_flag_d0  :  1'b0; 
48  
49  //输出数据
50  assign  cmos_frame_data = frame_val_flag  ?  cmos_data_t   :  1'b0; 
51         
52  always @(posedge cam_pclk or negedge rst_n) begin
53      if(!rst_n) begin
54          cam_vsync_d0 <= 1'b0;
55          cam_vsync_d1 <= 1'b0;
56          cam_href_d0 <= 1'b0;
57          cam_href_d1 <= 1'b0;
58      end
59      else begin
60          cam_vsync_d0 <= cam_vsync;
61          cam_vsync_d1 <= cam_vsync_d0;
62          cam_href_d0 <= cam_href;
63          cam_href_d1 <= cam_href_d0;
64      end
65  end
66  
67  //对帧数进行计数
68  always @(posedge cam_pclk or negedge rst_n) begin
69      if(!rst_n)
70          cmos_ps_cnt <= 4'd0;
71      else if(pos_vsync && (cmos_ps_cnt < WAIT_FRAME))
72          cmos_ps_cnt <= cmos_ps_cnt + 4'd1;
73  end
74  
75  //帧有效标志
76  always @(posedge cam_pclk or negedge rst_n) begin
77      if(!rst_n)
78          frame_val_flag <= 1'b0;
79      else if((cmos_ps_cnt == WAIT_FRAME) && pos_vsync)
80          frame_val_flag <= 1'b1;
81      else;    
82  end            
83  
84  //8位数据转16位RGB565数据        
85  always @(posedge cam_pclk or negedge rst_n) begin
86      if(!rst_n) begin
87          cmos_data_t <= 16'd0;
88          cam_data_d0 <= 8'd0;
89          byte_flag <= 1'b0;
90      end
91      else if(cam_href) begin
92          byte_flag <= ~byte_flag;
93          cam_data_d0 <= cam_data;
94          if(byte_flag)
95              cmos_data_t <= {cam_data_d0,cam_data};
96          else;   
97      end
98      else begin
99          byte_flag <= 1'b0;
100         cam_data_d0 <= 8'b0;
101     end    
102 end        
103 
104 //产生输出数据有效信号(cmos_frame_valid)
105 always @(posedge cam_pclk or negedge rst_n) begin
106     if(!rst_n)
107         byte_flag_d0 <= 1'b0;
108     else
109         byte_flag_d0 <= byte_flag;  
110 end 
111        
112 endmodule

CMOS图像采集模块第17行定义了参数WAIT_FRAME(寄存器数据稳定等待的帧个数),这个参数是设置摄像头初始化完成后,等待摄像头输出稳定数据的帧数,此处等待10帧,即通过采集场同步信号的上升沿来统计帧数,计数器计数超过10次之后产生数据有效的标志,开始采集图像。在程序的第84行开始的always块实现了8位数据转16位数据的功能。需要注意的是摄像头的图像数据是在像素时钟(cam_pclk)下输出的,因此摄像头的图像数据必须使用像素钟来采集,否则会造成数据采集错误。
开始传输控制模块的代码如下:

1  module start_transfer_ctrl(
2      input                 clk                ,   //时钟信号
3      input                 rst_n              ,   //复位信号,低电平有效
4      input                 udp_rec_pkt_done   ,   //GMII接收时钟 
5      input                 udp_rec_en         ,   //UDP单包数据接收完成信号
6      input        [31:0]   udp_rec_data       ,   //UDP接收的数据使能信号 
7      input        [15:0]   udp_rec_byte_num   ,   //UDP接收的数据
8                                                   //UDP接收到的字节数
9      output  reg           transfer_flag          //图像开始传输标志,1:开始传输 0:停止传输
10     );    
11     
12 //parameter define
13 parameter  START = "1";  //开始命令
14 parameter  STOP  = "0";  //停止命令
15 
16 //*****************************************************
17 //**                    main code
18 //*****************************************************
19 
20 //解析接收到的数据
21 always @(posedge clk or negedge rst_n) begin
22     if(!rst_n) 
23         transfer_flag <= 1'b0;
24     else if(udp_rec_pkt_done && udp_rec_byte_num == 1'b1) begin
25         if(udp_rec_data[31:24] == START)         //开始传输
26             transfer_flag <= 1'b1;
27         else if(udp_rec_data[31:24] == STOP)     //停止传输
28             transfer_flag <= 1'b0;
29     end
30 end 
31 
32 endmodule

开始传输控制模块的代码比较简单,当接收到UDP的数据包,并且这个数据包的有效字节个数为1时,开始对接收到的数据进行判断。如果这个数据等于ASCII码的“1”时,此时将transfer_flag赋值为1,表示以太网开始发送图像数据;如果这个数据等于ASCII码的“0”时,此时将transfer_flag赋值为0,表示以太网停止发送图像数据。
图像封装模块的代码如下:

1   module img_data_pkt(
2       input                 rst_n          ,   //复位信号,低电平有效
3       //图像相关信号
4       input                 cam_pclk       ,   //像素时钟
5       input                 img_vsync      ,   //帧同步信号
6       input                 img_data_en    ,   //数据有效使能信号
7       input        [15:0]   img_data       ,   //有效数据 
8       
9       input                 transfer_flag  ,   //图像开始传输标志,1:开始传输 0:停止传输
10      //以太网相关信号 
11      input                 eth_tx_clk     ,   //以太网发送时钟
12      input                 udp_tx_req     ,   //udp发送数据请求信号
13      input                 udp_tx_done    ,   //udp发送数据完成信号                               
14      output  reg           udp_tx_start_en,   //udp开始发送信号
15      output       [31:0]   udp_tx_data    ,   //udp发送的数据
16      output  reg  [15:0]   udp_tx_byte_num    //udp单包发送的有效字节数
17      );    
18      
19  //parameter define
20  parameter  CMOS_H_PIXEL = 16'd640;  //图像水平方向分辨率
21  parameter  CMOS_V_PIXEL = 16'd480;  //图像垂直方向分辨率
22  //图像帧头,用于标志一帧数据的开始
23  parameter  IMG_FRAME_HEAD = {32'hf0_5a_a5_0f};

程序中第20行至23行定义了三个参数,分别是CMOS_H_PIXEL(图像水平方向分辨率)、CMOS_V_PIXEL(图像垂直方向分辨率)和IMG_FRAME_HEAD(图像帧头)。我们在用网口传输图像数据时,一次发送一行图像数据。在发送一帧图像的第一行数据时,在一行数据的开头添加图像的帧头和图像的行场分辨率,共8个字节,图像的帧头是32’hf0_5a_a5_0f,共占用4个字节;而图像的行场分辨率占用4个字节,本次实验传输的图像分辨率为640*480。
这里我们来详细说明下在一帧的第一行添加帧头的原因。在使用UDP传输数据时,有时会出现数据丢包的现象。这里说的丢包现象并不是开发板没有把数据发送出去,而是电脑接收端系统繁忙或者其它的原因没有接收到数据。为了降低丢包对视频显示带来的影响,我们为每帧图像添加一个帧头,上位机解析到帧头之后,接下来将接收到的数据重新放到图像显示区域的起始位置,保证了在视频传输过程中,即使出现丢包的现象,视频也能恢复到正常显示的画面。图像帧头的值要尽量选择图像数据不容易出现的值,否则很容易把图像数据当成帧头,程序中把图像帧头设置为{32’hf0_5a_a5_0f},图像中出现的像素数据和帧头相同的概率极低。除此之外,上位机软件按照帧头为{32’hf0_5a_a5_0f}来解析数据,所以帧头的值不可以随意修改。

25  reg             img_vsync_d0    ;  //帧有效信号打拍
26  reg             img_vsync_d1    ;  //帧有效信号打拍
27  reg             neg_vsync_d0    ;  //帧有效信号下降沿打拍
28                                  
29  reg             wr_sw           ;  //用于位拼接的标志
30  reg    [15:0]   img_data_d0     ;  //有效图像数据打拍
31  reg             wr_fifo_en      ;  //写fifo使能
32  reg    [31:0]   wr_fifo_data    ;  //写fifo数据
33  
34  reg             img_vsync_txc_d0;  //以太网发送时钟域下,帧有效信号打拍
35  reg             img_vsync_txc_d1;  //以太网发送时钟域下,帧有效信号打拍
36  reg             tx_busy_flag    ;  //发送忙信号标志
37                                  
38  //wire define                   
39  wire            pos_vsync       ;  //帧有效信号上升沿
40  wire            neg_vsync       ;  //帧有效信号下降沿
41  wire            neg_vsynt_txc   ;  //以太网发送时钟域下,帧有效信号下降沿
42  wire   [9:0]    fifo_rdusedw    ;  //当前FIFO缓存的个数
43  
44  //*****************************************************
45  //**                    main code
46  //*****************************************************
47  
48  //信号采沿
49  assign neg_vsync = img_vsync_d1 & (~img_vsync_d0);
50  assign pos_vsync = ~img_vsync_d1 & img_vsync_d0;
51  assign neg_vsynt_txc = ~img_vsync_txc_d1 & img_vsync_txc_d0;
52  
53  //对img_vsync信号延时两个时钟周期,用于采沿
54  always @(posedge cam_pclk or negedge rst_n) begin
55      if(!rst_n) begin
56          img_vsync_d0 <= 1'b0;
57          img_vsync_d1 <= 1'b0;
58      end
59      else begin
60          img_vsync_d0 <= img_vsync;
61          img_vsync_d1 <= img_vsync_d0;
62      end
63  end
64  
65  //寄存neg_vsync信号
66  always @(posedge cam_pclk or negedge rst_n) begin
67      if(!rst_n) 
68          neg_vsync_d0 <= 1'b0;
69      else 
70          neg_vsync_d0 <= neg_vsync;
71  end    

程序中第54行至63行在cam_pclk时钟下对img_vsync信号延时两拍,用于采该信号的上升沿和下降沿。程序中第66行至71行对下降沿延时一拍,我们后面的处理中会用到这些信号。

73  //对wr_sw和img_data_d0信号赋值,用于位拼接
74  always @(posedge cam_pclk or negedge rst_n) begin
75      if(!rst_n) begin
76          wr_sw <= 1'b0;
77          img_data_d0 <= 1'b0;
78      end
79       else if(neg_vsync)
80          wr_sw <= 1'b0;
81      else if(img_data_en) begin
82          wr_sw <= ~wr_sw;
83          img_data_d0 <= img_data;
84      end    
85  end 

程序中第74行至85行代码对wr_sw和img_data_d0信号进行赋值,用于位拼接。这是由于输入的图像数据为16位,而以太网顶层模块的用户数据端口是32位,因此这里需要对输入的数据进行寄存,用于将16位拼接成32位。

87  //将帧头和图像数据写入FIFO
88  always @(posedge cam_pclk or negedge rst_n) begin
89      if(!rst_n) begin
90          wr_fifo_en <= 1'b0;
91          wr_fifo_data <= 1'b0;
92      end
93      else begin
94          if(neg_vsync) begin
95              wr_fifo_en <= 1'b1;
96              wr_fifo_data <= IMG_FRAME_HEAD;               //帧头
97          end
98          else if(neg_vsync_d0) begin
99              wr_fifo_en <= 1'b1;
100             wr_fifo_data <= {CMOS_H_PIXEL,CMOS_V_PIXEL};  //水平和垂直方向分辨率
101         end
102         else if(img_data_en && wr_sw) begin
103             wr_fifo_en <= 1'b1;
104             wr_fifo_data <= {img_data_d0,img_data};       //图像数据位拼接,16位转32位
105           end
106         else begin
107             wr_fifo_en <= 1'b0;
108             wr_fifo_data <= 1'b0;        
109         end
110     end
111 end

本次模块中例化了异步FIFO,我们需要将一帧的帧头、图像行场分辨率和拼接后的数据都写入FIFO中。需要注意的是,只有一帧图像的第一行图像才需要添加帧头和图像行场分辨率,而其余行的图像是不需要添加的,因此我们需要根据img_vsync(帧同步信号)来判断什么时候输入的数据是第一行的图像。
img_vsync作为帧同步信号,该信号的下降沿之后输入的数据,就是一帧的第一行数据。因此我们在img_vsync的下降沿将帧头IMG_FRAME_HEAD写入FIFO,在下一个时钟周期将行场分辨率{CMOS_H_PIXEL,CMOS_V_PIXEL}写入FIFO中。当img_data_en信号和wr_sw同时为高时,将拼接后的32位数据写入FIFO中即可。

113 //以太网发送时钟域下,对img_vsync信号延时两个时钟周期,用于采沿
114 always @(posedge eth_tx_clk or negedge rst_n) begin
115     if(!rst_n) begin
116         img_vsync_txc_d0 <= 1'b0;
117         img_vsync_txc_d1 <= 1'b0;
118     end
119     else begin
120         img_vsync_txc_d0 <= img_vsync;
121         img_vsync_txc_d1 <= img_vsync_txc_d0;
122     end
123 end
由于cam_pclk和eth_tx_clk为异步时钟,因此不能直接在eth_tx_clk时钟域下直接采集neg_vsync信号(该信号在cam_pclk时钟域下产生),因此这里在eth_tx_clk时钟域下重新对img_vsync信号进行打拍和采沿。
125 //控制以太网发送的字节数
126 always @(posedge eth_tx_clk or negedge rst_n) begin
127     if(!rst_n)
128         udp_tx_byte_num <= 1'b0;
129     else if(neg_vsynt_txc)
130         udp_tx_byte_num <= {CMOS_H_PIXEL,1'b0} + 16'd8;
131     else if(udp_tx_done)    
132         udp_tx_byte_num <= {CMOS_H_PIXEL,1'b0};
133 end
我们只在一帧的第一行添加了帧头和行场分辨率,因此只有在发送第一行图像数据时,发送的UDP字节数为1288640*2+8),而其余行单包发送的字节数为1280135 //控制以太网发送开始信号
136 always @(posedge eth_tx_clk or negedge rst_n) begin
137     if(!rst_n) begin
138         udp_tx_start_en <= 1'b0;
139         tx_busy_flag <= 1'b0;
140     end
141     //上位机未发送“开始”命令时,以太网不发送图像数据
142     else if(transfer_flag == 1'b0) begin
143         udp_tx_start_en <= 1'b0;
144         tx_busy_flag <= 1'b0;        
145     end
146     else begin
147         udp_tx_start_en <= 1'b0;
148         //当FIFO中的个数满足需要发送的字节数时
149         if(tx_busy_flag == 1'b0 && fifo_rdusedw >= udp_tx_byte_num[15:2]) begin
150             udp_tx_start_en <= 1'b1;                     //开始控制发送一包数据
151             tx_busy_flag <= 1'b1;
152         end
153         else if(udp_tx_done || neg_vsynt_txc) 
154             tx_busy_flag <= 1'b0;
155     end
156 end

当上位机发送“开始”命令后,transfer_flag信号为高电平,此时以太网开始传输图像数据。当以太网发送空闲时,且FIFO中的字节数达到待发送的字节数时,此时拉高udp_tx_start_en信号,开始控制以太网顶层模块读取FIFO,并将FIFO中的数据发送给上位机。

158 //异步FIFO
159 async_fifo_1024x32b async_fifo_1024x32b_inst (
160   .rst(pos_vsync | (~transfer_flag)), // FIFO复位控制
161   .wr_clk(cam_pclk),                  // FIFO写时钟
162   .rd_clk(eth_tx_clk),                // FIFO读时钟
163   .din(wr_fifo_data),                 // FIFO写数据
164   .wr_en(wr_fifo_en),                 // FIFO写使能
165   .rd_en(udp_tx_req),                 // FIFO读使能
166   .dout(udp_tx_data),                 // FIFO读数据
167   .full(),                       
168   .empty(),                 
169   .rd_data_count(fifo_rdusedw),       // FIFO读侧数据个数
170   .wr_rst_busy(),      
171   .rd_rst_busy()     
172 );   
173 
174 endmodule

程序中第158行至172行代码例化了异步FIFO,存储深度为1024,数据位宽为32位。值得一提的是,我们帧同步的上升沿和transfer_flag信号作为FIFO的复位信号,避免上位机在传输图像中途发送停止命令,FIFO没有被清空的情况。
下图为图像数据封装模块采集过程中ILA抓取的波形图,在img_vsync的下降沿,依次将图像帧头和行场分辨率写入fifo(如下图的wr_fifo_en和wr_fifo_data),帧头为32’hf0_5a_a5_0f,行分辨率为16’h0280(640),场分辨率为32’h01e0(32’h480)。
在这里插入图片描述

图 7.5.13.3 ILA波形图
35.5 下载验证
将下载器一端连接电脑,另一端与开发板上的JTAG下载口连接;将网线一端连接开发板的网口,另一端连接电脑的网口;将OV7725摄像头连接开发板上的摄像头接口,注意镜头方向朝外,如下图所示。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第10张图片

图 7.5.13.1 开发板硬件连接示意图
接下来我们打开电源开关,并下载程序。程序下载完成后,此时开发板上还看不到现象,我们接下来打开正点原子UDP传输视频显示的上位机,位于资料盘(A盘)/6_软件资料/1_软件/video_transfer文件夹下,如下图所示:
在这里插入图片描述

图 7.5.13.2 打开上位机软件
双击video_transfer.exe即可打开软件,如果软件无法打开,先双击安装上图中的vc_redist.x64.exe,安装完成后再打开video_transfer.exe。
打开后的界面如下图所示。
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第11张图片

图 7.5.13.3 上位机设置界面
按照上图的界面进行设置,这些设置和程序中定义的参数和代码是对应的。设置完成后,点击“打开”按钮,此时上位机会通过网口向开发板发送ASCII码“1”,开发板收到开始命令后,会开始传输图像数据,如下图所示,如果图像画面显示有模糊不聚焦的现象可以通过旋转摄像头镜头进行聚焦调试,正常显示如下图所示:
【正点原子FPGA连载】 第三十五章 基于OV7725的PL以太网视频传输实验-摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0_第12张图片

图 7.5.13.4 上位机实时显示画面
由上图可知,开发板传输的图像分辨率是640*480,帧率约为30fps,如果需要停止显示画面的话,只需要点击上方的“关闭”按钮即可。需要注意的是,上位机显示的画面受电脑性能的影响,如果电脑性能较差的话,上位机可能会出现卡顿和崩溃的现象。

你可能感兴趣的:(正点原子,fpga开发,音视频,图像处理)