【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

这篇具有很好参考价值的文章主要介绍了【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1)实验平台:正点原子MPSoC开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=692450874670
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第三十二章OV5640摄像头RGB-LCD显示实验

OV5640是OmniVision(豪威科技)公司生产的CMOS图像传感器,该传感器分辨率高、采集速率快,图像处理性能强,主要应用在手机、数码相机、电脑多媒体等领域。本章将使用FPGA开发板实现对OV5640的数字图像采集并通过LCD实时显示。
本章包括以下几个部分:
3232.1简介
32.2实验任务
32.3硬件设计
32.4程序设计
32.5下载验证
32.1简介
OV5640是一款1/4英寸单芯片图像传感器,其感光阵列达到25921944(即500W像素),能实现最快15fps QSXVGA(25921944)或者90fps VGA(640*480)分辨率的图像采集。传感器采用OmniVision推出的OmniBSI(背面照度)技术,使传感器达到更高的性能,如高灵敏度、低串扰和低噪声。传感器内部集成了图像处理的功能,包括自动曝光控制(AEC)、自动白平衡(AWB)等。同时该传感器支持LED补光、MIPI(移动产业处理器接口)输出接口和DVP(数字视频并行)输出接口选择、ISP(图像信号处理)以及AFC(自动聚焦控制)等功能。
OV5640的功能框图如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.1 OV5640功能框图
由上图可知,时序发生器(timing generator)控制着感光阵列(image array)、放大器(AMP)、AD转换以及输出外部时序信号(VSYNC、HREF和PCLK),外部时钟XVCLK经过PLL锁相环后输出的时钟作为系统的控制时钟;感光阵列将光信号转化成模拟信号,经过增益放大器之后进入10位AD转换器;AD转换器将模拟信号转化成数字信号,并且经过ISP进行相关图像处理,最终输出所配置格式的10位视频数据流。增益放大器控制以及ISP等都可以通过寄存器(registers)来配置,配置寄存器的接口就是SCCB接口,该接口协议兼容IIC协议。
SCCB(Serial Camera Control Bus,串行摄像头控制总线)是由OV(OmniVision的简称)公司定义和发展的三线式串行总线,该总线控制着摄像头大部分的功能,包括图像数据格式、分辨率以及图像处理参数等。OV公司为了减少传感器引脚的封装,现在SCCB总线大多采用两线式接口总线。
OV5640使用的是两线式接口总线,该接口总线包括SIO_C串行时钟输入线和SIO_D串行双向数据线,分别相当于IIC协议的SCL信号线和SDA信号线。我们在前面提到过SCCB协议兼容IIC协议,是因为SCCB协议和IIC协议非常相似,有关IIC协议的详细介绍请大家参考“EEPROM读写实验”章节。
SCCB的写传输协议如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.2 SCCB写传输协议
上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写 1:读);Sub-address为8位寄存器地址,一般有些寄存器是可改写的,有些是只读的,只有可改写的寄存器才能正确写入;Write Data为8位写数据,每一个寄存器地址对应8位的配置数据。上图中的第9位X表示Don’t Care(不必关心位),该位是由从机(此处指摄像头)发出应答信号来响应主机表示当前ID Address、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机(此处指FPGA)可不用判断此处是否有应答,直接默认当前传输完成即可。
我们可以发现,SCCB和IIC写传输协议是极为相似的,只是在SCCB写传输协议中,第9位为不必关心位,而IIC写传输协议为应答位。SCCB的读传输协议和IIC有些差异,在IIC读传输协议中,写完寄存器地址后会有restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,发起总线停止信号,下图为SCCB的读传输协议。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.3 SCCB读传输协议
由上图可知,SCCB读传输协议分为两个部分。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针指向虚写操作中寄存器地址的位置,当然虚写操作也可以通过前面介绍的写传输协议来完成。第二部分是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据。上图中的NA位由主机(这里指FPGA)产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
需要注意的是,对于OV5640摄像头来说,由于其可配置的寄存器非常多,所以OV5640摄像头的寄存器地址位数是16位(两个字节),OV5640 SCCB的写传输协议如下图所示:
图 32.1.4 OV5640 SCCB写传输协议
上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写 1:读),OV5640的器件地址为7’h3c,所以在写传输协议中,ID Address(W)= 8’h78(器件地址左移1位,低位补0);Sub-address(H)为高8位寄存器地址,Sub-address(L)为低8位寄存器地址,在OV5640众多寄存器中,有些寄存器是可改写的,有些是只读的,只有可改写的寄存器才能正确写入;Write Data为8位写数据,每一个寄存器地址对应8位的配置数据。
在OV5640正常工作之前,必须先对传感器进行初始化,即通过配置寄存器使其工作在预期的工作模式,以及得到较好画质的图像。因为SCCB的写传输协议和IIC几乎相同,因此我们可以直接使用IIC的驱动程序来配置摄像头。当然这么多寄存器也并非都需要配置,很多寄存器可以采用默认的值。OV公司提供了OV5640的软件应用手册(OV5640 Software Application Note,位于开发板所随附的资料“7_硬件资料/4_OV5640资料/OV5640_camera_module_software_application_notes.pdf”),如果某些寄存器不知道如何配置可以参考此手册,下表是本程序用到的关键寄存器的配置说明。
表 32.1.1 OV5640关键寄存器配置说明
地址
(HEX) 寄存器 默认值
(HEX) 详细说明
0x3008 SYSTEM CTROL0 0x02 Bit[7]:软件复位
Bit[6]:软件电源休眠
0x3016 PAD OUTPUT ENABLE 00 0x00 Bit[1]:闪光灯输出使能
0x3017 PAD OUTPUT ENABLE 01 0x00 输入/输出控制(0:输入 1:输出)
Bit[7]:FREX输出使能
Bit[6]:VSYNC输出使能
Bit[5]:HREF输出使能
Bit[4]:PCLK输出使能
Bit[3:0]:D[9:6]输出使能
0x3018 PAD OUTPUT ENABLE 02 0x00 输入/输出控制(0:输入 1:输出)
Bit[7:2]:D[5:0]输出使能
Bit[1]:GPIO1输出使能
Bit[0]:GPIO0输出使能
0x3019 PAD OUTPUT VALUE 00 0x00 Bit[1]: 闪光灯操作
0:关闭闪光灯
1:打开闪光灯
0x301C PAD SELECT 00 0x00 Bit[1]:闪光灯IO选择
0x3035 SC PLL CONTRL1 0x11 Bit[7:4]:系统时钟分频,用于降低所有的时钟频率
Bit[3:0]:MIPI分频
0x3036 SC PLL CONTRL2 0x69 Bit[7:0]:PLL倍频器(4~252)
在4~127范围内支持任意整数分频
在128~252范围内仅支持偶数分频
0x3808 TIMING DVPHO 0x0A Bit[3:0]:DVP 输出水平像素点数高4位
0x3809 TIMING DVPHO 0x20 Bit[7:0]:DVP 输出水平像素点数低8位
0x380A TIMING DVPVO 0x07 Bit[2:0]:DVP输出垂直像素点数高3位
0x380B TIMING DVPVO 0x98 Bit[7:0]:输出垂直像素点数低8位
0x4300 FORMAT CONTROL 0xF8 Bit[7:4]:数据输出格式
0:RAW
1:Y8
2:YUV444/RGB888
3:YUV422
4:YUV420
5:YUV420(仅在MIPI输出接口有效)
6:RGB565
Bit[3:0]:输出顺序
0:{b[4:0],g[5:3]},{g[2:0],r[4:0]}
1:{r[4:0],g[5:3]},{g[2:0],b[4:0]}
2:{g[4:0],r[5:3]},{r[2:0],b[4:0]}
3:{b[4:0],r[5:3]},{r[2:0],g[4:0]}
4:{g[4:0],b[5:3]},{b[2:0],r[4:0]}
5:{r[4:0],b[5:3]},{b[2:0],g[4:0]}
6~14:不允许
15:{g[2:0],b[4:0]},{r[4:0],g[5:3]}
7:RGB555格式1
8:RGB555格式2
9:RGB444格式1
10:RGB444格式2
11~14:不允许
15:Bypass formatter module
OV5640的寄存器较多,对于其它寄存器的描述可以参考OV5640的数据手册。需要注意的是,OV5640的数据手册并没有提供全部的寄存器描述,而大多数必要的寄存器配置在ov5640的软件应用手册中可以找到,可以结合这两个手册学习如何对OV5640进行配置。
输出图像参数设置
接下来,我们介绍一下OV5640的ISP输入窗口设置、预缩放窗口设置和输出大小窗口设置,这几个设置与我们的正常使用密切相关,有必要了解一下,它们的设置关系如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.5 图像窗口设置
ISP输入窗口设置(ISP Input Size)允许用户设置整个传感器显示区域(physical pixel size,26321951,其中25921944像素是有效的),开窗范围从00~26321951都可以任意设置。也就是上图中的X_ADDR_ST(寄存器地址0x3800、0x3801)、Y_ADDR_ST(寄存器地址0x3802、0x3803)、X_ADDR_END(寄存器地址0x3804、0x3805)和Y_ADDR_END(寄存器地址0x3806、0x3807)寄存器。该窗口设置范围中的像素数据将进入ISP进行图像处理。
预缩放窗口设置(pre-scaling size)允许用户在ISP输入窗口的基础上进行裁剪,用于设置将进行缩放的窗口大小,该设置仅在ISP输入窗口内进行X/Y方向的偏移。可以通过X_OFFSET(寄存器地址0x3810、0x3811)和Y_OFFSET(寄存器地址0x3812、0x3813)进行配置。
输出大小窗口设置(data output size)是在预缩放窗口的基础上,经过内部DSP进行缩放处理,并将处理后的数据输出给外部的图像窗口,图像窗口控制着最终的图像输出尺寸。可以通过X_OUTPUT_SIZE(寄存器地址0x3808、0x3809)和Y_OUTPUT_SIZE(寄存器地址0x380A、0x380B)进行配置。注意:当输出大小窗口与预缩放窗口比例不一致时,图像将进行缩放处理(图像变形),仅当两者比例一致时,输出比例才是1:1(正常图像)。
图 32.1.5中,右侧data output size区域,才是OV5640输出给外部的图像尺寸,也就是显示在显示器或者液晶屏上面的图像大小。输出大小窗口与预缩放窗口比例不一致时,会进行缩放处理,在显示器上面看到的图像将会变形。
输出像素格式
OV5640支持多种不同的数据像素格式,包括YUV(亮度参量和色度参量分开表示的像素格式)、RGB(其中RGB格式包含RGB565、RGB555等)以及RAW(原始图像数据),通过寄存器地址0x4300配置成不同的数据像素格式。
由于数据像素格式常用RGB565,我们这里也将ov5640配置为RGB565格式。由上表(表 32.1.1)可知,将寄存器0x4300寄存器的Bit[7:4]设置成0x6即可。OV5640支持调节RGB565输出格式中各颜色变量的顺序,对于我们常见的应用来说,一般是使用RGB或BGR序列。其中RGB序列最为常用,因此将寄存器0x4300寄存器的Bit[3:0]设置成0x1。
由于摄像头采集的图像最终要通过RGB LCD接口显示在LCD液晶屏上,且DFZU2EG/4EV MPSoC开发板上的LCD接口为RGB888格式(详情请参考“LCD彩条显示实验”章节),因此我们将OV5640摄像头输出的图像像素数据配置成RGB565格式,然后通过颜色分量低位补零的方式将RGB565格式转换为RGB888格式。下图为摄像头输出的时序图。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.6 OV5640输出时序图
在介绍时序图之前先了解几个基本的概念。
VSYNC:场同步信号,由摄像头输出,用于标志一帧数据的开始与结束。上图中VSYNC的高电平作为一帧的同步信号,在低电平时输出的数据有效。需要注意的是场同步信号是可以通过设置寄存器0x4740 Bit[0]位进行取反的,即低电平同步高电平有效,本次实验使用的是和上图一致的默认设置;
HREF/HSYNC:行同步信号,由摄像头输出,用于标志一行数据的开始与结束。上图中的HREF和HSYNC是由同一引脚输出的,只是数据的同步方式不一样。本次实验使用的是HREF格式输出,当HREF为高电平时,图像输出有效,可以通过寄存器0x4740 Bit[1]进行配置。本次实验使用的是HREF格式输出;
D[9:0]:数据信号,由摄像头输出,在RGB格式输出中,只有高8位D[9:2]是有效的;
tPCLK:一个像素时钟周期;
下图为OV5640输出RGB565格式的时序图:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.1.7 RGB565模式时序图
上图中的PCLK为OV5640输出的像素时钟,HREF为行同步信号,D[9:2]为8位像素数据。OV5640最大可以输出10位数据,在RGB565输出模式中,只有高8位是有效的。像素数据在HREF为高电平时有效,第一次输出的数据为RGB565数据的高8位,第二次输出的数据为RGB565数据的低8位,first byte和second byte组成一个16位RGB565数据。由上图可知,数据是在像素时钟的下降沿改变的,为了在数据最稳定的时刻采集图像数据,所以我们需要在像素时钟的上升沿采集数据。
32.2实验任务
本节实验任务是使用DFZU2EG/4EV MPSoC开发板及OV5640摄像头实现图像采集,并通过RGB-LCD接口驱动RGB-LCD液晶屏(支持目前正点原子推出的所有RGB-LCD屏),并实时显示出图像。
32.3硬件设计
我们的DFZU2EG/4EV MPSOC开发板上有一个扩展接口(J19),该接口可以用来连接一些扩展模块,如双目OV5640摄像头、高速ADDA模块、IO扩展板模块等。本次实验就是通过连接双目OV5640摄像头,实现单个OV5640摄像头图像的采集和显示。原理图如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.3.1扩展接口原理图
ATK-Dual-OV5640是正点原子推出的一款高性能双目OV5640 500W像素高清摄像头模块。该模块通过2*20排母(2.54mm间距)同外部连接,我们将摄像头的排母直接插在开发板上的扩展接口即可,模块外观如图 32.3.2所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.3.2 ATK-OV5640摄像头模块实物图
我们在前面说过,OV5640在RGB565模式中只有高8位数据是有效的即D[9:2],而我们的摄像头排母上数据引脚的个数是8位。实际上,摄像头排母上的8位数据连接的就是OV5640传感器的D[9:2],所以我们直接使用摄像头排母上的8位数据引脚即可。
由于RGB LCD屏的引脚数目较多,且在前面相应的章节中已经给出它们的管脚列表,这里只列出摄像头相关管脚分配, 如下表所示:
表 32.3.1 OV5640摄像头管脚分配
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets cam_pclk_IBUF_inst/O]

set_property -dict {PACKAGE_PIN C13 IOSTANDARD LVCMOS33} [get_ports cam_pclk]
set_property -dict {PACKAGE_PIN F13 IOSTANDARD LVCMOS33} [get_ports cam_rst_n]
set_property -dict {PACKAGE_PIN B15 IOSTANDARD LVCMOS33} [get_ports cam_pwdn]
set_property -dict {PACKAGE_PIN E15 IOSTANDARD LVCMOS33} [get_ports {cam_data[0]}]
set_property -dict {PACKAGE_PIN D15 IOSTANDARD LVCMOS33} [get_ports {cam_data[1]}]
set_property -dict {PACKAGE_PIN E14 IOSTANDARD LVCMOS33} [get_ports {cam_data[2]}]
set_property -dict {PACKAGE_PIN D14 IOSTANDARD LVCMOS33} [get_ports {cam_data[3]}]
set_property -dict {PACKAGE_PIN E13 IOSTANDARD LVCMOS33} [get_ports {cam_data[4]}]
set_property -dict {PACKAGE_PIN B13 IOSTANDARD LVCMOS33} [get_ports {cam_data[5]}]
set_property -dict {PACKAGE_PIN C14 IOSTANDARD LVCMOS33} [get_ports {cam_data[6]}]
set_property -dict {PACKAGE_PIN A13 IOSTANDARD LVCMOS33} [get_ports {cam_data[7]}]
set_property -dict {PACKAGE_PIN G14 IOSTANDARD LVCMOS33} [get_ports cam_vsync]
set_property -dict {PACKAGE_PIN G13 IOSTANDARD LVCMOS33} [get_ports cam_href]
set_property -dict {PACKAGE_PIN H13 IOSTANDARD LVCMOS33} [get_ports cam_scl]
set_property -dict {PACKAGE_PIN F15 IOSTANDARD LVCMOS33} [get_ports cam_sda]
set_property PULLUP true [get_ports cam_scl]
set_property PULLUP true [get_ports cam_sda]]

32.4程序设计
图 32.4.1是根据本章实验任务画出的系统框图。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.1 顶层系统框图
由上图可知,本节实验的系统输入时钟是一对差分时钟(100Mhz),系统时钟进入DDR4顶层模块内部的MIG IP核后,由MIG IP内部的MMCM输出一路50Mhz的时钟给OV5640顶层模块、LCD顶层模块以及图像分辨率模块使用。其中LCD顶层模块会读取外部LCD显示屏的ID,并将这个ID传递给图像分辨率模块,而图像分辨率模块会根据不同的显示屏ID确定显示图像的不同分辨率,并生成对应的摄像头配置参数和DDR4的最大读写地址参数,其中摄像头配置参数传递给OV5640顶层模块,DDR4的最大读写地址参数传递给MIG IP核驱动模块。之后OV5640顶层模块会根据图像分辨率模块传递过来的参数去配置OV5640摄像头,等待摄像头配置完成后会接收摄像头采集到的数据,并将这个数据传递给MIG IP核模块。MIG IP模块会根据图像分辨率模块传递来的参数去配置DDR4最大读写地址,并将OV5640顶层模块传递过来的数据写入到外部DDR4芯片中去。最后LCD顶层模块再通过MIG IP核把外部DDR4芯片中存储的数据读出来传递给LCD显示屏,从而让LCD显示屏可以正常的显示摄像头捕捉的图像数据。
顶层模块的原理图如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.2 顶层模块原理图
FPGA顶层模块(ov5640_lcd)例化了以下四个模块: OV5640驱动模块(ov5640_dri)、摄像头图像分辨率设置模块(picture_size)、DDR控制模块(ddr4_top)和LCD顶层模块(lcd_rgb_top)。
OV5640驱动模块(ov5640_dri):OV5640驱动模块负责驱动OV5640 SCCB接口总线,将像素时钟驱动下的传感器输出的场同步信号、行同步信号以及8位数据转换成DDR读写控制模块的写使能信号和16位写数据信号,完成对OV5640传感器图像的采集。
图像分辨率设置模块(picture_size):图像尺寸配置模块用于配置摄像头输出图像尺寸的大小,此外还完成了DDR4的读写结束地址设置。
DDR控制模块(ddr4_top):DDR读写控制器模块负责驱动DDR片外存储器,缓存图像传感器输出的图像数据。该模块将MIG IP核复杂的读写操作封装成类似FIFO的用户接口,非常方便用户的使用。
LCD顶层模块(lcd_rgb_top):LCD顶层模块负责驱动LCD屏的驱动信号的输出,同时为其他模块提供屏体参数、场同步信号和数据请求信号。
接下来我们来详细分析一下这几个模块的代码,其中顶层代码仅仅是例化四个子模块,实现各模块的数据交互,此处不再贴出代码。
我们先来看看摄像头驱动代码,如下所示:

1   module ov5640_dri (
2       input           clk             ,  //时钟
3       input           rst_n           ,  //复位信号,低电平有效
4       //摄像头接口 
5       input           cam_pclk        ,  //cmos 数据像素时钟
6       input           cam_vsync       ,  //cmos 场同步信号
7       input           cam_href        ,  //cmos 行同步信号
8       input    [7:0]  cam_data        ,  //cmos 数据  
9       output          cam_rst_n       ,  //cmos 复位信号,低电平有效
10      output          cam_pwdn        ,  //cmos 电源休眠模式选择信号
11      output          cam_scl         ,  //cmos SCCB_SCL线
12      input           cam_sda_i       ,  //cmos SCCB_SDA输入
13      output          cam_sda_o       ,  //cmos SCCB_SDA输出 
14      output          cam_sda_t       ,  //cmos SCCB_SDA使能        
15      
16      //摄像头分辨率配置接口
17      input    [12:0] cmos_h_pixel    ,  //水平方向分辨率
18      input    [12:0] cmos_v_pixel    ,  //垂直方向分辨率
19      input    [12:0] total_h_pixel   ,  //水平总像素大小
20      input    [12:0] total_v_pixel   ,  //垂直总像素大小
21      input           capture_start   ,  //图像采集开始信号
22      output          cam_init_done   ,  //摄像头初始化完成
23      
24      //用户接口
25      output          cmos_frame_vsync,  //帧有效信号    
26      output          cmos_frame_href ,  //行有效信号
27      output          cmos_frame_valid,  //数据有效使能信号
28      output  [15:0]  cmos_frame_data    //有效数据  
29  );
30  
31  //parameter define
32  parameter SLAVE_ADDR = 7'h3c   ; //OV5640的器件地址7'h3c
33  parameter BIT_CTRL   = 1'b1    ; //OV5640的字节地址为16位  0:8位 1:16位
34  parameter CLK_FREQ   = 27'd50_000_000; //i2c_dri模块的驱动时钟频率 
35  parameter I2C_FREQ   = 18'd250_000    ; //I2C的SCL时钟频率,不超过400KHz
36  
37  //wire difine
38  wire        i2c_exec       ;  //I2C触发执行信号
39  wire [23:0] i2c_data       ;  //I2C要配置的地址与数据(高8位地址,低8位数据)          
40  wire        i2c_done       ;  //I2C寄存器配置完成信号
41  wire        i2c_dri_clk    ;  //I2C操作时钟
42  wire [ 7:0] i2c_data_r     ;  //I2C读出的数据
43  wire        i2c_rh_wl      ;  //I2C读写控制信号
44  
45  //*****************************************************
46  //**                    main code                      
47  //*****************************************************
48  
49  //电源休眠模式选择 0:正常模式 1:电源休眠模式
50  assign  cam_pwdn  = 1'b0;
51  assign  cam_rst_n = 1'b1;
52      
53  //I2C配置模块
54  i2c_ov5640_rgb565_cfg u_i2c_cfg(
55      .clk                (i2c_dri_clk),
56      .rst_n              (rst_n),
57              
58      .i2c_exec           (i2c_exec),
59      .i2c_data           (i2c_data),
60      .i2c_rh_wl          (i2c_rh_wl),        //I2C读写控制信号
61      .i2c_done           (i2c_done), 
62      .i2c_data_r         (i2c_data_r),   
63                  
64      .cmos_h_pixel       (cmos_h_pixel),     //CMOS水平方向像素个数
65      .cmos_v_pixel       (cmos_v_pixel) ,    //CMOS垂直方向像素个数
66      .total_h_pixel      (total_h_pixel),    //水平总像素大小
67      .total_v_pixel      (total_v_pixel),    //垂直总像素大小
68          
69      .init_done          (cam_init_done) 
70      );    
71  
72  //I2C驱动模块
73  i2c_dri #(
74      .SLAVE_ADDR         (SLAVE_ADDR),       //参数传递
75      .CLK_FREQ           (CLK_FREQ  ),              
76      .I2C_FREQ           (I2C_FREQ  ) 
77      )
78  u_i2c_dr(
79      .clk                (clk),
80      .rst_n              (rst_n     ),
81  
82      .i2c_exec           (i2c_exec  ),   
83      .bit_ctrl           (BIT_CTRL  ),   
84      .i2c_rh_wl          (i2c_rh_wl),        //固定为0,只用到了IIC驱动的写操作   
85      .i2c_addr           (i2c_data[23:8]),   
86      .i2c_data_w         (i2c_data[7:0]),   
87      .i2c_data_r         (i2c_data_r),   
88      .i2c_done           (i2c_done  ),
89      
90      .scl                (cam_scl     ),   
91      .sda_i              (cam_sda_i   ),  
92      .sda_o              (cam_sda_o   ),   
93      .sda_t              (cam_sda_t   ),        
94      .dri_clk            (i2c_dri_clk)       //I2C操作时钟
95      );
96  
97  //CMOS图像数据采集模块
98  cmos_capture_data u_cmos_capture_data(      //系统初始化完成之后再开始采集数据 
99      .rst_n              (rst_n & capture_start),
100     
101     .cam_pclk           (cam_pclk),
102     .cam_vsync          (cam_vsync),
103     .cam_href           (cam_href),
104     .cam_data           (cam_data),         
105     
106     .cmos_frame_vsync   (cmos_frame_vsync),
107     .cmos_frame_href    (cmos_frame_href ),
108     .cmos_frame_valid   (cmos_frame_valid), //数据有效使能信号
109     .cmos_frame_data    (cmos_frame_data )  //有效数据 
110     );
111 
112 endmodule

摄像头驱动模块的顶层同样例化了三个模块,分别是摄像头配置模块(i2c_ov5640_rgb565_cfg)、IIC驱动模块(i2c_dri)以及数据采集模块(cmos_capture_data)。其中摄像头配置模块主要是包含了OV5640摄像头的寄存器信息,而IIC驱动模块就是把这些寄存器信息配置到OV5640摄像头中去;数据采集模块的作用就是接收摄像头采集到的数据并将数据传递给DDR4驱动模块。
下面我们先来看看摄像头配置模块(i2c_ov5640_rgb565_cfg)的代码,如下所示:

1   module i2c_ov5640_rgb565_cfg
2      (  
3       input                clk      ,     //时钟信号
4       input                rst_n    ,     //复位信号,低电平有效
5       
6       input        [7:0]   i2c_data_r,    //I2C读出的数据
7       input                i2c_done ,     //I2C寄存器配置完成信号
8       input        [12:0]  cmos_h_pixel ,
9       input        [12:0]  cmos_v_pixel ,
10      input        [12:0]  total_h_pixel, //水平总像素大小
11      input        [12:0]  total_v_pixel, //垂直总像素大小
12      output  reg          i2c_exec ,     //I2C触发执行信号   
13      output  reg  [23:0]  i2c_data ,     //I2C要配置的地址与数据(高16位地址,低8位数据)
14      output  reg          i2c_rh_wl,     //I2C读写控制信号
15      output  reg          init_done      //初始化完成信号
16      );
17  
18  //parameter define
19  localparam  REG_NUM = 8'd250  ;       //总共需要配置的寄存器个数
20  
21  //reg define
22  reg   [12:0]   start_init_cnt;        //等待延时计数器
23  reg    [7:0]   init_reg_cnt  ;        //寄存器配置个数计数器
24  
25  //*****************************************************
26  //**                    main code
27  //*****************************************************
28  
29  //clk时钟配置成1Mhz,周期为1000ns 20000*1000ns = 20ms
30  //OV5640上电到开始配置IIC至少等待20ms
31  always @(posedge clk or negedge rst_n) begin
32      if(!rst_n)
33          start_init_cnt <= 13'b0;
34      else if(start_init_cnt < 13'd20000) begin
35          start_init_cnt <= start_init_cnt + 1'b1;                    
36      end
37  end
38  
39  //寄存器配置个数计数    
40  always @(posedge clk or negedge rst_n) begin
41      if(!rst_n)
42          init_reg_cnt <= 8'd0;
43      else if(i2c_exec)   
44          init_reg_cnt <= init_reg_cnt + 8'b1;
45  end
46  
47  //i2c触发执行信号   
48  always @(posedge clk or negedge rst_n) begin
49      if(!rst_n)
50          i2c_exec <= 1'b0;
51      else if(start_init_cnt == 13'd19999)
52          i2c_exec <= 1'b1;
53      else if(i2c_done && (init_reg_cnt < REG_NUM))
54          i2c_exec <= 1'b1;
55      else
56          i2c_exec <= 1'b0;
57  end 
58  
59  //配置I2C读写控制信号
60  always @(posedge clk or negedge rst_n) begin
61      if(!rst_n)
62          i2c_rh_wl <= 1'b1;
63      else if(init_reg_cnt == 8'd2)  
64          i2c_rh_wl <= 1'b0;  
65  end
66  
67  //初始化完成信号
68  always @(posedge clk or negedge rst_n) begin
69      if(!rst_n)
70          init_done <= 1'b0;
71      else if((init_reg_cnt == REG_NUM) && i2c_done)  
72          init_done <= 1'b1;  
73  end
74  
75  //配置寄存器地址与数据
76  always @(posedge clk or negedge rst_n) begin
77      if(!rst_n)
78          i2c_data <= 24'b0;
79      else begin
80          case(init_reg_cnt)
81              //先对寄存器进行软件复位,使寄存器恢复初始值
82              //寄存器软件复位后,需要延时1ms才能配置其它寄存器
83              8'd0  : i2c_data <= {16'h300a,8'h0}; //
84              8'd1  : i2c_data <= {16'h300b,8'h0}; //
配置代码较长,省略部分源代码……

295             8'd204: i2c_data <= {16'h5025,8'h00};
296             //系统时钟分频 Bit[7:4]:系统时钟分频 input clock =24Mhz, PCLK = 48Mhz
297             8'd205: i2c_data <= {16'h3035,8'h11}; 
298             8'd206: i2c_data <= {16'h3036,8'h3c}; //PLL倍频
299             8'd207: i2c_data <= {16'h3c07,8'h08};
300             //时序控制 16'h3800~16'h3821
301             8'd208: i2c_data <= {16'h3820,8'h46};
302             8'd209: i2c_data <= {16'h3821,8'h01};
303             8'd210: i2c_data <= {16'h3814,8'h31};
304             8'd211: i2c_data <= {16'h3815,8'h31};
305             8'd212: i2c_data <= {16'h3800,8'h00};
306             8'd213: i2c_data <= {16'h3801,8'h00};
307             8'd214: i2c_data <= {16'h3802,8'h00};
308             8'd215: i2c_data <= {16'h3803,8'h04};
309             8'd216: i2c_data <= {16'h3804,8'h0a};
310             8'd217: i2c_data <= {16'h3805,8'h3f};
311             8'd218: i2c_data <= {16'h3806,8'h07};
312             8'd219: i2c_data <= {16'h3807,8'h9b};
313             //设置输出像素个数
314             //DVP 输出水平像素点数高4位
315             8'd220: i2c_data <= {16'h3808,{4'd0,cmos_h_pixel[11:8]}};
316             //DVP 输出水平像素点数低8位
317             8'd221: i2c_data <= {16'h3809,cmos_h_pixel[7:0]};
318             //DVP 输出垂直像素点数高3位
319             8'd222: i2c_data <= {16'h380a,{5'd0,cmos_v_pixel[10:8]}};
320             //DVP 输出垂直像素点数低8位
321             8'd223: i2c_data <= {16'h380b,cmos_v_pixel[7:0]};
322             //水平总像素大小高5位
323             8'd224: i2c_data <= {16'h380c,{3'd0,total_h_pixel[12:8]}};
324             //水平总像素大小低8位 
325             8'd225: i2c_data <= {16'h380d,total_h_pixel[7:0]};
326             //垂直总像素大小高5位 
327             8'd226: i2c_data <= {16'h380e,{3'd0,total_v_pixel[12:8]}};
328             //垂直总像素大小低8位     
329             8'd227: i2c_data <= {16'h380f,total_v_pixel[7:0]};
配置代码较长,省略部分源代码……

350             8'd246: i2c_data <= {16'h3016,8'h02};
351             8'd247: i2c_data <= {16'h301c,8'h02};
352             8'd248: i2c_data <= {16'h3019,8'h02}; //打开闪光灯
353             8'd249: i2c_data <= {16'h3019,8'h00}; //关闭闪光灯
354             //只读存储器,防止在case中没有列举的情况,之前的寄存器被重复改写
355             default : i2c_data <= {16'h300a,8'h00}; //器件ID高8位
356         endcase
357     end
358 end
359 
360 endmodule

I2C配置模块寄存需要配置的寄存器地址、数据以及控制初始化的开始与结束。需要注意的是,由OV5640的数据手册可知,图像传感器上电后到开始配置寄存器需要延时20ms,所以程序中定义了一个延时计数器(start_init_cnt),用于延时20ms。当计数器计数到预设值之后,开始第一次配置传感器即软件复位,目的是让所有的寄存器复位到默认的状态。在代码的第19行定义了总共需要配置的寄存器的个数,如果增加或者删减了寄存器的配置,需要修改此参数。代码第4045行是用来计数已经配置了多少个寄存器。代码第4857行是用来生成IIC启动信号(i2c_exec)。代码第68~73行是当配置寄存器计数器(init_reg_cnt)数到了需要配置寄存器个数的最大值时(REG_NUM)认为寄存器配置完成。再往后面的代码就是寄存器具体的地址和需要写入的值了,这部分内容需要参考数据手册,几个比较重要的寄存器在简介部分也给大家着重讲解过了,这里不再重复赘述。
在程序的第313行至第329行,是对摄像头需要输出的行场分辨率和行场总像素进行设置的寄存器配置,根据不同的LCD屏幕的ID,这几个寄存器配置的参数也是不相同的。
关于IIC驱动模块的代码在前面“IIC读写测试实验”的例程中有详细的讲解,所以本节实验不再讲解了。最后我们来看看数据采集模块的代码,如下所示:

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          
97          else;   
98      end
99      else begin
100         byte_flag <= 1'b0;
101         cam_data_d0 <= 8'b0;
102         cmos_data_t <= 16'd0;
103     end    
104 end        
105 
106 //产生输出数据有效信号(cmos_frame_valid)
107 always @(posedge cam_pclk or negedge rst_n) begin
108     if(!rst_n)
109         byte_flag_d0 <= 1'b0;
110     else
111         byte_flag_d0 <= byte_flag;  
112 end 
113     
114 endmodule

摄像头在配置完寄存器后就会输出图像数据,但是此时输出的数据有可能不是特别稳定,所以我们的数据采集模块并没有立刻把摄像头输出的数据接收过来,而是在代码的6782行做了一个帧计数器,当数到摄像头已经传输完10帧数据后(此时我们认为摄像头数据已经处于稳定状态)才开始接收数据并且拉高帧有效标志(frame_val_flag)。代码第85104行是将8位的摄像头原始数据拼接成16位rgb565格式的像素数据。代码第106~114行是每当一个数据拼接完成就输出一个数据有效信号。
接下来我们来看看DDR控制模块,代码如下所示:

1   module ddr4_top(
2       input              sys_rst_n       ,   //复位,低有效
3       input              sys_init_done   ,   //系统初始化完成               
4       //DDR4接口信号                       
5       input   [27:0]     app_addr_rd_min ,   //读DDR4的起始地址
6       input   [27:0]     app_addr_rd_max ,   //读DDR4的结束地址
7       input   [7:0]      rd_bust_len     ,   //从DDR4中读数据时的突发长度
8       input   [27:0]     app_addr_wr_min ,   //读DDR4的起始地址
9       input   [27:0]     app_addr_wr_max ,   //读DDR4的结束地址
10      input   [7:0]      wr_bust_len     ,   //从DDR4中读数据时的突发长度
11      // DDR4 IO接口    
12      input              c0_sys_clk_p    ,
13      input              c0_sys_clk_n    ,
14      output             c0_ddr4_act_n   ,
15      output [16:0]      c0_ddr4_adr     ,
16      output [1:0]       c0_ddr4_ba      ,
17      output [0:0]       c0_ddr4_bg      ,
18      output [0:0]       c0_ddr4_cke     ,
19      output [0:0]       c0_ddr4_odt     ,
20      output [0:0]       c0_ddr4_cs_n    ,
21      output [0:0]       c0_ddr4_ck_t    ,
22      output [0:0]       c0_ddr4_ck_c    ,
23      output             c0_ddr4_reset_n ,
24      inout  [1:0]       c0_ddr4_dm_dbi_n,
25      inout  [15:0]      c0_ddr4_dq      ,
26      inout  [1:0]       c0_ddr4_dqs_c   ,
27      inout  [1:0]       c0_ddr4_dqs_t   , 
28  
29      //用户
30      input              ddr4_read_valid     ,   //DDR4 读使能   
31      input              ddr4_pingpang_en    ,   //DDR4 乒乓操作使能       
32      input              wr_clk              ,   //wfifo时钟   
33      input              rd_clk              ,   //rfifo的读时钟      
34      input              datain_valid        ,   //数据有效使能信号
35      input   [15:0]     datain              ,   //有效数据 
36      input              rdata_req           ,   //请求像素点颜色数据输入  
37      input              rd_load             ,   //输出源更新信号
38      input              wr_load             ,   //输入源更新信号
39      output  [15:0]     dataout             ,   //rfifo输出数据
40      output             clk_50m             ,
41      output             init_calib_complete     //ddr4初始化完成信号
42      );                
43                      
44  //wire define  
45  wire                  ui_clk               ;   //用户时钟
46  wire [27:0]           app_addr             ;   //ddr4 地址
47  wire [2:0]            app_cmd              ;   //用户读写命令
48  wire                  app_en               ;   //MIG IP核使能
49  wire                  app_rdy              ;   //MIG IP核空闲
50  wire [127:0]          app_rd_data          ;   //用户读数据
51  wire                  app_rd_data_end      ;   //突发读当前时钟最后一个数据 
52  wire                  app_rd_data_valid    ;   //读数据有效
53  wire [127:0]          app_wdf_data         ;   //用户写数据 
54  wire                  app_wdf_end          ;   //突发写当前时钟最后一个数据 
55  wire [15:0]           app_wdf_mask         ;   //写数据屏蔽                           
56  wire                  app_wdf_rdy          ;   //写空闲                               
57  wire                  app_sr_active        ;   //保留                                 
58  wire                  app_ref_ack          ;   //刷新请求                             
59  wire                  app_zq_ack           ;   //ZQ 校准请求                          
60  wire                  app_wdf_wren         ;   //ddr4 写使能                          
61  wire                  clk_ref_i            ;   //ddr4参考时钟                         
62  wire                  sys_clk_i            ;   //MIG IP核输入时钟                     
63  wire                  ui_clk_sync_rst      ;   //用户复位信号                         
64  wire [20:0]           rd_cnt               ;   //实际读地址计数                       
65  wire [3 :0]           state_cnt            ;   //状态计数器                           
66  wire [23:0]           rd_addr_cnt          ;   //用户读地址计数器                     
67  wire [23:0]           wr_addr_cnt          ;   //用户写地址计数器                     
68  wire                  rfifo_wren           ;   //从ddr4读出数据的有效使能             
69  wire [10:0]           wfifo_rcount         ;   //rfifo剩余数据计数                    
70  wire [10:0]           rfifo_wcount         ;   //wfifo写进数据计数  
71  
72                                                                                                                                                                      
73  //*****************************************************                               
74  //**                    main code                                                     
75  //*****************************************************                               
76                                                                                      
77  //读写模块                                                                            
78  ddr4_rw u_ddr4_rw(                                                                   
79      .ui_clk               (ui_clk)              ,                                     
80      .ui_clk_sync_rst      (ui_clk_sync_rst)     ,                                      
81      //MIG 接口                                                                        
82      .init_calib_complete  (init_calib_complete) ,   //ddr4初始化完成信号                                   
83      .app_rdy              (app_rdy)             ,   //MIG IP核空闲                                   
84      .app_wdf_rdy          (app_wdf_rdy)         ,   //写空闲                                   
85      .app_rd_data_valid    (app_rd_data_valid)   ,   //读数据有效                                   
86      .app_addr             (app_addr)            ,   //ddr4 地址                                   
87      .app_en               (app_en)              ,   //MIG IP核使能                                   
88      .app_wdf_wren         (app_wdf_wren)        ,   //ddr4 写使能                                    
89      .app_wdf_end          (app_wdf_end)         ,   //突发写当前时钟最后一个数据                                   
90      .app_cmd              (app_cmd)             ,   //用户读写命令                                                                                                                         
91      //ddr4 地址参数                                                                   
92      .app_addr_rd_min      (app_addr_rd_min)     ,   //读ddr4的起始地址                                  
93      .app_addr_rd_max      (app_addr_rd_max)     ,   //读ddr4的结束地址                                  
94      .rd_bust_len          (rd_bust_len)         ,   //从ddr4中读数据时的突发长度                                  
95      .app_addr_wr_min      (app_addr_wr_min)     ,   //写ddr4的起始地址                                  
96      .app_addr_wr_max      (app_addr_wr_max)     ,   //写ddr4的结束地址                                  
97      .wr_bust_len          (wr_bust_len)         ,   //从ddr4中写数据时的突发长度                                  
98      //用户接口                                                                        
99      .rfifo_wren           (rfifo_wren)          ,   //从ddr4读出数据的有效使能 
100     .rd_load              (rd_load)             ,   //输出源更新信号
101     .wr_load              (wr_load)             ,   //输入源更新信号
102     .ddr4_read_valid      (ddr4_read_valid)     ,   //ddr4 读使能
103     .ddr4_pingpang_en     (ddr4_pingpang_en)    ,   //ddr4 乒乓操作使能    
104     .wfifo_rcount         (wfifo_rcount)        ,   //rfifo剩余数据计数                  
105     .rfifo_wcount         (rfifo_wcount)            //wfifo写进数据计数
106     );                   
107 
108 ddr4_0 u_ddr4_0 (
109 .c0_init_calib_complete(init_calib_complete), //初始化完成        
110 .dbg_clk(),                             
111 .c0_sys_clk_p(c0_sys_clk_p),            // 系统时钟p
112 .c0_sys_clk_n(c0_sys_clk_n),            // 系统时钟n
113 .dbg_bus(),                             // output wire [511 : 0] dbg_bus
114 .c0_ddr4_adr(c0_ddr4_adr),              // 行列地址
115 .c0_ddr4_ba(c0_ddr4_ba),                // bank地址
116 .c0_ddr4_cke(c0_ddr4_cke),              // 时钟使能
117 .c0_ddr4_cs_n(c0_ddr4_cs_n),            // 片选信号
118 .c0_ddr4_dm_dbi_n(c0_ddr4_dm_dbi_n),    // 数据掩码
119 .c0_ddr4_dq(c0_ddr4_dq),                // 数据线
120 .c0_ddr4_dqs_c(c0_ddr4_dqs_c),         // 数据选通信号
121 .c0_ddr4_dqs_t(c0_ddr4_dqs_t),         // 数据选通信号
122 .c0_ddr4_odt(c0_ddr4_odt),             // 终端电阻使能
123 .c0_ddr4_bg(c0_ddr4_bg),               //bank组地址
124 .c0_ddr4_reset_n(c0_ddr4_reset_n),     // 复位信号
125 .c0_ddr4_act_n(c0_ddr4_act_n),         // 指令激活
126 .c0_ddr4_ck_c(c0_ddr4_ck_c),           // ddr时钟
127 .c0_ddr4_ck_t(c0_ddr4_ck_t),           // ddr时钟
128 //user interface
129 .c0_ddr4_ui_clk(ui_clk),               // 用户时钟    
130 .c0_ddr4_ui_clk_sync_rst(ui_clk_sync_rst), //用户复位  
131 .c0_ddr4_app_en(app_en),              // 指令使能
132 .c0_ddr4_app_hi_pri(1'b0),           
133 .c0_ddr4_app_wdf_end(app_wdf_end),    // 写数据最后一个数据
134 .c0_ddr4_app_wdf_wren(app_wdf_wren),  // 写数据使能
135 .c0_ddr4_app_rd_data_end(app_rd_data_end),   // 读数据最后一个数据
136 .c0_ddr4_app_rd_data_valid(app_rd_data_valid), // 读数据有效
137 .c0_ddr4_app_rdy(app_rdy),                     // 指令接收准备完成,可以接收指令
138 .c0_ddr4_app_wdf_rdy(app_wdf_rdy),             // 数据接收准备完成,可以接收数据
139 .c0_ddr4_app_addr(app_addr),                   // 用户地址
140 .c0_ddr4_app_cmd(app_cmd),                     // 读写指令
141 .c0_ddr4_app_wdf_data(app_wdf_data),       // 写数据
142 .c0_ddr4_app_wdf_mask(16'b0),              // 写数据掩码
143 .c0_ddr4_app_rd_data(app_rd_data),         // 读数据
144 .addn_ui_clkout1(clk_50m),                 // 锁相环时钟输出
145 .sys_rst(~sys_rst_n)                       // 系统复位
146 );
147                                                     
148 ddr4_fifo_ctrl u_ddr4_fifo_ctrl (
149 
150     .rst_n               (sys_rst_n &&sys_init_done )  ,  
151     //输入源接口
152     .wr_clk              (wr_clk)         ,
153     .rd_clk              (rd_clk)         ,
154     .clk_100             (ui_clk)         ,    //用户时钟 
155     .datain_valid        (datain_valid)   ,    //数据有效使能信号
156     .datain              (datain)         ,    //有效数据 
157     .rfifo_din           (app_rd_data)    ,    //用户读数据 
158     .rdata_req           (rdata_req)      ,    //请求像素点颜色数据输入 
159     .rfifo_wren          (rfifo_wren)     ,    //ddr4读出数据的有效使能 
160     .wfifo_rden          (app_wdf_wren)   ,    //ddr4 写使能         
161     //用户接口
162     .wfifo_rcount        (wfifo_rcount)   ,    //rfifo剩余数据计数                 
163     .rfifo_wcount        (rfifo_wcount)   ,    //wfifo写进数据计数                
164     .wfifo_dout          (app_wdf_data)   ,    //用户写数据 
165     .rd_load             (rd_load)        ,    //输出源更新信号
166     .wr_load             (wr_load)        ,    //输入源更新信号
167     .pic_data            (dataout)             //rfifo输出数据         
168     );
169 
170 endmodule

DDR4控制模块例化了三个子模块,分别是DDR4读写模块、MIG IP核以及FIFO控制模块。大家学习了前面的“DDR4读写实验例程”后,再来看我们本节实验的DDR4控制模块就比较轻松了,因为DDR4控制模块是在“DDR4读写实验例程”的基础上修改过来的。相比较“DDR4读写实验例程”,本节实验DDR4控制模块最大的区别就是添加了BANK切换和FIFO的调用。首先我们先来看一下我们为什么要添加BANK切换。
在“DDR4读写测试实验”的程序中,读写操作地址都是DDR的同一存储空间,如果只使用一个存储空间缓存图像数据,那么这么做虽然保证了数据不会出现错乱的问题,但是会导致当前读取的图像与上一次存入的图像存在交错,如下图所示:
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.3 DDR4单个BANK缓存图像机制
为了避免这一情况,我们在DDR的其它BANK中开辟一个相同大小的存储空间,使用乒乓操作的方式来写入和读取数据,所以本次实验在“DDR4读写测试实验”的程序里做了改动。
我们在DDR中开辟出2个存储空间进行乒乓操作用于缓存帧图像。在摄像头初始化结束后输出的第一个数据对应图像的第一个像素点,将其写入存储空间的首地址中。通过在DDR控制模块中对输出的图像数据进行计数,从而将它们分别写入相应的地址空间。计数达存入DDR的最大写地址后,完成一帧图像的存储,然后当帧复位到来时来切换BANK以达到乒乓操作的目的,并同时回到存储空间的首地址继续下一帧图像的存储。在显示图像时,LCD顶层模块从DDR存储空间的首地址开始读数据,同样对读过程进行计数,并将读取的图像数据分别显示到显示器相应的像素点位置。
图像数据总是在两个存储空间之间不断切换写入,而读请求信号在读完当前存储空间后判断哪个存储空间没有被写入,然后去读取没有被写入的存储空间。对于本次程序设计来说,数据写入较慢而读出较快,因此会出现同一存储空间被读取多次的情况,但保证了读出的数据一定是一帧完整的图像而不是两帧数据拼接的图像。当正在读取其中一个缓存空间,另一个缓存空间已经写完,并开始切换写入下一个缓存空间时,由于图像数据读出的速度总是大于写入的速度,因此,读出的数据仍然是一帧完整的图像。下面给出ddr4_rw模块的代码,一起来看看BANK是如何切换的。

1   module ddr4_rw(          
2       input           ui_clk               ,  //用户时钟
3       input           ui_clk_sync_rst      ,  //复位,高有效
4       input           init_calib_complete  ,  //ddr4初始化完成
5       input           app_rdy              ,  //MIG IP核空闲
6       input           app_wdf_rdy          ,  //MIG写FIFO空闲
7       input           app_rd_data_valid    ,  //读数据有效
8       input   [10:0]  wfifo_rcount         ,  //写端口FIFO中的数据量
9       input   [10:0]  rfifo_wcount         ,  //读端口FIFO中的数据量
10      input           rd_load              ,  //输出源更新信号
11      input           wr_load              ,  //输入源更新信号
12      input   [27:0]  app_addr_rd_min      ,  //读ddr4的起始地址
13      input   [27:0]  app_addr_rd_max      ,  //读ddr4的结束地址
14      input   [7:0]   rd_bust_len          ,  //从ddr4中读数据时的突发长度
15      input   [27:0]  app_addr_wr_min      ,  //写ddr4的起始地址
16      input   [27:0]  app_addr_wr_max      ,  //写ddr4的结束地址
17      input   [7:0]   wr_bust_len          ,  //从ddr4中写数据时的突发长度
18  
19      input           ddr4_read_valid      ,  //ddr4 读使能   
20      input           ddr4_pingpang_en     ,  //ddr4 乒乓操作使能          
21      output          rfifo_wren           ,  //从ddr4读出数据的有效使能 
22      output  [27:0]  app_addr             ,  //ddr4地址                 
23      output          app_en               ,  //MIG IP核操作使能
24      output          app_wdf_wren         ,  //用户写使能   
25      output          app_wdf_end          ,  //突发写当前时钟最后一个数据 
26      output  [2:0]   app_cmd                 //MIG IP核操作命令,读或者写       
27      );
28      
29  //localparam 
30  localparam IDLE        = 4'b0001;   //空闲状态
31  localparam ddr4_DONE   = 4'b0010;   //ddr4初始化完成状态
32  localparam WRITE       = 4'b0100;   //读FIFO保持状态
33  localparam READ        = 4'b1000;   //写FIFO保持状态
34  
35  //reg define
36  reg    [27:0] app_addr;             //ddr4地址 
37  reg    [27:0] app_addr_rd;          //ddr4读地址
38  reg    [27:0] app_addr_wr;          //ddr4写地址
39  reg    [3:0]  state_cnt;            //状态计数器
40  reg    [23:0] rd_addr_cnt;          //用户读地址计数
41  reg    [23:0] wr_addr_cnt;          //用户写地址计数 
42  reg    [8:0]  burst_rd_cnt;         //突发读次数计数器
43  reg    [8:0]  burst_wr_cnt;         //突发写次数计数器    
44  reg    [10:0] raddr_rst_h_cnt;      //输出源的帧复位脉冲进行计数 
45  reg    [27:0] app_addr_rd_min_a;    //读ddr4的起始地址
46  reg    [27:0] app_addr_rd_max_a;    //读ddr4的结束地址
47  reg    [7:0]  rd_bust_len_a;        //从ddr4中读数据时的突发长度
48  reg    [27:0] app_addr_wr_min_a;    //写ddr4的起始地址
49  reg    [27:0] app_addr_wr_max_a;    //写ddr4的结束地址
50  reg    [7:0]  wr_bust_len_a;        //从ddr4中写数据时的突发长度
51  reg           star_rd_flag;         //复位后写入2帧的标志信号
52  reg           rd_load_d0;
53  reg           rd_load_d1;
54  reg           raddr_rst_h;          //输出源的帧复位脉冲
55  reg           wr_load_d0;
56  reg           wr_load_d1;
57  reg           wr_rst;               //输入源帧复位标志
58  reg           rd_rst;               //输出源帧复位标志
59  reg           raddr_page;           //ddr4读地址切换信号
60  reg           waddr_page;           //ddr4写地址切换信号
61  reg           burst_done_wr;        //一次突发写结束信号
62  reg           burst_done_rd;        //一次读发写结束信号
63  reg           wr_end;               //一次突发写结束信号
64  reg           rd_end;               //一次读发写结束信号   
65  
66  wire          rst_n;
67  
68  //*****************************************************
69  //**                    main code
70  //***************************************************** 
71  
72  //将数据有效信号赋给wfifo写使能
73  assign rfifo_wren =  app_rd_data_valid;
74  
75  assign rst_n = ~ui_clk_sync_rst;
76  
77  //在写状态MIG空闲且写有效,或者在读状态MIG空闲,此时使能信号为高,其他情况为低
78  assign app_en = ((state_cnt == WRITE && (app_rdy && app_wdf_rdy))
79                  ||(state_cnt == READ && app_rdy)) ? 1'b1:1'b0;
80                  
81  //在写状态,MIG空闲且写有效,此时拉高写使能
82  assign app_wdf_wren = (state_cnt == WRITE && (app_rdy && app_wdf_rdy)) ? 1'b1:1'b0;
83  
84  //由于我们ddr4芯片时钟和用户时钟的分频选择4:1,突发长度为8,故两个信号相同
85  assign app_wdf_end = app_wdf_wren; 
86  
87  //处于读的时候命令值为1,其他时候命令值为0
88  assign app_cmd = (state_cnt == READ) ? 3'd1 :3'd0; 
89  
90  //将数据读写地址赋给ddr地址
91  always @(*)  begin
92      if(~rst_n)
93          app_addr <= 0;
94      else if(state_cnt == READ )
95          if(ddr4_pingpang_en)
96              app_addr <= {2'b0,raddr_page,app_addr_rd[24:0]};
97          else 
98              app_addr <= {3'b0,app_addr_rd[24:0]};            
99      else if(ddr4_pingpang_en)
100         app_addr <= {2'b0,waddr_page,app_addr_wr[24:0]};
101     else
102         app_addr <= {3'b0,app_addr_wr[24:0]};        
103 end  
104 

在程序的第73行至88行,这些代码在“DDR4读写测试实验”程序中已有解释,此处不再详述。
在程序的第91行至103行,设计的代码的意义是根据状态计数器(state_cnt)的状态来判断写入ddr4的地址是写地址还是读地址,在第95行和99行,表示的意义是当乒乓使能为高时,对读写存储空间进行乒乓操作,保证读写的存储不会在同一个空间,反之,就不进行乒乓操作,使读写的存储在同一个空间。

105 //对信号进行打拍处理
106 always @(posedge ui_clk or negedge rst_n)  begin
107     if(~rst_n)begin
108         rd_load_d0 <= 0;
109         rd_load_d1 <= 0; 
110         wr_load_d0 <= 0; 
111         wr_load_d1 <= 0;                    
112     end   
113     else begin
114         rd_load_d0 <= rd_load;
115         rd_load_d1 <= rd_load_d0;  
116         wr_load_d0 <= wr_load; 
117         wr_load_d1 <= wr_load_d0;                
118     end    
119 end 
120 
121 //对异步信号进行打拍处理
122 always @(posedge ui_clk or negedge rst_n)  begin
123     if(~rst_n)begin
124         app_addr_rd_min_a <= 0;
125         app_addr_rd_max_a <= 0; 
126         rd_bust_len_a <= 0; 
127         app_addr_wr_min_a <= 0;  
128         app_addr_wr_max_a <= 0; 
129         wr_bust_len_a <= 0;                            
130     end   
131     else begin
132         app_addr_rd_min_a <= app_addr_rd_min;
133         app_addr_rd_max_a <= app_addr_rd_max; 
134         rd_bust_len_a <= rd_bust_len; 
135         app_addr_wr_min_a <= app_addr_wr_min;  
136         app_addr_wr_max_a <= app_addr_wr_max; 
137         wr_bust_len_a <= wr_bust_len;                    
138     end    
139 end 
140 
141 //对输入源做个帧复位标志
142 always @(posedge ui_clk or negedge rst_n)  begin
143     if(~rst_n)
144         wr_rst <= 0;                
145     else if(wr_load_d0 && !wr_load_d1)
146         wr_rst <= 1;               
147     else
148         wr_rst <= 0;           
149 end
150 
151 //对输出源做个帧复位标志 
152 always @(posedge ui_clk or negedge rst_n)  begin
153     if(~rst_n)
154         rd_rst <= 0;                
155     else if(rd_load_d0 && !rd_load_d1)
156         rd_rst <= 1;               
157     else
158         rd_rst <= 0;           
159 end
160 
161 //对输出源的读地址做个帧复位脉冲 
162 always @(posedge ui_clk or negedge rst_n)  begin
163     if(~rst_n)
164         raddr_rst_h <= 1'b0;
165     else if(rd_load_d0 && !rd_load_d1)
166         raddr_rst_h <= 1'b1;
167     else if(app_addr_rd == app_addr_rd_min_a)   
168         raddr_rst_h <= 1'b0;
169     else
170         raddr_rst_h <= raddr_rst_h;              
171 end 
172 
173 //对输出源的帧复位脉冲进行计数 
174 always @(posedge ui_clk or negedge rst_n)  begin
175     if(~rst_n)
176         raddr_rst_h_cnt <= 11'b0;
177     else if(raddr_rst_h)
178         raddr_rst_h_cnt <= raddr_rst_h_cnt + 1'b1;
179     else
180         raddr_rst_h_cnt <= 11'b0;            
181 end 
182 
183 //对输出源帧的读地址高位切换
184 always @(posedge ui_clk or negedge rst_n)  begin
185     if(~rst_n)
186         raddr_page <= 1'b0;
187     else if( rd_end)
188         raddr_page <= ~waddr_page;         
189     else
190         raddr_page <= raddr_page;           
191 end 
192 

在程序的第106行至119行,这段代码是对输入进来的更新信号进行打拍处理,为了同步时钟域,方便后面产生复位信号。
在程序的第122行至139行,这段代码是对输入进来的更新信号进行打拍处理,为了同步时钟域,减少信号扇出和延迟。
在程序的第142行至159行,这段代码是更新信号的上升沿来做复位信号的。
在程序的第162行至181行,设计这几行的目的是为了保证在FIFO完全复位后再对MIG IP核进行操作,这样就防止了FIFO还没有复位完全就写入数据的情况,如下图所示,当复位结束后,full信号还将持续一段时间,在这段时间内是不能写入数据的,必须让信号raddr_rst_h拉高一段时间。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.4 rfifo的复位时序图

193 //对输入源帧的写地址高位切换
194 always @(posedge ui_clk or negedge rst_n)  begin
195     if(~rst_n)
196         waddr_page <= 1'b1;
197     else if( wr_end)
198         waddr_page <= ~waddr_page ;         
199     else
200         waddr_page <= waddr_page;           
201 end   
202 
203 //ddr4读写逻辑实现
204 always @(posedge ui_clk or negedge rst_n) begin
205     if(~rst_n) begin 
206         state_cnt    <= IDLE;              
207         wr_addr_cnt  <= 24'd0;      
208         rd_addr_cnt  <= 24'd0;       
209         app_addr_wr  <= 28'd0;   
210         app_addr_rd  <= 28'd0; 
211         wr_end       <= 1'b0;
212         rd_end       <= 1'b0;       
213     end
214     else begin
215         case(state_cnt)
216             IDLE:begin
217                 if(init_calib_complete)
218                     state_cnt <= ddr4_DONE ;
219                 else
220                     state_cnt <= IDLE;
221             end
222             ddr4_DONE:begin
223                 if(wr_rst)begin   //当帧复位到来时,对寄存器进行复位
224                     state_cnt <= ddr4_DONE;
225                     wr_addr_cnt  <= 24'd0;  
226                     app_addr_wr <= app_addr_wr_min_a;                   
227                 end    //当读到结束地址对寄存器复位
228                 else if(app_addr_rd >= app_addr_rd_max_a - 8)begin  
229                         state_cnt <= ddr4_DONE;
230                         rd_addr_cnt  <= 24'd0;      
231                         app_addr_rd <= app_addr_rd_min_a; 
232                         rd_end <= 1'b1;
233                 end    //当写到结束地址对寄存器复位
234                 else if(app_addr_wr >= app_addr_wr_max_a - 8)begin  
235                         state_cnt <= ddr4_DONE;
236                         rd_addr_cnt  <= 24'd0;      
237                         app_addr_wr <= app_addr_wr_min_a; 
238                         wr_end <= 1'b1;
239                 end                             
240                 else if(wfifo_rcount >= wr_bust_len_a - 2 )begin  
241                     state_cnt <= WRITE;              //跳到写操作
242                     wr_addr_cnt  <= 24'd0;                       
243                     app_addr_wr <= app_addr_wr;      //写地址保持不变
244                 end
245                 else if(raddr_rst_h)begin           //当帧复位到来时,对寄存器进行复位 
246                     if(raddr_rst_h_cnt >= 1000 && ddr4_read_valid)begin  
247                         state_cnt <= READ;         //保证读fifo在复位时不回写入数据
248                         rd_addr_cnt  <= 24'd0;      
249                         app_addr_rd <= app_addr_rd_min_a; 
250                     end
251                     else begin
252                         state_cnt <= ddr4_DONE;
253                         rd_addr_cnt  <= 24'd0;      
254                         app_addr_rd <= app_addr_rd;                                
255                     end                                
256                 end      //当rfifo存储数据少于一次突发长度时,并且ddr已经写入了1帧数据
257                 else if(rfifo_wcount < rd_bust_len_a && ddr4_read_valid )begin  
258                     state_cnt <= READ;                              //跳到读操作
259                     rd_addr_cnt <= 24'd0;
260                     app_addr_rd <= app_addr_rd;      //读地址保持不变
261                 end
262                 else begin
263                     state_cnt <= state_cnt;   
264                     wr_addr_cnt  <= 24'd0;      
265                     rd_addr_cnt  <= 24'd0;  
266                     rd_end <= 1'b0;   
267                     wr_end <= 1'b0;                                       
268                 end
269             end    
270             WRITE:   begin 
271                 if((wr_addr_cnt == (wr_bust_len_a - 1)) && 
272                 (app_rdy && app_wdf_rdy))begin    //写到设定的长度跳到等待状态                  
273                     state_cnt    <= ddr4_DONE;       //写到设定的长度跳到等待状态               
274                     app_addr_wr <= app_addr_wr + 8;  //一次性写进8个数,故加8
275                 end       
276                 else if(app_rdy && app_wdf_rdy)begin   //写条件满足
277                     wr_addr_cnt  <= wr_addr_cnt + 1'd1;//写地址计数器自加
278                     app_addr_wr  <= app_addr_wr + 8;   //一次性写进8个数,故加8
279                 end
280                 else begin                             //写条件不满足,保持当前值     
281                     wr_addr_cnt  <= wr_addr_cnt;
282                     app_addr_wr  <= app_addr_wr; 
283                 end
284             end
285             READ:begin                      //读到设定的地址长度    
286                 if((rd_addr_cnt == (rd_bust_len_a - 1)) && app_rdy)begin
287                     state_cnt   <= ddr4_DONE;          //则跳到空闲状态 
288                     app_addr_rd <= app_addr_rd + 8;
289 
290                 end       
291                 else if(app_rdy)begin               //若MIG已经准备好,则开始读
292                     rd_addr_cnt <= rd_addr_cnt + 1'd1; //用户地址计数器每次加一
293                     app_addr_rd <= app_addr_rd + 8; //一次性读出8个数,ddr4地址加8
294                 end
295                 else begin                         //若MIG没准备好,则保持原值
296                     rd_addr_cnt <= rd_addr_cnt;
297                     app_addr_rd <= app_addr_rd; 
298                 end
299             end             
300             default:begin
301                 state_cnt    <= IDLE;
302                 wr_addr_cnt  <= 24'd0;
303                 rd_addr_cnt  <= 24'd0;
304             end
305         endcase
306     end
307 end                          
308 
309 endmodule

程序中第184至201行定义了两个用于切换BANK的信号(waddr_page信号和raddr_page信号), waddr_page信号根据写DDR4的结束地址标志的高电平进行翻转,raddr_page信号是读DDR4的结束地址标志的高电平时将waddr_page信号取反赋值得到的。程序中定义了两个信号app_addr_wr和信号app_addr_rd分别代表DDR4的写入地址和读出地址,其最高三位表示BANK的地址,切换BANK时改变app_addr_wr和app_addr_rd的高三位地址,相当于数据在BANK0和BANK2之间切换。当waddr_page=1时,数据写入BANK0,从BANK2中读出数据;当waddr_page=0时,数据写入BANK2,从BANK0中读出数据。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.5 ddr读写状态跳转图
如程序中第204至307行所示,这段代码是DDR4读写逻辑实现,转态跳转如上图所示。
在复位结束后,如果DDR4没有初始化完成,那么状态一直在空闲状态(IDLE),否则跳到DDR4空闲状态(ddr4_DONE)。在DDR4空闲状态,当输入端的帧复位到来时,对写地址计数器和写地址进行复位操作。
程序中第228至239行,当读写地址达到设定值时,对地址和计数器进行复位操作,同时拉高结束地址标志信号。
如程序中第240至244行所示,FIFO控制模块优先处理DDR4写请求,以免写FIFO溢出时,用于写入DDR4的数据丢失。当写FIFO中的数据量大于写突发长度时,执行写DDR4操作;当读FIFO中的数据量小于读突发长度时,执行读DDR4操作。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.6 写状态时序
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.7 读状态时序
当写fifo的存储数据达到一次写操作的长度时,就将状态跳到写状态(WRITE),如图 32.4.6所示。这里说明下在代码第240行减2的原因,因为在FIFO调度模块中用的FIFO不是标准FIFO的模式,是First Word Fall Through模式,这种模式的特点是数据和读使能同时出来。在地址复位寄存器为高,且持续的时间大于1000个时钟后,将状态跳转到读状态(READ),否则状态继续留在DDR4空闲状态,这样做的目的是为了保证在FIFO复位的时候不会向其写入数据。当rfifo里面的数据不满一次读操作的情况下,将状态跳转到读状态(READ),如图 32.4.7所示,否则留在DDR4空闲状态。
在写状态中,当写地址计数器达到一次写操作的时候,并且握手信号命令使能app_rdy和写数据有效使能app_wdf_rdy同时为高的时候,将状态跳转到DDR4空闲状态。细心的朋友可能发现,这里的地址为什么加8,这是因为用户端在每一个用户时钟进行一个128bit的数据的传输,在DDR4物理芯片端需要分8次传输,每次传输一个地址位宽16bit,8次就需要8个地址。同理读状态也是如此,此处不在详述。
接下来我们再来看看FIFO读写模块的代码,如下所示:

1   module ddr4_fifo_ctrl(
2       input           rst_n            ,  //复位信号
3       input           wr_clk           ,  //wfifo时钟
4       input           rd_clk           ,  //rfifo时钟
5       input           clk_100          ,  //用户时钟
6       input           datain_valid     ,  //数据有效使能信号
7       input  [15:0]   datain           ,  //有效数据
8       input  [127:0]  rfifo_din        ,  //用户读数据
9       input           rdata_req        ,  //请求像素点颜色数据输入 
10      input           rfifo_wren       ,  //从ddr4读出数据的有效使能
11      input           wfifo_rden       ,  //wfifo读使能
12      input           rd_load          ,  //输出源场信号
13      input           wr_load          ,  //输入源场信号          
14  
15      output [127:0]  wfifo_dout       ,  //用户写数据
16      output [10:0]   wfifo_rcount     ,  //rfifo剩余数据计数
17      output [10:0]   rfifo_wcount     ,  //wfifo写进数据计数
18      output [15:0]   pic_data            //有效数据      
19      );
20          
21  //reg define
22  reg  [127:0] datain_t          ;  //由16bit输入源数据移位拼接得到
23  reg  [7:0]   cam_data_d0       ; 
24  reg  [4:0]   i_d0              ;
25  reg  [30:0]  rd_load_d         ;  //由输出源场信号移位拼接得到           
26  reg  [6:0]   byte_cnt          ;  //写数据移位计数器
27  reg  [127:0] data              ;  //rfifo输出数据打拍得到
28  reg  [15:0]  pic_data          ;  //有效数据 
29  reg  [4:0]   i                 ;  //读数据移位计数器
30  reg  [15:0]  wr_load_d         ;  //由输入源场信号移位拼接得到 
31  reg  [3:0]   cmos_ps_cnt       ;  //等待帧数稳定计数器
32  reg          cam_href_d0       ;
33  reg          cam_href_d1       ;
34  reg          wr_load_d0        ;
35  reg          rd_load_d0        ;
36  reg          rdfifo_rst_h      ;  //rfifo复位信号,高有效
37  reg          wr_load_d1        ;
38  reg          wfifo_rst_h       ;  //wfifo复位信号,高有效
39  reg          wfifo_wren        ;  //wfifo写使能信号
40  
41  //wire define 
42  wire [127:0] rfifo_dout        ;  //rfifo输出数据    
43  wire [127:0] wfifo_din         ;  //wfifo写数据
44  wire [15:0]  dataout[0:15]     ;  //定义输出数据的二维数组
45  wire         rfifo_rden        ;  //rfifo的读使能
46  
47  //*****************************************************
48  //**                    main code
49  //*****************************************************  
50  
51  //rfifo输出的数据存到二维数组
52  assign dataout[0] = data[127:112];
53  assign dataout[1] = data[111:96];
54  assign dataout[2] = data[95:80];
55  assign dataout[3] = data[79:64];
56  assign dataout[4] = data[63:48];
57  assign dataout[5] = data[47:32];
58  assign dataout[6] = data[31:16];
59  assign dataout[7] = data[15:0];
60  
61  assign wfifo_din = datain_t ;
62  
63  //移位寄存器计满时,从rfifo读出一个数据
64  assign rfifo_rden = (rdata_req && (i==7)) ? 1'b1  :  1'b0; 
65  
66  //16位数据转128位RGB565数据        
67  always @(posedge wr_clk or negedge rst_n) begin
68      if(!rst_n) begin
69          datain_t <= 0;
70          byte_cnt <= 0;
71      end
72      else if(datain_valid) begin
73          if(byte_cnt == 7)begin
74              byte_cnt <= 0;
75              datain_t <= {datain_t[111:0],datain};
76          end
77          else begin
78              byte_cnt <= byte_cnt + 1;
79              datain_t <= {datain_t[111:0],datain};
80          end
81      end
82      else begin
83          byte_cnt <= byte_cnt;
84          datain_t <= datain_t;
85      end    
86  end 
87  

在程序的第51行至59行是将DDR4输出的128bit的数据存入一个二维数组(dataout)中,方便后面的数据拆解。
在程序的第64行,表示当读请求信号(rdata_req)拉高8个时钟周期后,rfifo的读使能(rfifo_rden)拉高一个周期,这么做的原因是 rfifo的数据输出为128bit而本模块的数据输出(pic_data)为16bit。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.8 写数据数据转换
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.4.9 读数据数据转换
在程序的第66行至86行对摄像头输出的数据进行了移位拼接,具体时序如图 32.4.8所示。因为摄像头的输入数据(datain)为16bit每周期,而MIG核每一个时钟周期写入的信号位宽为128bit,所以写数据移位计数器(byte_cnt)信号计到7时清零,同时FIFO的写使能wfifo_wren拉高一个周期。同理LCD顶层模块的输入也是16bit一个时钟周期,所以在程序的第107行至131行对128bit的数据进行了拆解,具体时序如图 32.4.9所示。

88  //wfifo写使能产生
89  always @(posedge wr_clk or negedge rst_n) begin
90      if(!rst_n) 
91          wfifo_wren <= 0;
92      else if(wfifo_wren == 1)
93          wfifo_wren <= 0;
94      else if(byte_cnt == 7 && datain_valid )  //输入源数据传输8次,写使能拉高一次
95          wfifo_wren <= 1;
96      else 
97          wfifo_wren <= 0;
98  end
99      
100 always @(posedge rd_clk or negedge rst_n) begin
101     if(!rst_n)
102         data <= 127'b0;
103     else 
104         data <= rfifo_dout; 
105 end     
106 
107 //对rfifo出来的128bit数据拆解成16个16bit数据
108 always @(posedge rd_clk or negedge rst_n) begin
109     if(!rst_n) begin
110         pic_data <= 16'b0;
111         i <=0;
112         i_d0 <= 0;
113     end
114     else if(rdata_req) begin
115         if(i == 7)begin
116             pic_data <= dataout[i_d0];
117             i <= 0;
118             i_d0 <= i;
119         end
120         else begin
121             pic_data <= dataout[i_d0];
122             i <= i + 1;
123             i_d0 <= i;
124         end
125     end 
126     else begin
127         pic_data <= pic_data;
128         i <=0;
129         i_d0 <= 0;
130     end
131 end  
132 
133 always @(posedge clk_100 or negedge rst_n) begin
134     if(!rst_n)
135         rd_load_d0 <= 1'b0;
136     else
137         rd_load_d0 <= rd_load;      
138 end 
139 
140 //对输出源场信号进行移位寄存
141 always @(posedge clk_100 or negedge rst_n) begin
142     if(!rst_n)
143         rd_load_d <= 1'b0;
144     else
145         rd_load_d <= {rd_load_d[30:0],rd_load_d0};       
146 end 
147 
在程序的第88行至98行,当写数据有效使能信号(datain_valid)拉高8个时钟周期后,wfifo的写使能才拉高一个周期,这么做的原因是wfifo的数据输入为128bit而本模块的数据输入(datain)为16bit。
在程序的第132行至138行,是对LCD的场信号进行时钟域同步,方便后面代码的调用,以减少信号的跨时钟域。程序的第158行至168行,原因也是如此。
在程序的第140行至146行,对LCD的场信号进行移位寄存,方便后面产生一段复位电平信号。

148 //产生一段复位电平,满足fifo复位时序  
149 always @(posedge clk_100 or negedge rst_n) begin
150     if(!rst_n)
151         rdfifo_rst_h <= 1'b0;
152     else if(rd_load_d[0] && !rd_load_d[29])
153         rdfifo_rst_h <= 1'b1;   
154     else
155         rdfifo_rst_h <= 1'b0;              
156 end  
157 
158 //对输入源场信号进行移位寄存
159 always @(posedge wr_clk or negedge rst_n) begin
160     if(!rst_n)begin
161         wr_load_d0 <= 1'b0;
162         wr_load_d  <= 16'b0;        
163     end     
164     else begin
165         wr_load_d0 <= wr_load;
166         wr_load_d <= {wr_load_d[14:0],wr_load_d0};      
167     end                 
168 end  
169 
170 //产生一段复位电平,满足fifo复位时序 
171 always @(posedge wr_clk or negedge rst_n) begin
172     if(!rst_n)
173     wfifo_rst_h <= 1'b0;          
174     else if(wr_load_d[0] && !wr_load_d[15])
175     wfifo_rst_h <= 1'b1;       
176     else
177     wfifo_rst_h <= 1'b0;                      
178 end   
179 
180 rd_fifo u_rd_fifo (
181 .rst               (~rst_n|rdfifo_rst_h),                    
182 .wr_clk            (clk_100),   
183 .rd_clk            (rd_clk),    
184 .din               (rfifo_din), 
185 .wr_en             (rfifo_wren),
186 .rd_en             (rfifo_rden),
187 .dout              (rfifo_dout),
188 .full              (),          
189 .empty             (),          
190 .rd_data_count     (),  
191 .wr_data_count     (rfifo_wcount),  
192 .wr_rst_busy       (),      
193 .rd_rst_busy       ()      
194 );
195 
196 wr_fifo u_wr_fifo (
197 .rst               (~rst_n|wfifo_rst_h),
198 .wr_clk            (wr_clk),            
199 .rd_clk            (clk_100),           
200 .din               (wfifo_din),         
201 .wr_en             (wfifo_wren),        
202 .rd_en             (wfifo_rden),        
203 .dout              (wfifo_dout ),       
204 .full              (),                  
205 .empty             (),                  
206 .rd_data_count     (wfifo_rcount),  
207 .wr_data_count     (),  
208 .wr_rst_busy       (),      
209 .rd_rst_busy       ()    
210 );
211 
212 endmodule 

在程序的第148行至156行和第170行至178行是分别对rfifo和wfifo进行帧复位,这里进行复位是为了在帧结束后清空FIFO,保证下帧数据到来之前FIFO为空。细心的朋友应该发现了一般复位是一个时钟周期,为什么这里的复位是一段电平,因为XILINX的手册规定FIFO的复位周期必须大于读写时钟的5个时钟周期,才能使FIFO完全复位。
看完了DDR4控制模块的代码我们再来看看图像分辨率模块的代码,如下所示:

1   module picture_size (
2       input              rst_n         ,
3       input              clk           ,         
4       input       [15:0] ID_lcd        ,
5               
6       output      [12:0] cmos_h_pixel  ,
7       output      [12:0] cmos_v_pixel  ,   
8       output      [12:0] total_h_pixel ,
9       output      [12:0] total_v_pixel ,
10      output      [23:0] sdram_max_addr
11  );
12  
13  reg [12:0] cmos_h_pixel;
14  reg [12:0] cmos_v_pixel;   
15  reg [12:0] total_h_pixel;
16  reg [12:0] total_v_pixel;
17  reg [23:0] sdram_max_addr;
18  
19  //parameter define
20  parameter  ID_4342 =   16'h4342;
21  parameter  ID_7084 =   16'h7084;
22  parameter  ID_7016 =   16'h7016;
23  parameter  ID_1018 =   16'h1018;
24  
25  //*****************************************************
26  //**                    main code                      
27  //*****************************************************
28  
29  //配置摄像头输出尺寸的大小
30  always @(posedge clk or negedge rst_n) begin 
31      if(!rst_n) begin
32          cmos_h_pixel <= 13'b0;
33          cmos_v_pixel <= 13'd0;
34          sdram_max_addr <= 23'd0;        
35      end 
36      else begin    
37          case(ID_lcd ) 
38              16'h4342 : begin
39                  cmos_h_pixel   = 13'd480;    
40                  cmos_v_pixel   = 13'd272;
41                  sdram_max_addr = 23'd130560;
42              end 
43              16'h7084 : begin
44                  cmos_h_pixel   = 13'd800;    
45                  cmos_v_pixel   = 13'd480;           
46                  sdram_max_addr = 23'd384000;
47              end 
48              16'h7016 : begin
49                  cmos_h_pixel   = 13'd1024;    
50                  cmos_v_pixel   = 13'd600;           
51                  sdram_max_addr = 23'd614400;
52              end    
53              16'h1018 : begin
54                  cmos_h_pixel   = 13'd1280;    
55                  cmos_v_pixel   = 13'd800;           
56                  sdram_max_addr = 23'd1024000;
57              end 
58          default : begin
59                  cmos_h_pixel   = 13'd800;    
60                  cmos_v_pixel   = 13'd480;           
61                  sdram_max_addr = 23'd384000;
62          end
63          endcase
64      end    
65  end 
66  
67  //对HTS及VTS的配置会影响摄像头输出图像的帧率
68  always @(*) begin
69      case(ID_lcd)
70          ID_4342 : begin 
71              total_h_pixel = 13'd1800;
72              total_v_pixel = 13'd1000;
73          end 
74          ID_7084 : begin  
75              total_h_pixel = 13'd1800;
76              total_v_pixel = 13'd1000;
77          end 
78          ID_7016 : begin  
79              total_h_pixel = 13'd2200;
80              total_v_pixel = 13'd1000;
81          end 
82          ID_1018 : begin 
83              total_h_pixel = 13'd2570;
84              total_v_pixel = 13'd980;
85          end 
86      default : begin
87              total_h_pixel = 13'd1800;
88              total_v_pixel = 13'd1000;
89      end 
90      endcase
91  end 
92  
93  endmodule 
图像分辨率模块的代码时是非常简单的就是根据LCD驱动模块传递过来的不同ID值去配置不同的图像分辨率参数(cmos_h_pixel、cmos_v_pixel、total_h_pixel、total_v_pixel)和DDR4最大读写地址(sdram_max_addr),所以这里关于图像分辨率模块的代码这里就不作过多讲解了。
看完图像分辨率模块的代码后我们再来看看LCD驱动模块的代码,如下所示:
1   module lcd_rgb_top(
2       input           sys_clk      ,  //系统时钟
3       input           sys_rst_n,      //复位信号 
4       input           sys_init_done, 
5       //lcd接口  
6       output          lcd_clk,        //LCD驱动时钟    
7       output          lcd_hs,         //LCD 行同步信号
8       output          lcd_vs,         //LCD 场同步信号
9       output          lcd_de,         //LCD 数据输入使能
10      inout  [23:0]   lcd_rgb,        //LCD RGB颜色数据
11      output          lcd_bl,         //LCD 背光控制信号
12      output          lcd_rst,        //LCD 复位信号
13      output          lcd_pclk,       //LCD 采样时钟
14      output  [15:0]  lcd_id,         //LCD屏ID  
15      output          out_vsync,      //lcd场信号 
16      output  [10:0]  pixel_xpos,     //像素点横坐标
17      output  [10:0]  pixel_ypos,     //像素点纵坐标        
18      output  [10:0]  h_disp,         //LCD屏水平分辨率
19      output  [10:0]  v_disp,         //LCD屏垂直分辨率         
20      input   [15:0]  data_in,        //数据输入   
21      output          data_req        //请求数据输入
22      
23      );
24  
25  //wire define
26  wire  [15:0] lcd_rgb_565;           //输出的16位lcd数据
27  wire  [23:0] lcd_rgb_o ;            //LCD 输出颜色数据
28  wire  [23:0] lcd_rgb_i ;            //LCD 输入颜色数据
29  wire         lcd_pclk_180;
30  //*****************************************************
31  //**                    main code
32  //***************************************************** 
33  //将摄像头16bit数据转换为24bit的lcd数据
34  assign lcd_rgb_o = {lcd_rgb_565[15:11],3'b000,lcd_rgb_565[10:5],2'b00,
35                      lcd_rgb_565[4:0],3'b000};          
36  assign lcd_pclk= (lcd_id==16'h1018)?~lcd_pclk_180:lcd_pclk_180;
37  //像素数据方向切换
38  assign lcd_rgb = lcd_de ?  lcd_rgb_o :  {24{1'bz}};
39  assign lcd_rgb_i = lcd_rgb;
40              
41  //时钟分频模块    
42  clk_div u_clk_div(
43      .clk                    (sys_clk  ),
44      .rst_n                  (sys_rst_n),
45      .lcd_id                 (lcd_id   ),
46      .lcd_pclk               (lcd_clk  )
47      );  
48  
49  //读LCD ID模块
50  rd_id u_rd_id(
51      .clk                    (sys_clk  ),
52      .rst_n                  (sys_rst_n),
53      .lcd_rgb                (lcd_rgb_i),
54      .lcd_id                 (lcd_id   )
55      );  
56  
57  //lcd驱动模块
58  lcd_driver u_lcd_driver(           
59      .lcd_clk        (lcd_clk),    
60      .sys_rst_n      (sys_rst_n & sys_init_done), 
61      .lcd_id         (lcd_id),   
62  
63      .lcd_hs         (lcd_hs),       
64      .lcd_vs         (lcd_vs),       
65      .lcd_de         (lcd_de),       
66      .lcd_rgb        (lcd_rgb_565),
67      .lcd_bl         (lcd_bl),
68      .lcd_rst        (lcd_rst),
69      .lcd_pclk       (lcd_pclk_180),
70      
71      .pixel_data     (data_in), 
72      .data_req       (data_req),
73      .out_vsync      (out_vsync),
74      .h_disp         (h_disp),
75      .v_disp         (v_disp), 
76      .pixel_xpos     (pixel_xpos), 
77      .pixel_ypos     (pixel_ypos)
78      ); 
79                  
80  endmodule  

LCD驱动模块代码的顶层例化了三个子模块,分别是时钟分频模块(clk_div)、ID读取模块(rd_id)以及LCD驱动时序模块(lcd_driver)。其中时钟分频模块的作用是根据外部不同的显示屏提供与显示屏相对应的驱动时钟;ID读取模块的作用就是读取外部显示屏的ID;LCD驱动时序模块主要就是生成LCD的驱动时序。LCD驱动模块顶层主要作用就是例化子模块没什么好讲的,但是有两个地方需要注意,一个是代码第3435行的数据转换,我们的摄像头数据是rgb565的数据格式而显示屏的数据格式是rgb888格式因此在这里需要通过补零的手段去将rgb565格式转换成rgb888格式。第二个需要注意的点就是代码第3839行,因为lcd_rgb信号是一个双向通道(24bit位宽),因此我们用零个assign语句来生成一个三态门,由lcd_de信号决定lcd_rgb通道是输入还是输出。关于例化的三个子模块代码在前面“LCD彩条显示实验”已经讲解过了这里就不在重复啰嗦了。
到这里整个OV5640 LCD显示实验的代码就全部讲解完了。
32.5下载验证
首先将FPC排线一端与RGB-LCD模块上的RGB接口连接,另一端与DFZU2EG/4EV MPSoC开发板上的RGB-LCD接口连接。与RGB-LCD模块连接时,先掀开FPC连接器上的黑色翻盖,将FPC排线蓝色面朝里(靠近FPGA端)插入连接器,最后将黑色翻盖压下以固定FPC排线,如图 32.5.1所示。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.5.1 正点原子RGBLCD模块FPC连接器
与DFZU2EG/4EV MPSoC开发板上的RGB TFTLCD接口连接时,先掀起开发板上的棕色的翻盖到垂直开发板方向,将FPC排线蓝色面朝棕色翻盖垂直插入连接器,最后将棕色的翻盖下按至水平方向,如图 32.5.2所示。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.5.2 DFZU2EG/4EV MPSoC开发板连接RGB-LCD液晶屏
然后将双目OV5640摄像头(本节实验只使用双目摄像头模块的其中一个摄像头)模块插在DFZU2EG/4EV MPSoC开发板J19扩展口上,摄像头实物连接如上图所示。最后将下载器一端连电脑,另一端与开发板上的JTAG端口连接,接下来连接电源线后拨动开关按键给开发板上电。
接下来我们下载程序,验证OV5640 RGB-LCD实时显示功能。下载完成后观察RGB-LCD模块显示的图案如下图所示,说明OV5640 RGB-LCD实时显示程序下载验证成功。
【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

图 32.5.3 RGB-LCD实时显示图像文章来源地址https://www.toymoban.com/news/detail-401642.html

到了这里,关于【正点原子FPGA连载】第三十二章OV5640摄像头RGB-LCD显示实验 摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包