OV5640摄像头RGB-LCD显示实验
OV5640是OmniVision(豪威科技)公司生产的CMOS图像传感器,该传感器分辨率高、采集速率快,图像处理性能强,主要应用在手机、数码相机、电脑多媒体等领域。本章将使用FPGA开发板实现对OV5640的数字图像采集并通过LCD实时显示。
本章包括以下几个部分:
- 简介
- 实验任务
- 硬件设计
- 程序设计
- 下载验证
简介
OV5640是一款1/4英寸单芯片图像传感器,其感光阵列达到2592*1944(即500W像素),能实现最快15fps QSXVGA(2592*1944)或者90fps VGA(640*480)分辨率的图像采集。传感器采用OmniVision推出的OmniBSI(背面照度)技术,使传感器达到更高的性能,如高灵敏度、低串扰和低噪声。传感器内部集成了图像处理的功能,包括自动曝光控制(AEC)、自动白平衡(AWB)等。同时该传感器支持LED补光、MIPI(移动产业处理器接口)输出接口和DVP(数字视频并行)输出接口选择、ISP(图像信号处理)以及AFC(自动聚焦控制)等功能。
OV5640的功能框图如下图所示:
图32.1.1功能框图
由上图可知,时序发生器(timing)控制着感光阵列(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的写传输协议如下图所示:
图32.1.2写传输协议
上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写:读);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的读传输协议。
图32.1.3读传输协议
由上图可知,SCCB读传输协议分为两个部分。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针指向虚写操作中寄存器地址的位置,当然虚写操作也可以通过前面介绍的写传输协议来完成。第二部分是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据。上图中的NA位由主机(这里指FPGA)产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
需要注意的是,对于OV5640摄像头来说,由于其可配置的寄存器非常多,所以OV5640摄像头的寄存器地址位数是16位(两个字节),OV5640 SCCB的写传输协议如下图所示:
图32.1.4写传输协议
上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写:读),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关键寄存器配置说明
地址 (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:输入:输出) 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:输入:输出) Bit[7:2]:D[5:0]输出使能 Bit[1]:GPIO1输出使能 Bit[0]:GPIO0输出使能 |
0x3019 | PAD OUTPUT VALUE 00 | 0x00 | Bit[1]: 闪光灯操作 :关闭闪光灯 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]:数据输出格式 :RAW 1:Y8 2:YUV444/RGB888 3:YUV422 4:YUV420 5:YUV420(仅在MIPI输出接口有效) :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输入窗口设置、预缩放窗口设置和输出大小窗口设置,这几个设置与我们的正常使用密切相关,有必要了解一下,它们的设置关系如下图所示:
图32.1.5 图像窗口设置
ISP输入窗口设置(ISP Input Size)允许用户设置整个传感器显示区域(physical pixel size,2632*1951,其中2592*1944像素是有效的),开窗范围从0*0~2632*1951都可以任意设置。也就是上图中的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)是在预缩放窗口的基础上,经过内部DSP进行缩放处理,并将处理后的数据输出给外部的图像窗口,图像窗口控制着最终的图像输出尺寸。可以通过X_OUTPUT_SIZE(寄存器地址0x3808、0x3809)和Y_OUTPUT_SIZE(寄存器地址0x380A、0x380B)进行配置。注意:当输出大小窗口与预缩放窗口比例不一致时,图像将进行缩放处理(图像变形),仅当两者比例一致时,输出比例才是1:1(正常图像)。
图中,右侧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格式。下图为摄像头输出的时序图。
图32.1.6输出时序图
在介绍时序图之前先了解几个基本的概念。
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格式的时序图:
图32.1.7模式时序图
上图中的PCLK为OV5640输出的像素时钟,HREF为行同步信号,D[9:2]为8位像素数据。OV5640最大可以输出10位数据,在RGB565输出模式中,只有高8位是有效的。像素数据在HREF为高电平时有效,第一次输出的数据为RGB565数据的高8位,第二次输出的数据为RGB565数据的低8位,first byte和second byte组成一个16位RGB565数据。由上图可知,数据是在像素时钟的下降沿改变的,为了在数据最稳定的时刻采集图像数据,所以我们需要在像素时钟的上升沿采集数据。
实验任务
本节实验任务是使用DFZU2EG/4EV MPSoC开发板及OV5640摄像头实现图像采集,并通过RGB-LCD接口驱动RGB-LCD液晶屏(支持目前正点原子推出的所有RGB-LCD屏),并实时显示出图像。
硬件设计
我们的DFZU2EG/4EV MPSOC开发板上有一个扩展接口(J19),该接口可以用来连接一些扩展模块,如双目OV5640摄像头、高速ADDA模块、IO扩展板模块等。本次实验就是通过连接双目OV5640摄像头,实现单个OV5640摄像头图像的采集和显示。原理图如下图所示:
图32.3.1扩展接口原理图
ATK-Dual-OV5640是正点原子推出的一款高性能双目OV5640 500W像素高清摄像头模块。该模块通过2*20排母(2.54mm间距)同外部连接,我们将摄像头的排母直接插在开发板上的扩展接口即可,模块外观如图所示:
图32.3.2摄像头模块实物图
我们在前面说过,OV5640在RGB565模式中只有高8位数据是有效的即D[9:2],而我们的摄像头排母上数据引脚的个数是8位。实际上,摄像头排母上的8位数据连接的就是OV5640传感器的D[9:2],所以我们直接使用摄像头排母上的8位数据引脚即可。
由于RGB LCD屏的引脚数目较多,且在前面相应的章节中已经给出它们的管脚列表,这里只列出摄像头相关管脚分配,如下表所示:
表32.3.1 OV5640摄像头管脚分配
信号名 | 方向 | 管脚 | 端口说明 | IO电平 |
cam_pclk | input | C13 | cmos 数据像素时钟 | LVCMOS33 |
cam_vsync | input | G14 | cmos 场同步信号 | LVCMOS33 |
cam_href | input | G13 | cmos 行同步信号 | LVCMOS33 |
cam_rst_n | output | F13 | cmos 复位信号,低电平有效 | LVCMOS33 |
cam_pwdn | output | B15 | cmos 电源休眠模式选择信号 | LVCMOS33 |
cam_scl | output | H13 | cmos SCCB_SCL线 | LVCMOS33 |
cam_sda | inout | F15 | cmos SCCB_SDA线 | LVCMOS33 |
cam_data[0] | input | E15 | cmos 数据 | LVCMOS33 |
cam_data[1] | input | D15 | cmos 数据 | LVCMOS33 |
cam_data[2] | input | E14 | cmos 数据 | LVCMOS33 |
cam_data[3] | input | D14 | cmos 数据 | LVCMOS33 |
cam_data[4] | input | E13 | cmos 数据 | LVCMOS33 |
cam_data[5] | input | B13 | cmos 数据 | LVCMOS33 |
cam_data[6] | input | C14 | cmos 数据 | LVCMOS33 |
cam_data[7] | input | A13 | cmos 数据 | LVCMOS33 |
摄像头XDC约束文件如下:
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.1是根据本章实验任务画出的系统框图。
图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显示屏可以正常的显示摄像头捕捉的图像数据。
顶层模块的原理图如下图所示:
图32.4.2顶层模块原理图
FPGA顶层模块(ov5640_lcd)例化了以下四个模块:驱动模块(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行定义了总共需要配置的寄存器的个数,如果增加或者删减了寄存器的配置,需要修改此参数。代码第40~45行是用来计数已经配置了多少个寄存器。代码第48~57行是用来生成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
摄像头在配置完寄存器后就会输出图像数据,但是此时输出的数据有可能不是特别稳定,所以我们的数据采集模块并没有立刻把摄像头输出的数据接收过来,而是在代码的67~82行做了一个帧计数器,当数到摄像头已经传输完10帧数据后(此时我们认为摄像头数据已经处于稳定状态)才开始接收数据并且拉高帧有效标志(frame_val_flag)。代码第85~104行是将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的同一存储空间,如果只使用一个存储空间缓存图像数据,那么这么做虽然保证了数据不会出现错乱的问题,但是会导致当前读取的图像与上一次存入的图像存在交错,如下图所示:
图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拉高一段时间。
图32.4.4的复位时序图
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中读出数据。
图32.4.5读写状态跳转图
如程序中第204至307行所示,这段代码是DDR4读写逻辑实现,转态跳转如上图所示。
在复位结束后,如果DDR4没有初始化完成,那么状态一直在空闲状态(IDLE),否则跳到DDR4空闲状态(ddr4_DONE)。在DDR4空闲状态,当输入端的帧复位到来时,对写地址计数器和写地址进行复位操作。
程序中第228至239行,当读写地址达到设定值时,对地址和计数器进行复位操作,同时拉高结束地址标志信号。
如程序中第240至244行所示,FIFO控制模块优先处理DDR4写请求,以免写FIFO溢出时,用于写入DDR4的数据丢失。当写FIFO中的数据量大于写突发长度时,执行写DDR4操作;当读FIFO中的数据量小于读突发长度时,执行读DDR4操作。
图32.4.6写状态时序
图32.4.7读状态时序
当写fifo的存储数据达到一次写操作的长度时,就将状态跳到写状态(WRITE),如图32.4.6所示。这里说明下在代码第240行减2的原因,因为在FIFO调度模块中用的FIFO不是标准FIFO的模式,是First Word Fall Through模式,这种模式的特点是数据和读使能同时出来。在地址复位寄存器为高,且持续的时间大于1000个时钟后,将状态跳转到读状态(READ),否则状态继续留在DDR4空闲状态,这样做的目的是为了保证在FIFO复位的时候不会向其写入数据。当rfifo里面的数据不满一次读操作的情况下,将状态跳转到读状态(READ),如图所示,否则留在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。
图32.4.8写数据数据转换
图32.4.9读数据数据转换
在程序的第66行至86行对摄像头输出的数据进行了移位拼接,具体时序如图32.4.8所示。因为摄像头的输入数据(datain)为16bit每周期,而MIG核每一个时钟周期写入的信号位宽为128bit,所以写数据移位计数器(byte_cnt)信号计到7时清零,同时FIFO的写使能wfifo_wren拉高一个周期。同理LCD顶层模块的输入也是16bit一个时钟周期,所以在程序的第107行至131行对128bit的数据进行了拆解,具体时序如图所示。
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驱动模块顶层主要作用就是例化子模块没什么好讲的,但是有两个地方需要注意,一个是代码第34~35行的数据转换,我们的摄像头数据是rgb565的数据格式而显示屏的数据格式是rgb888格式因此在这里需要通过补零的手段去将rgb565格式转换成rgb888格式。第二个需要注意的点就是代码第38~39行,因为lcd_rgb信号是一个双向通道(24bit位宽),因此我们用零个assign语句来生成一个三态门,由lcd_de信号决定lcd_rgb通道是输入还是输出。关于例化的三个子模块代码在前面“LCD彩条显示实验”已经讲解过了这里就不在重复嗦了。
到这里整个OV5640 LCD显示实验的代码就全部讲解完了。
下载验证
首先将FPC排线一端与RGB-LCD模块上的RGB接口连接,另一端与DFZU2EG/4EV MPSoC开发板上的RGB-LCD接口连接。与RGB-LCD模块连接时,先掀开FPC连接器上的黑色翻盖,将FPC排线蓝色面朝里(靠近FPGA端)插入连接器,最后将黑色翻盖压下以固定FPC排线,如图32.5.1所示。
图32.5.1正点原子RGBLCD模块FPC连接器
与DFZU2EG/4EV MPSoC开发板上的RGB TFTLCD接口连接时,先掀起开发板上的棕色的翻盖到垂直开发板方向,将FPC排线蓝色面朝棕色翻盖垂直插入连接器,最后将棕色的翻盖下按至水平方向,如图32.5.2所示。
图32.5.2 DFZU2EG/4EV MPSoC开发板连接RGB-LCD液晶屏
然后将双目OV5640摄像头(本节实验只使用双目摄像头模块的其中一个摄像头)模块插在DFZU2EG/4EV MPSoC开发板J19扩展口上,摄像头实物连接如上图所示。最后将下载器一端连电脑,另一端与开发板上的JTAG端口连接,接下来连接电源线后拨动开关按键给开发板上电。
接下来我们下载程序,验证OV5640 RGB-LCD实时显示功能。下载完成后观察RGB-LCD模块显示的图案如下图所示,说明OV5640 RGB-LCD实时显示程序下载验证成功。
图32.5.3 RGB-LCD实时显示图像