搜索
bottom↓
回复: 0

《ATK-DFPGL22G之FPGA开发指南_V1.0》第三十九章

[复制链接]

出0入234汤圆

发表于 2023-2-23 09:59:21 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2023-2-23 09:59 编辑

1)实验平台:正点原子紫光PGL22G开发板
2)购买链接:https://item.taobao.com/item.htm?&id=692368045899
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-340253-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子FPGA交流群:435699340 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png


第三十九章 OV7725摄像头RGB-LCD显示实验

OV7725是OmniVision(豪威科技)公司生产的一颗CMOS图像传感器,该传感器功耗低、可靠性高且采集速率快,主要应用在玩具、安防监控、电脑多媒体等领域。本章我们将使用FPGA开发板实现对OV7725的数字图像采集并通过LCD实时显示。
本章包括以下几个部分:
39.1简介
39.2实验任务
39.3硬件设计
39.4程序设计
39.5下载验证


39.1简介

OV7725是一款1/4英寸单芯片图像传感器,其感光阵列达到640*480,能实现最快60fps VGA分辨率的图像采集。传感器内部集成了图像处理的功能,包括自动曝光控制(AEC)、自动增益控制(AGC)和自动白平衡(AWB)等。同时传感器具有较高的感光灵敏度,适合低照度的应用,下图为OV7725的功能框图。
OV7725摄像头RGB343.png
图 39.1.1 OV7725功能框图

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

上图中的ID ADDRESS是由7位器件地址和1位读写控制位构成(0:写 1:读),OV7725的器件地址为7’h21,所以在写传输协议中,ID Address(W) = 8’h42(器件地址左移1位,低位补0);Sub-address为8位寄存器地址,在OV7725的数据手册中定义了0x00~0xAC共173个寄存器,有些寄存器是可改写的,有些是只读的,只有可改写的寄存器才能正确写入;Write Data为8位写数据,每一个寄存器地址对应8位的配置数据。上图中的第9位X表示Don’t Care(不必关心位),该位是由从机(此处指OV7725)发出应答信号来响应主机表示当前ID Address、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机(此处指FPGA)可不用判断此处是否有应答,直接默认当前传输完成即可。
我们可以发现,SCCB和IIC写传输协议是极为相似的,只是在SCCB写传输协议中,第9位为不必关心位,而IIC写传输协议为应答位。SCCB的读传输协议和IIC有些差异,在IIC读传输协议中,写完寄存器地址后会有restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,发起总线停止信号,下图为SCCB的读传输协议。
OV7725摄像头RGB1633.png
图 39.1.3 SCCB读传输协议

由上图可知,SCCB读传输协议分为两个部分。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针指向虚写操作中寄存器地址的位置,当然虚写操作也可以通过前面介绍的写传输协议来完成。第二部分是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据,注意ID Address(R)= 8’h43(器件地址左移1位,低位补1)。上图中的NA位由主机(这里指FPGA)产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
在OV7725正常工作前,必须先对传感器进行初始化,即通过配置寄存器使其工作在预期的工作模式,以及得到较好画质的图像。因为SCCB的写传输协议和IIC几乎相同,因此我们可以直接使用IIC的驱动程序来配置摄像头。当然这么多寄存器也并非都需要配置,很多寄存器可以采用默认的值。OV公司提供了OV7725的软件使用手册(位于开发板所随附的资料“7_硬件资料/6_OV7725资料/OV7725软件使用手册.pdf”),如果某些寄存器不知道如何配置可以参考此手册,下表是本程序用到的关键寄存器的配置说明。
表 39.1.1 OV7725关键寄存器配置说明
lQLPJwafJYM3feDNAhHNAoeww8bwvMZWLb4D8JVu4IADAA_647_529.png
lQLPJv_W67RTjGDNAuvNAoewmFOov1eBErID8JWIGwBGAA_647_747.png

OV7725的寄存器较多,对于其它寄存器的描述可以参考OV7725的数据手册(位于开发板所随附的资料“7_硬件资料/6_OV7725资料/OV7725数据手册.pdf”)。
下图为OV7725的一些特性。
OV7725摄像头RGB3457.png
图 39.1.4 OV7725的特性

从上图可以看出,OV7725的输入时钟频率的范围是10Mhz~48Mhz,本次实验摄像头的输入时钟为12 Mhz;SCCB总线的SIO_C的时钟频率最大为400KHz;配置寄存器软件复位(寄存器地址0x12 Bit[7]位)和硬件复位(cam_rst_n引脚)后需要等待最大1ms才能配置其它寄存器;每次配置完寄存器后,需要最大300ms时间的延迟,也就是10帧图像输出的时间才能输出稳定的视频流。
OV7725支持多种不同分辨率图像的输出,包括VGA(640*480)、QVGA(320*240)以及CIF(一种常用的标准化图像格式,分辨率为352*288)到40*30等任意尺寸。可通过寄存器地址0x12(COM7)、0x17(HSTART)、0x18(HSIZE)、0x19(VSTRT)、0x1A(VSIZE)、0x32(HREF)、0x29(HoutSize)、0x2C(VOutSize)、0x2A(EXHCH)来配置输出图像的分辨率。
OV7725支持多种不同的数据像素格式,包括YUV(亮度参量和色度参量分开表示的像素格式)、RGB(其中RGB格式包含RGB565、RGB555等)以及8位的RAW(原始图像数据)和10位的RAW,通过寄存器地址0x12(COM7)配置不同的数据像素格式。
由于摄像头采集的图像最终要在LCD上显示,且ATK-DFPGL22G开发板上的数据接口为RGB888格式(详情请参考“LCD彩条显示实验”章节),因此我们将OV7725摄像头输出的图像像素数据配置成RGB565格式,然后转换为RGB888格式。下图为摄像头输出的VGA帧模式时序图。
OV7725摄像头RGB4217.png
图 39.1.5 VGA帧模式输出时序图

在介绍时序图之前先了解几个基本的概念。
VSYNC:场同步信号,由摄像头输出,用于标志一帧数据的开始与结束。上图中VSYNC的高电平作为一帧的同步信号,在低电平时输出的数据有效。需要注意的是场同步信号是可以通过设置寄存器0x15 Bit[1]位进行取反的,即低电平同步高电平有效,本次实验使用的是和上图一致的默认设置;
HREF/HSYNC:行同步信号,由摄像头输出,用于标志一行数据的开始与结束。上图中的HREF和HSYNC是由同一引脚输出的,只是数据的同步方式不一样。本次实验使用的是HREF格式输出,当HREF为高电平时,图像输出有效,可以通过寄存器0x15 Bit[6]进行配置。本次实验使用的是HREF格式输出;
D[9:0]:数据信号,由摄像头输出,在RGB格式输出中,只有高8位D[9:2]是有效的;
tPCLK:一个像素时钟周期;
tp:单个数据周期,这里需要注意的是上图中左下角红框标注的部分,在RGB模式中,tp代表两个tPCLK(像素时钟)。以RGB565数据格式为例,RGB565采用16bit数据表示一个像素点,而OV7725在一个像素周期(tPCLK)内只能传输8bit数据,因此需要两个时钟周期才能输出一个RGB565数据;
tLine:摄像头输出一行数据的时间,共784个tp,包含640tp个高电平和144tp个低电平,其中640tp为有效像素数据输出的时间。以RGB565数据格式为例,640tp实际上是640*2=1280个tPCLK;
由图 39.1.5可知,VSYNC的上升沿作为一帧的开始,高电平同步脉冲的时间为4*tLine,紧接着等待18*tLine时间后,HREF开始拉高,此时输出有效数据;HREF由640tp个高电平和144tp个低电平构成;输出480行数据之后等待8*tLine时间一帧数据传输结束。所以输出一帧图像的时间实际上是tFrame =(4 + 18 + 480 + 8)*tLine = 510tLine。
本次实验采用OV7725支持的最大分辨率640*480,摄像头的输入时钟为12 Mhz,摄像头的输出时钟为24 Mhz(详细公式请看表 39.1.1),由此我们可以计算出摄像头的输出帧率,以PCLK=24Mhz(周期为42ns)为例,计算出OV7725输出一帧图像所需的时间如下:
一帧图像输出时间:tFrame = 510*tLine = 510*784tp = 510*784*2tPCLK = 799680*42ns =33.5866ms;
摄像头输出帧率:1000ms/33.5866ms ≈ 30Hz。
如果把像素时钟频率提高到摄像头的最大时钟频率48Mhz,通过上述计算方法,摄像头的输出帧率约为60Hz。
下图为OV7725输出RGB565格式的时序图:
OV7725摄像头RGB5507.png
图 39.1.6 RGB565模式时序图

上图中的PCLK为OV7725输出的像素时钟,HREF为行同步信号,D[9:2]为8位像素数据。OV7725最大可以输出10位数据,在RGB565输出模式中,只有高8位是有效的。像素数据在HREF为高电平时有效,第一次输出的数据为RGB565数据的高8位,第二次输出的数据为RGB565数据的低8位,first byte和second byte组成一个16位RGB565数据。由上图可知,数据是在像素时钟的下降沿改变的,为了在数据最稳定的时刻采集图像数据,所以我们需要在像素时钟的上升沿采集数据。
39.2实验任务
本节实验任务是使用ATK-DFPGL22G开发板及OV7725摄像头实现图像采集,并通过RGB-LCD接口驱动RGB-LCD液晶屏(支持目前正点原子推出的所有RGB-LCD屏),并实时显示出图像。
39.3硬件设计
ATK-DFPGL22G开发板上有一个摄像头扩展接口,该接口可以用来连接OV7725/OV5640等摄像头模块,摄像头扩展接口原理图如下所示:
OV7725摄像头RGB6003.png
图 39.3.1 摄像头扩展接口原理图

ATK-OV7725是正点原子推出的一款高性能30W像素高清摄像头模块。该模块通过2*9排针(2.54mm间距)同外部连接,我们将摄像头的排针直接插在开发板上的摄像头接口即可。
前面说过,OV7725在RGB565模式中只有高8位(即D[9:2])数据是有效的,而我们的摄像头排针上数据引脚的个数是8位。实际上,摄像头排针上的8位数据连接的就是OV7725传感器的D[9:2],所以我们直接使用摄像头排针上的8位数据引脚即可。
需要注意的是,由图 39.3.1可知,摄像头扩展口的第18个引脚定义为CMOS_PWDN,而我们的OV7725摄像头模块的PWDN引脚固定为低电平,也就是一直处于正常工作模式。OV7725摄像头模块的第18个引脚定义为SGM_CTRL(CMOS_PWDN),这个引脚是摄像头驱动时钟的选择引脚。OV7725摄像头模块内部自带晶振的,当SGM_CTRL引脚为低电平时,选择使用摄像头的外部时钟,也就是FPGA需要输出时钟给摄像头;当SGM_CTRL引脚为高电平时,选择使用摄像头的晶振提供时钟。本次实验将SGM_CTRL引脚驱动为高电平,这样就不用为摄像头提供驱动时钟,即不用在CMOS_XCLK引脚上输出时钟。
由于LCD接口和DDR3引脚数目较多且在前面相应的章节中已经给出它们的管脚列表,这里只列出摄像头相关管脚分配,如下表所示:
表 39.3.1 OV7725摄像头RGB-LCD显示实验管脚分配
lQLPJxzclU8qguDNAizNArGwbxl8tJFhuU8D8JWoFQD5AA_689_556.png

可以看出本章与之前例程相比,电平标准发生了改变,这里用到的电平标准是LVTTL33(该电平标准为配置引脚时的默认电平标准),用户也可根据需求将其改为LVCMOS33, LVCMOS33和LVTTL33可以直接相互驱动,所以这里没有进行修改,保持默认;LVCMOS和LVTTL的区别在于LVCMOS噪声容限更大,抗噪性更好,而LVTTL噪声容限小,但是速度快。
摄像头约束文件如下:
define_attribute {n:cam_pclk} {PAP_CLOCK_DEDICATED_ROUTE} {FALSE}
define_attribute {p:cam_scl} {PAP_IO_DIRECTION} {Output}
define_attribute {p:cam_scl} {PAP_IO_LOC} {E6}
define_attribute {p:cam_scl} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_scl} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_scl} {PAP_IO_DRIVE} {16}
define_attribute {p:cam_scl} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_scl} {PAP_IO_SLEW} {SLOW}
define_attribute {p:cam_sda} {PAP_IO_DIRECTION} {Inout}
define_attribute {p:cam_sda} {PAP_IO_LOC} {B6}
define_attribute {p:cam_sda} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_sda} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_sda} {PAP_IO_DRIVE} {16}
define_attribute {p:cam_sda} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_sda} {PAP_IO_SLEW} {SLOW}
define_attribute {p:cam_data[0]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[0]} {PAP_IO_LOC} {A6}
define_attribute {p:cam_data[0]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[0]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[0]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[1]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[1]} {PAP_IO_LOC} {B2}
define_attribute {p:cam_data[1]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[1]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[1]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[2]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[2]} {PAP_IO_LOC} {A5}
define_attribute {p:cam_data[2]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[2]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[2]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[3]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[3]} {PAP_IO_LOC} {A2}
define_attribute {p:cam_data[3]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[3]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[3]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[4]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[4]} {PAP_IO_LOC} {F7}
define_attribute {p:cam_data[4]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[4]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[4]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[5]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[5]} {PAP_IO_LOC} {B1}
define_attribute {p:cam_data[5]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[5]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[5]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[6]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[6]} {PAP_IO_LOC} {F8}
define_attribute {p:cam_data[6]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[6]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[6]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_data[7]} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_data[7]} {PAP_IO_LOC} {A1}
define_attribute {p:cam_data[7]} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_data[7]} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_data[7]} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_href} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_href} {PAP_IO_LOC} {F4}
define_attribute {p:cam_href} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_href} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_href} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_pclk} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_pclk} {PAP_IO_LOC} {C1}
define_attribute {p:cam_pclk} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_pclk} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_pclk} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_vsync} {PAP_IO_DIRECTION} {Input}
define_attribute {p:cam_vsync} {PAP_IO_LOC} {G4}
define_attribute {p:cam_vsync} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_vsync} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_vsync} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_rst_n} {PAP_IO_DIRECTION} {Output}
define_attribute {p:cam_rst_n} {PAP_IO_LOC} {D1}
define_attribute {p:cam_rst_n} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_rst_n} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_rst_n} {PAP_IO_SLEW} {SLOW}
define_attribute {p:cam_rst_n} {PAP_IO_DRIVE} {4}
define_attribute {p:cam_rst_n} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_DIRECTION} {Output}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_LOC} {C2}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_VCCIO} {3.3}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_STANDARD} {LVTTL33}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_SLEW} {SLOW}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_DRIVE} {4}
define_attribute {p:cam_sgm_ctrl} {PAP_IO_PULLUP} {TRUE}
cam_pclk是摄像头时钟,因为配置在了普通io口,没有走时钟专用管脚且最终上了时钟网络,所以PDS流程运行到place&route会报错((报错编号为E: Place-0084)),如果将该时钟相关的net或该net的驱动pin脚设置PAP_CLOCK_DEDICATED_ROUTE属性为FLASE,则PDS流程会在place&route中将报错降级为Critical Warning(C: Place-2028)。最终的表现形式为PDS流程在place&route阶段不会报错停止而是以报警告代替。cam_pclk约束语句如下:
define_attribute {n:cam_pclk} {PAP_CLOCK_DEDICATED_ROUTE} {FALSE}
39.4程序设计
OV7725在VGA帧模式下,以RGB565格式输出最高帧率可达60Hz,LCD屏的刷新频率也可以达到60Hz,那么是不是直接将采集到的图像数据连接到LCD屏的输入数据端口就行了呢?答案是不可以。我们在前面说过,OV7725帧率如果要达到60Hz,那么像素时钟频率必须为48Mhz,而LCD屏根据屏的分辨率不同时钟也不同,首先就有时钟不匹配的问题,其次是时序方面的不匹配,LCD屏驱动对时序有着严格的要求。我们在“RGB-LCD彩条显示实验”的章节中可以获知,LCD一行或一场分为四个部分:低电平同步脉冲、显示后沿、有效数据段以及显示前沿,各个部分的时序参数很显然跟OV7725并不是完全一致的。因此必须先把一帧图像缓存下来,然后再把图像数据按照LCD的时序发送到LCD屏上显示。OV7725在VGA帧模式输出下,一帧图像的数据量达到640*480*16bit = 4915200bit = 4800kbit = 4.6875Mbit,带宽为4.6875Mbit*45Hz(帧率)=210.9375 Mbit/S,我们ATK-DFPGL22G开发板芯片型号为PGL22G-6MGB324,从器件手册可以发现,PGL22G-6MGB324的片内存储资源为864Kbit,远不能达到存储要求。因此我们只能使用板载的外部存储器DDR3来缓存图像数据,开发板板载的DDR3容量为4096Mbit(一块DDR3的容量),最大带宽为12.8Gbit/S,足以满足缓存图像数据的需求。
OV7725在正常工作之前必须通过配置寄存器进行初始化,而配置寄存器的SCCB协议和I2C协议在写操作时几乎一样,所以我们需要一个I2C驱动模块。为了使OV7725在期望的模式下运行并且提高图像显示效果,需要配置较多的寄存器,这么多寄存器的地址与参数需要单独放在一个模块,因此还需要一个寄存配置信息的I2C配置模块。在摄像头配置完成后,开始输出图像数据,因此需要一个摄像头图像采集模块来采集图像;外接DDR3存储器当然离不开DDR3控制器模块的支持,最后LCD顶层模块读取DDR3缓存的数据以达到最终实时显示的效果。
OV7725摄像头RGB13426.png
图 39.4.1 程序结构框图

由上图可知,时钟模块为LCD顶层模块、DDR3控制模块以及I2C驱动模块提供驱动时钟。I2C配置模块和I2C驱动模块控制着传感器初始化的开始与结束,传感器初始化完成后图像采集模块将采集到的数据写入DDR控制模块,LCD顶层模块从DDR3控制模块中读出数据,完成了数据的采集、缓存与显示。需要注意的是图像数据采集模块是在DDR3和传感器都初始化完成之后才开始输出数据的,避免了在DDR3初始化过程中向里面写入数据。
顶层模块代码如下:
1    module ov7725_lcd(
2        input             sys_clk          ,
3        input             sys_rst_n        ,
4        //lcd接口                        
5        output            lcd_hs           ,  //LCD 行同步信号
6        output            lcd_vs           ,  //LCD 场同步信号
7        output            lcd_de           ,  //LCD 数据输入使能
8        inout  [23:0]     lcd_rgb          ,  //LCD 颜色数据
9        output            lcd_bl           ,  //LCD 背光控制信号
10       output            lcd_rst          ,  //LCD 复位信号
11       output            lcd_pclk         ,  //LCD 采样时钟
12       //摄像头接口                       
13       input             cam_pclk         ,  //cmos 数据像素时钟
14       input             cam_vsync        ,  //cmos 场同步信号
15       input             cam_href         ,  //cmos 行同步信号
16       input  [7:0]      cam_data         ,  //cmos 数据
17       output            cam_rst_n        ,  //cmos 复位信号,低电平有效
18       output            cam_sgm_ctrl     ,  //cmos 时钟选择信号, 1:使用摄像头自带的晶振
19       output            cam_scl          ,  //cmos SCCB_SCL线
20       inout             cam_sda          ,  //cmos SCCB_SDA线
21       //DDR3接口
22       input             pad_loop_in      ,  //低位温度补偿输入
23       input             pad_loop_in_h    ,  //高位温度补偿输入
24       output            pad_rstn_ch0     ,  //Memory复位
25       output            pad_ddr_clk_w    ,  //Memory差分时钟正
26       output            pad_ddr_clkn_w   ,  //Memory差分时钟负
27       output            pad_csn_ch0      ,  //Memory片选
28       output [15:0]     pad_addr_ch0     ,  //Memory地址总线
29       inout  [16-1:0]   pad_dq_ch0       ,  //数据总线
30       inout  [16/8-1:0] pad_dqs_ch0      ,  //数据时钟正端
31       inout  [16/8-1:0] pad_dqsn_ch0     ,  //数据时钟负端
32       output [16/8-1:0] pad_dm_rdqs_ch0  ,  //数据Mask
33       output            pad_cke_ch0      ,  //Memory差分时钟使
34       output            pad_odt_ch0      ,  //On Die Terminati
35       output            pad_rasn_ch0     ,  //行地址strobe
36       output            pad_casn_ch0     ,  //列地址strobe
37       output            pad_wen_ch0      ,  //写使能
38       output [2:0]      pad_ba_ch0       ,  //Bank地址总线
39       output            pad_loop_out     ,  //低位温度补偿输出
40       output            pad_loop_out_h      //高位温度补偿输出
41      );
42   
43   //parameter define
44   parameter SLAVE_ADDR = 7'h21         ; //OV7725的器件地址7'h21
45   parameter BIT_CTRL   = 1'b0          ; //OV7725的字节地址为8位  0:8位 1:16位
46   parameter CLK_FREQ   = 27'd50_000_000; //i2c_dri模块的驱动时钟频率
47   parameter I2C_FREQ   = 18'd250_000   ; //I2C的SCL时钟频率,不超过400KHz
48   parameter APP_ADDR_MIN = 28'd0       ; //ddr3读写起始地址,以一个16bit的数据为一个单位
49   parameter BURST_LENGTH = 8'd64       ; //ddr3读写突发长度,64个128bit的数据
50   
顶层模块中第46至第47行定义了两个变量:CLK_FREQ(i2c_dri模块的驱动时钟频率)和I2C_FREQ(I2C的SCL时钟频率),I2C_FREQ的时钟频率不能超过400KHz,否则有可能导致摄像头配置不成功。
51   //wire define
52   wire        sys_init_done   ;  //系统初始化完成(DDR3初始化+摄像头初始化)
53   wire        rst_n           ;  //全局复位
54   //PLL
55   wire        clk_50m         ;  //output 50M
56   wire        clk_100m        ;  //output 100M
57   wire        clk_locked      ;
58   //OV7725
59   wire        i2c_dri_clk     ;  //I2C操作时钟
60   wire        i2c_done        ;  //I2C寄存器配置完成信号
61   wire        i2c_exec        ;  //I2C触发执行信号
62   wire [15:0] i2c_data        ;  //I2C要配置的地址与数据(高8位地址,低8位数据)
63   wire        cam_init_done   ;  //摄像头初始化完成
64   wire        cmos_frame_vsync;  //帧有效信号
65   wire        cmos_frame_href ;  //行有效信号
66   wire        cmos_frame_valid;  //数据有效使能信号
67   wire [15:0] wr_data         ;  //OV7725写入DDR3控制器模块的数据
68   wire [27:0] ddr3_addr_max   ;  //存入DDR3的最大读写地址
69   //LCD                       
70   wire        lcd_clk         ;  //分频产生的LCD 采样时钟
71   wire [10:0] h_disp          ;  //LCD屏水平分辨率
72   wire [10:0] v_disp          ;  //LCD屏垂直分辨率
73   wire [15:0] lcd_id          ;  //LCD屏的ID号
74   wire        out_vsync       ;  //LCD场信号
75   wire        rdata_req       ;  //读数据请求
76   //DDR3
77   wire [15:0] rd_data         ;  //DDR3控制器模块输出的数据
78   wire        fram_done       ;  //DDR中已经存入一帧画面标志
79   wire        ddr_init_done   ;  //ddr3初始化完成
80   
81   //*****************************************************
82   //**                    main code
83   //*****************************************************
84   
85   //待时钟锁定后产生结束复位信号
86   assign  rst_n = sys_rst_n & clk_locked;
87   
88   //系统初始化完成:DDR3和摄像头都初始化完成
89   //避免了在DDR3初始化过程中向里面写入数据
90   assign  sys_init_done = ddr_init_done & cam_init_done;
91   
92   //cmos 时钟选择信号, 1:使用摄像头自带的晶振
93   assign  cam_sgm_ctrl = 1'b1;
94   
95   //不对摄像头硬件复位,固定高电平
96   assign  cam_rst_n    = 1'b1;
97   
98   //例化PLL IP核
99   pll_clk  u_pll_clk(
100      .pll_rst        (~sys_rst_n  ),
101      .clkin1         (sys_clk     ),
102      .clkout0        (clk_50m     ),
103      .clkout1        (clk_100m    ),
104      .pll_lock       (clk_locked  )
105  );
106  
107  //I2C配置模块   
108  i2c_ov7725_rgb565_cfg  u_i2c_cfg(
109      .clk            (i2c_dri_clk  ), //in
110      .rst_n          (rst_n        ), //in
111      .i2c_done       (i2c_done     ), //in
112      .i2c_exec       (i2c_exec     ), //out
113      .i2c_data       (i2c_data     ), //out
114      .init_done      (cam_init_done)  //out
115      );   
116  
117  //I2C驱动模块
118  i2c_dri
119     #(
120      .SLAVE_ADDR     (SLAVE_ADDR),  //参数传递
121      .CLK_FREQ       (CLK_FREQ  ),
122      .I2C_FREQ       (I2C_FREQ  )
123      )               
124     u_i2c_dri(      
125      .clk            (clk_50m       ),
126      .rst_n          (rst_n         ),
127      //i2c interface
128      .i2c_exec       (i2c_exec      ),
129      .bit_ctrl       (BIT_CTRL      ),
130      .i2c_rh_wl      (1'b0          ), //固定为0,只用到了IIC驱动的写操作
131      .i2c_addr       (i2c_data[15:8]),
132      .i2c_data_w     (i2c_data[7:0] ),
133      .i2c_data_r     (              ), //out
134      .i2c_done       (i2c_done      ), //out
135      .i2c_ack        (              ), //out
136      .scl            (cam_scl       ), //out
137      .sda            (cam_sda       ), //inout
138      //user interface
139      .dri_clk        (i2c_dri_clk   )  //I2C操作时钟//out
140  );
141  
142  //图像采集顶层模块
143  cmos_data_top u_cmos_data_top(
144      .rst_n                 (rst_n & sys_init_done), //系统初始化完成之后再开始采集数据
145      .cam_pclk              (cam_pclk             ),
146      .cam_vsync             (cam_vsync            ),
147      .cam_href              (cam_href             ),
148      .cam_data              (cam_data             ),
149      .lcd_id                (lcd_id               ), //in
150      .h_disp                (h_disp               ), //in
151      .v_disp                (v_disp               ), //in
152      .h_pixel               (                     ),
153      .v_pixel               (                     ),
154      .ddr3_addr_max         (ddr3_addr_max        ), //out
155      .cmos_frame_vsync      (cmos_frame_vsync     ),
156      .cmos_frame_href       (cmos_frame_href      ),
157      .cmos_frame_valid      (cmos_frame_valid     ), //数据有效使能信号
158      .cmos_frame_data       (wr_data              )  //有效数据
159      );
160  
在程序的第157和第158行,CMOS图像数据采集模块输出的cmos_frame_valid(数据有效使能信号)和wr_data(有效数据)连接到DDR3控制模块,实现了图像数据的缓存。
161  //ddr3
162  ddr3_top u_ddr3_top(
163      .refclk_in             (clk_50m         ),
164      .rst_n                 (rst_n           ),
165      .app_addr_rd_min       (APP_ADDR_MIN    ),
166      .app_addr_rd_max       (ddr3_addr_max   ),
167      .rd_bust_len           (BURST_LENGTH    ),
168      .app_addr_wr_min       (APP_ADDR_MIN    ),
169      .app_addr_wr_max       (ddr3_addr_max   ),
170      .wr_bust_len           (BURST_LENGTH    ),
171      .ddr3_read_valid       (1'b1            ),
172      .ddr3_pingpang_en      (1'b1            ),
173      .wr_clk                (cam_pclk        ),
174      .rd_clk                (lcd_clk         ),
175      .datain_valid          (cmos_frame_valid),
176      .datain                (wr_data         ),
177      .rdata_req             (rdata_req       ),
178      .rd_load               (out_vsync       ),
179      .wr_load               (cmos_frame_vsync),
180      .fram_done             (fram_done       ),
181      .dataout               (rd_data         ),
182      .pll_lock              (pll_lock        ),
183      .ddr_init_done         (ddr_init_done   ),
184      .ddrphy_rst_done       (                ),
185      .pad_loop_in           (pad_loop_in     ),
186      .pad_loop_in_h         (pad_loop_in_h   ),
187      .pad_rstn_ch0          (pad_rstn_ch0    ),
188      .pad_ddr_clk_w         (pad_ddr_clk_w   ),
189      .pad_ddr_clkn_w        (pad_ddr_clkn_w  ),
190      .pad_csn_ch0           (pad_csn_ch0     ),
191      .pad_addr_ch0          (pad_addr_ch0    ),
192      .pad_dq_ch0            (pad_dq_ch0      ),
193      .pad_dqs_ch0           (pad_dqs_ch0     ),
194      .pad_dqsn_ch0          (pad_dqsn_ch0    ),
195      .pad_dm_rdqs_ch0       (pad_dm_rdqs_ch0 ),
196      .pad_cke_ch0           (pad_cke_ch0     ),
197      .pad_odt_ch0           (pad_odt_ch0     ),
198      .pad_rasn_ch0          (pad_rasn_ch0    ),
199      .pad_casn_ch0          (pad_casn_ch0    ),
200      .pad_wen_ch0           (pad_wen_ch0     ),
201      .pad_ba_ch0            (pad_ba_ch0      ),
202      .pad_loop_out          (pad_loop_out    ),
203      .pad_loop_out_h        (pad_loop_out_h  )
204      
205      );  
206  
在程序的第167和第170行,信号(rd_bust_len)和信号(wr_bust_len)表示一次向DDR读或写的长度,这里长度我们设置为64(2的6次方),因为64*8可以被最大读写地址整除,能够匹配正点原子在售的所有RGB-LCD屏,这里建议突发长度设置为2的整数次方,其他突发长度可能会出现数据反卷,反卷现象的详细说明可以查看“DDR3读写测试实验”的程序设计部分。
207  //LCD驱动显示模块
208  lcd_rgb_top  u_lcd_rgb_top(
209      .sys_clk               (clk_50m             ),
210      .clk_100m              (clk_100m            ),
211      .sys_rst_n             (rst_n && clk_locked ),
212      .sys_init_done         (sys_init_done       ),
213      //lcd接口
214      .lcd_id                (lcd_id              ), //LCD屏的ID号
215      .lcd_hs                (lcd_hs              ), //LCD 行同步信号
216      .lcd_vs                (lcd_vs              ), //LCD 场同步信号
217      .lcd_de                (lcd_de              ), //LCD 数据输入使能
218      .lcd_rgb               (lcd_rgb             ), //LCD 颜色数据
219      .lcd_bl                (lcd_bl              ), //LCD 背光控制信号
220      .lcd_rst               (lcd_rst             ), //LCD 复位信号
221      .lcd_pclk              (lcd_pclk            ), //LCD 采样时钟
222      .lcd_clk               (lcd_clk             ), //LCD 驱动时钟
223      //用户接口                                 
224      .h_disp                (h_disp              ), //行分辨率
225      .v_disp                (v_disp              ), //场分辨率
226      .pixel_xpos            (                    ),
227      .pixel_ypos            (                    ),
228      .out_vsync             (out_vsync           ),
229      .data_in               (rd_data             ), //rfifo输出数据
230      .data_req              (rdata_req           )  //请求数据输入
231      );
232  
233  endmodule
在程序的第224和第225行,LCD顶层模块引出了h_disp和v_disp信号,并将其引入摄像头图像采集模块。因为LCD屏的分辨率是不一样的,而本次实验是驱动自适应不同分辨率的LCD屏,所以将这两个信号引入摄像头图像采集模块是为了与摄像头采集的分辨率进行比较。当摄像头的分辨率大时,就取LCD屏分辨率大小的数据存入DDR中,其余数据丢弃,当摄像头的分辨率小时,就将摄像头采集的数据存入DDR中,LCD屏其他显示区域填充黑色。
在程序的230行,LCD顶层模块输出rdata_req(请求像素点颜色数据输入)连接到DDR控制模块, 实现了图像数据的读取,将读出的数据连接到LCD顶层模块的data_in信号,从而实现了LCD实时显示的功能。
FPGA顶层模块(ov7725_lcd)例化了以下六个模块:时钟模块(pll_clk)、I2C驱动模块(i2c_dri)、I2C配置模块(i2c_ov7725_rgb565_cfg)、图像采集顶层模块(cmos_data_top)、ddr3控制模块(ddr3_top)和LCD顶层模块(lcd_rgb_top)。
时钟模块(pll_clk):时钟模块通过调用PLL IP核实现,共输出2个时钟,频率分别为50Mhz和100M时钟。100Mhz时钟给lcd的时钟分频模块使用,50Mhz时钟作为ddr3控制模块,I2C驱动模块和LCD顶层模块的驱动时钟。
I2C驱动模块(i2c_dri):I2C驱动模块负责驱动OV7725 SCCB接口总线,用户根据该模块提供的用户接口可以很方便的对OV7725的寄存器进行配置,该模块和“EEPROM读写实验”章节中用到的I2C驱动模块为同一个模块,有关该模块的详细介绍请大家参考“EEPROM读写实验”章节。
I2C配置模块(i2c_ov7725_rgb565_cfg):I2C配置模块的驱动时钟是由I2C驱动模块输出的时钟提供的,这样方便了I2C驱动模块和I2C配置模块之间的数据交互。该模块寄存需要配置的寄存器地址、数据以及控制初始化的开始与结束,同时该模块输出OV7725的寄存器地址和数据以及控制I2C驱动模块开始执行的控制信号,直接连接到I2C驱动模块的用户接口,从而完成对OV7725传感器的初始化。
图像采集顶层模块(cmos_data_top):摄像头采集模块在像素时钟的驱动下将传感器输出的场同步信号、行同步信号以及8位数据转换成DDR控制模块的写使能信号和16位写数据信号,完成对OV7725传感器图像的采集。如果LCD屏的分辨率小于OV7725的分辨率,还要对OV7725采集的数据进行裁剪,以匹配LCD屏的分辨率。
ddr3控制模块(ddr3_top):ddr3控制模块负责驱动DDR3片外存储器,缓存图像传感器输出的图像数据。该模块将DDR3复杂的读写操作封装成类似FIFO的用户接口,非常方便用户的使用。在“DDR3读写测试实验”的程序中,读写操作地址都是DDR的同一存储空间,如果只使用一个存储空间缓存图像数据,那么同一存储空间中会出现两帧图像叠加的情况,为了避免这一情况,我们在DDR3的其它BANK中开辟了三个相同大小的存储空间,使用乒乓操作的方式来写入和读取数据,所以本次实验在“DDR3读写测试实验”的程序里做了一个小小的改动,有关该模块的详细介绍请大家参考“DDR3读写测试实验”章节,本章只对改动的地方作介绍。
LCD顶层模块(lcd_rgb_top):LCD顶层模块负责驱动LCD屏的驱动信号的输出,同时为其他模块提供屏体参数、场同步信号和数据请求信号。
I2C配置模块寄存需要配置的寄存器地址、数据以及控制初始化的开始与结束,代码如下所示:
1    module i2c_ov7725_rgb565_cfg(  
2        input                clk      ,  //时钟信号
3        input                rst_n    ,  //复位信号,低电平有效
4        
5        input                i2c_done ,  //I2C寄存器配置完成信号
6        output  reg          i2c_exec ,  //I2C触发执行信号
7        output  reg  [15:0]  i2c_data ,  //I2C要配置的地址与数据(高8位地址,低8位数据)
8        output  reg          init_done   //初始化完成信号
9        );
10   
11   //parameter define
12   parameter  REG_NUM = 7'd70   ;       //总共需要配置的寄存器个数
13   
在代码的第12行定义了总共需要配置的寄存器的个数,如果增加或者删减了寄存器的配置,需要修改此参数。
14   //reg define
15   reg    [9:0]   start_init_cnt;       //等待延时计数器
16   reg    [6:0]   init_reg_cnt  ;       //寄存器配置个数计数器
17   
18   //*****************************************************
19   //**                    main code
20   //*****************************************************
21   
22   //cam_scl配置成250khz,输入的clk为1Mhz,周期为1us,1023*1us = 1.023ms
23   //寄存器延时配置
24   always @(posedge clk or negedge rst_n) begin
25       if(!rst_n)
26           start_init_cnt <= 10'b0;
27       else if((init_reg_cnt == 7'd1) && i2c_done)
28           start_init_cnt <= 10'b0;
29       else if(start_init_cnt < 10'd1023) begin
30           start_init_cnt <= start_init_cnt + 1'b1;
31       end
32   end
33   
图像传感器刚开始上电时电压有可能不够稳定,所以程序中的23行至32行定义了一个延时计数器(start_init_cnt)等待传感器工作在稳定的状态。当计数器计数到预设值之后,开始第一次配置传感器即软件复位,目的是让所有的寄存器复位到默认的状态。从前面介绍的OV7725的特性可知,软件复位需要等待1ms的时间才能配置其它的寄存器,因此发送完软件复位命令后,延时计数器清零,并重新开始计数。当计数器计数到预设值之后,紧接着配置剩下的寄存器。只有软件复位命令需要1ms的等待时间,其它寄存器不需要等待时间,直接按照程序中定义的顺序发送即可。
34   //寄存器配置个数计数
35   always @(posedge clk or negedge rst_n) begin
36       if(!rst_n)
37           init_reg_cnt <= 7'd0;
38       else if(i2c_exec)
39           init_reg_cnt <= init_reg_cnt + 7'b1;
40   end
41   
42   //i2c触发执行信号
43   always @(posedge clk or negedge rst_n) begin
44       if(!rst_n)
45           i2c_exec <= 1'b0;
46       else if(start_init_cnt == 10'd1022)
47           i2c_exec <= 1'b1;
48       //只有刚上电和配置第一个寄存器增加延时
49       else if(i2c_done && (init_reg_cnt != 7'd1) && (init_reg_cnt < REG_NUM))
50           i2c_exec <= 1'b1;
51       else
52           i2c_exec <= 1'b0;
53   end
54   
55   //初始化完成信号
56   always @(posedge clk or negedge rst_n) begin
57       if(!rst_n)
58           init_done <= 1'b0;
59       else if((init_reg_cnt == REG_NUM) && i2c_done)
60           init_done <= 1'b1;
61   end
62   
63   //配置寄存器地址与数据
64   always @(posedge clk or negedge rst_n) begin
65       if(!rst_n)
66           i2c_data <= 16'b0;
67       else begin
68           case(init_reg_cnt)
69               //先对寄存器进行软件复位,使寄存器恢复初始值
70               //寄存器软件复位后,需要延时1ms才能配置其它寄存器
71               7'd0  : i2c_data <= {8'h12, 8'h80}; //COM7 BIT[7]:复位所有的寄存器
72               7'd1  : i2c_data <= {8'h3d, 8'h03}; //COM12 模拟过程直流补偿
73               7'd2  : i2c_data <= {8'h15, 8'h00}; //COM10 href/vsync/pclk/data信号控制
74               7'd3  : i2c_data <= {8'h17, 8'h23}; //HSTART 水平起始位置
75               7'd4  : i2c_data <= {8'h18, 8'ha0}; //HSIZE 水平尺寸
76               7'd5  : i2c_data <= {8'h19, 8'h07}; //VSTRT 垂直起始位置
77               7'd6  : i2c_data <= {8'h1a, 8'hf0}; //VSIZE 垂直尺寸            
78               7'd7  : i2c_data <= {8'h32, 8'h00}; //HREF 图像开始和尺寸控制,控制低位
79               7'd8  : i2c_data <= {8'h29, 8'ha0}; //HOutSize 水平输出尺寸
80               7'd9  : i2c_data <= {8'h2a, 8'h00}; //EXHCH 虚拟像素MSB
81               7'd10 : i2c_data <= {8'h2b, 8'h00}; //EXHCL 虚拟像素LSB
82               7'd11 : i2c_data <= {8'h2c, 8'hf0}; //VOutSize 垂直输出尺寸
83               7'd12 : i2c_data <= {8'h0d, 8'h41}; //COM4 PLL倍频设置(multiplier)
84                                                   //Bit[7:6]:  0:1x 1:4x 2:6x 3:8x
85               7'd13 : i2c_data <= {8'h11, 8'h00}; //CLKRC 内部时钟配置
86                                                   //Freq=multiplier/[(CLKRC[5:0]+1)*2]
87               7'd14 : i2c_data <= {8'h12, 8'h06}; //COM7 输出VGA RGB565格式
88               7'd15 : i2c_data <= {8'h0c, 8'h10}; //COM3 Bit[0]: 0:图像数据 1:彩条测试
89               //DSP 控制
90               7'd16 : i2c_data <= {8'h42, 8'h7f}; //TGT_B 黑电平校准蓝色通道目标值
91               7'd17 : i2c_data <= {8'h4d, 8'h09}; //FixGain 模拟增益放大器
92               7'd18 : i2c_data <= {8'h63, 8'hf0}; //AWB_Ctrl0 自动白平衡控制字节0
93               7'd19 : i2c_data <= {8'h64, 8'hff}; //DSP_Ctrl1 DSP控制字节1
94               7'd20 : i2c_data <= {8'h65, 8'h00}; //DSP_Ctrl2 DSP控制字节2
95               7'd21 : i2c_data <= {8'h66, 8'h00}; //DSP_Ctrl3 DSP控制字节3
96               7'd22 : i2c_data <= {8'h67, 8'h00}; //DSP_Ctrl4 DSP控制字节4
97               //AGC AEC AWB
98               //COM8 Bit[2]:自动增益使能 Bit[1]:自动白平衡使能 Bit[0]:自动曝光功能
99               7'd23 : i2c_data <= {8'h13, 8'hff}; //COM8
100              7'd24 : i2c_data <= {8'h0f, 8'hc5}; //COM6
101              7'd25 : i2c_data <= {8'h14, 8'h11};
102              7'd26 : i2c_data <= {8'h22, 8'h98};
103              7'd27 : i2c_data <= {8'h23, 8'h03};
104              7'd28 : i2c_data <= {8'h24, 8'h40};
105              7'd29 : i2c_data <= {8'h25, 8'h30};
106              7'd30 : i2c_data <= {8'h26, 8'ha1};
107              7'd31 : i2c_data <= {8'h6b, 8'haa};
108              7'd32 : i2c_data <= {8'h13, 8'hff};
109              //matrix sharpness brightness contrast UV
110              7'd33 : i2c_data <= {8'h90, 8'h0a}; //EDGE1 边缘增强控制1
111              //DNSOff 降噪阈值下限,仅在自动模式下有效
112              7'd34 : i2c_data <= {8'h91, 8'h01}; //DNSOff
113              7'd35 : i2c_data <= {8'h92, 8'h01}; //EDGE2 锐度(边缘增强)强度上限
114              7'd36 : i2c_data <= {8'h93, 8'h01}; //EDGE3 锐度(边缘增强)强度下限
115              7'd37 : i2c_data <= {8'h94, 8'h5f}; //MTX1 矩阵系数1
116              7'd38 : i2c_data <= {8'h95, 8'h53}; //MTX1 矩阵系数2
117              7'd39 : i2c_data <= {8'h96, 8'h11}; //MTX1 矩阵系数3
118              7'd40 : i2c_data <= {8'h97, 8'h1a}; //MTX1 矩阵系数4
119              7'd41 : i2c_data <= {8'h98, 8'h3d}; //MTX1 矩阵系数5
120              7'd42 : i2c_data <= {8'h99, 8'h5a}; //MTX1 矩阵系数6
121              7'd43 : i2c_data <= {8'h9a, 8'h1e}; //MTX_Ctrl 矩阵控制
122              7'd44 : i2c_data <= {8'h9b, 8'h3f}; //BRIGHT 亮度
123              7'd45 : i2c_data <= {8'h9c, 8'h25}; //CNST 对比度
124              7'd46 : i2c_data <= {8'h9e, 8'h81};
125              7'd47 : i2c_data <= {8'ha6, 8'h06}; //SDE 特殊数字效果控制
126              7'd48 : i2c_data <= {8'ha7, 8'h65}; //USAT "U"饱和增益
127              7'd49 : i2c_data <= {8'ha8, 8'h65}; //VSAT "V"饱和增益
128              7'd50 : i2c_data <= {8'ha9, 8'h80}; //VSAT "V"饱和增益
129              7'd51 : i2c_data <= {8'haa, 8'h80}; //VSAT "V"饱和增益
130              //伽马控制
131              7'd52 : i2c_data <= {8'h7e, 8'h0c};
132              7'd53 : i2c_data <= {8'h7f, 8'h16};
133              7'd54 : i2c_data <= {8'h80, 8'h2a};
134              7'd55 : i2c_data <= {8'h81, 8'h4e};
135              7'd56 : i2c_data <= {8'h82, 8'h61};
136              7'd57 : i2c_data <= {8'h83, 8'h6f};
137              7'd58 : i2c_data <= {8'h84, 8'h7b};
138              7'd59 : i2c_data <= {8'h85, 8'h86};
139              7'd60 : i2c_data <= {8'h86, 8'h8e};
140              7'd61 : i2c_data <= {8'h87, 8'h97};
141              7'd62 : i2c_data <= {8'h88, 8'ha4};
142              7'd63 : i2c_data <= {8'h89, 8'haf};
143              7'd64 : i2c_data <= {8'h8a, 8'hc5};
144              7'd65 : i2c_data <= {8'h8b, 8'hd7};
145              7'd66 : i2c_data <= {8'h8c, 8'he8};
146              7'd67 : i2c_data <= {8'h8d, 8'h20};
147              
148              7'd68 : i2c_data <= {8'h0e, 8'h65}; //COM5
149              7'd69 : i2c_data <= {8'h09, 8'h00}; //COM2  Bit[1:0] 输出电流驱动能力
150              //只读存储器,防止在case中没有列举的情况,之前的寄存器被重复改写
151              default:i2c_data <=    {8'h1C, 8'h7F}; //MIDH 制造商ID 高8位
152          endcase
153      end
154  end
155  
156  endmodule
在程序的83至86行,说明了关于摄像头输出时钟的寄存器配置,摄像头的寄存器地址0x0d配置成0x41,表示PLL倍频设置设为了4倍频,摄像头的寄存器地址0x11配置成0x00,而摄像头的输入时钟为12M,所以根据第86行的公式可得到摄像头的输出时钟为24M。
图像采集顶层模块的代码如下:
1   module cmos_data_top(
2       input                 rst_n            ,  //复位信号
3       input       [15:0]    lcd_id           ,  //LCD屏的ID号
4       input       [10:0]    h_disp           ,  //LCD屏水平分辨率
5       input       [10:0]    v_disp           ,  //LCD屏垂直分辨率
6       //摄像头接口
7       input                 cam_pclk         ,  //cmos 数据像素时钟
8       input                 cam_vsync        ,  //cmos 场同步信号
9       input                 cam_href         ,  //cmos 行同步信号
10      input  [7:0]          cam_data         ,  //cmos 数据
11      //用户接口
12      output      [10:0]    h_pixel          ,  //存入ddr3的水平分辨率
13      output      [10:0]    v_pixel          ,  //存入ddr3的屏垂直分辨率
14      output      [27:0]    ddr3_addr_max    ,  //存入DDR3的最大读写地址
15      output                cmos_frame_vsync ,  //帧有效信号
16      output                cmos_frame_href  ,  //行有效信号
17      output                cmos_frame_valid ,  //数据有效使能信号
18      output       [15:0]   cmos_frame_data     //有效数据
19      );
20  
21  //wire define
22  wire  [10:0] h_pixel       ;  //存入ddr3的水平分辨率
23  wire  [10:0] v_pixel       ;  //存入ddr3的屏垂直分辨率
24  wire  [15:0] lcd_id_a      ;  //时钟同步后的LCD屏的ID号
25  wire  [27:0] ddr3_addr_max ;  //存入DDR3的最大读写地址
26  wire  [15:0] wr_data_tailor;  //经过裁剪的摄像头数据
27  wire  [15:0] wr_data       ;  //没有经过裁剪的摄像头数据
28  
29  //*****************************************************
30  //**                    main code
31  //*****************************************************
32  
33  assign cmos_frame_valid = (lcd_id_a == 16'h4342) ? data_valid_tailor : data_valid ;
34  assign cmos_frame_data = (lcd_id_a == 16'h4342) ? wr_data_tailor : wr_data ;
35  
36  //摄像头数据裁剪模块
37  cmos_tailor  u_cmos_tailor(
38      .rst_n                 (rst_n            ),
39      .lcd_id                (lcd_id           ),
40      .lcd_id_a              (lcd_id_a         ),
41      .cam_pclk              (cam_pclk         ),
42      .cam_vsync             (cmos_frame_vsync ),
43      .cam_href              (cmos_frame_href  ),
44      .cam_data              (wr_data          ),
45      .cam_data_valid        (data_valid       ),
46      .h_disp                (h_disp           ),
47      .v_disp                (v_disp           ),
48      .h_pixel               (h_pixel          ),
49      .v_pixel               (v_pixel          ),
50      .ddr3_addr_max         (ddr3_addr_max    ),
51      .cmos_frame_valid      (data_valid_tailor),
52      .cmos_frame_data       (wr_data_tailor   )
53  
54  );
55  
56  //摄像头数据采集模块
57  cmos_capture_data u_cmos_capture_data(
58      .rst_n                 (rst_n           ),
59      .cam_pclk              (cam_pclk        ),
60      .cam_vsync             (cam_vsync       ),
61      .cam_href              (cam_href        ),
62      .cam_data              (cam_data        ),
63      .cmos_frame_vsync      (cmos_frame_vsync),
64      .cmos_frame_href       (cmos_frame_href ),
65      .cmos_frame_valid      (data_valid      ),
66      .cmos_frame_data       (wr_data         )
67      );
68  
69  endmodule
图像采集顶层模块(cmos_data_ top)例化了以下二个模块:图像采集模块(cmos_capture_data)、图像裁剪模块(cmos_tailor)。
图像采集模块(cmos_capture_data)为其他模块提供摄像头8bit输入数据转化后的16bit数据和数据使能以及摄像头稳定后的行场信号。图像裁剪模块(cmos_tailor)只有在LCD的器件ID为16’h4342时起作用,即摄像头的分辨率大于LCD屏的分辨率,起到裁剪图像数据,使图像的有效数据达到匹配LCD屏的尺寸。
在程序中的33行至34行,定义了两个信号cmos_frame_valid和cmos_frame_data,这两个信号的输出是根据LCD的器件ID来选择的,当摄像头的分辨率大于LCD的分辨率时,将裁剪后的数据和数据使能赋给这两个信号,反之将没有裁剪的信号和使能赋给这两个信号。
摄像头接口输出8位像素数据,而本次实验配置摄像头的模式是RGB565,所以需要在图像采集模块实现8位数据转16位数据的功能。
CMOS图像数据采集模块的代码如下所示:
1    module cmos_capture_data(
2        input                 rst_n            ,  //复位信号
3        //摄像头接口                           
4        input                 cam_pclk         ,  //cmos 数据像素时钟
5        input                 cam_vsync        ,  //cmos 场同步信号
6        input                 cam_href         ,  //cmos 行同步信号
7        input  [7:0]          cam_data         ,
8        //用户接口                              
9        output                cmos_frame_vsync ,  //帧有效信号
10       output                cmos_frame_href  ,  //行有效信号
11       output                cmos_frame_valid ,  //数据有效使能信号
12       output       [15:0]   cmos_frame_data     //有效数据
13       );
14   
15   //寄存器全部配置完成后,先等待10帧数据
16   //待寄存器配置生效后再开始采集图像
17   //parameter define
18   parameter  WAIT_FRAME = 4'd10    ;  //寄存器数据稳定等待的帧个数
19   
20   //reg define
21   reg             cam_vsync_d0     ;
22   reg             cam_vsync_d1     ;
23   reg             cam_href_d0      ;
24   reg             cam_href_d1      ;
25   reg    [3:0]    cmos_ps_cnt      ;  //等待帧数稳定计数器
26   reg    [7:0]    cam_data_d0      ;
27   reg    [15:0]   cmos_data_t      ;  //用于8位转16位的临时寄存器
28   reg             byte_flag        ;  //16位RGB数据转换完成的标志信号
29   reg             byte_flag_d0     ;
30   reg             frame_val_flag   ;  //帧有效的标志
31   
32   //wire define
33   wire            pos_vsync        ;  //采输入场同步信号的上升沿
34   
35   //*****************************************************
36   //**                    main code
37   //*****************************************************
38   
39   //采输入场同步信号的上升沿
40   assign pos_vsync = (~cam_vsync_d1) & cam_vsync_d0;
41   
42   //输出帧有效信号
43   assign  cmos_frame_vsync = frame_val_flag  ?  cam_vsync_d1  :  1'b0;
44   
45   //输出行有效信号
46   assign  cmos_frame_href  = frame_val_flag  ?  cam_href_d1   :  1'b0;
47   
48   //输出数据使能有效信号
49   assign  cmos_frame_valid = frame_val_flag  ?  byte_flag_d0  :  1'b0;
50   
51   //输出数据
52   assign  cmos_frame_data  = frame_val_flag  ?  cmos_data_t   :  1'b0;
53   
54   always @(posedge cam_pclk or negedge rst_n) begin
55       if(!rst_n) begin
56           cam_vsync_d0 <= 1'b0;
57           cam_vsync_d1 <= 1'b0;
58           cam_href_d0 <= 1'b0;
59           cam_href_d1 <= 1'b0;
60       end
61       else begin
62           cam_vsync_d0 <= cam_vsync;
63           cam_vsync_d1 <= cam_vsync_d0;
64           cam_href_d0 <= cam_href;
65           cam_href_d1 <= cam_href_d0;
66       end
67   end
68   
69   //对帧数进行计数
70   always @(posedge cam_pclk or negedge rst_n) begin
71       if(!rst_n)
72           cmos_ps_cnt <= 4'd0;
73       else if(pos_vsync && (cmos_ps_cnt < WAIT_FRAME))
74           cmos_ps_cnt <= cmos_ps_cnt + 4'd1;
75   end
76   
77   //帧有效标志
78   always @(posedge cam_pclk or negedge rst_n) begin
79       if(!rst_n)
80           frame_val_flag <= 1'b0;
81       else if((cmos_ps_cnt == WAIT_FRAME) && pos_vsync)
82           frame_val_flag <= 1'b1;
83       else;
84   end
85   
86   //8位数据转16位RGB565数据
87   always @(posedge cam_pclk or negedge rst_n) begin
88       if(!rst_n) begin
89           cmos_data_t <= 16'd0;
90           cam_data_d0 <= 8'd0;
91           byte_flag <= 1'b0;
92       end
93       else if(cam_href) begin
94           byte_flag <= ~byte_flag;
95           cam_data_d0 <= cam_data;
96           if(byte_flag)
97               cmos_data_t <= {cam_data_d0,cam_data};
98           else;
99       end
100      else begin
101          byte_flag <= 1'b0;
102          cam_data_d0 <= 8'b0;
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
CMOS图像采集模块第18行定义了参数WAIT_FRAME(寄存器数据稳定等待的帧个数),我们在前面介绍寄存器时提到过配置寄存器生效的时间最长为300ms,约为摄像头输出10帧图像数据。所以这里采集场同步信号的上升沿来统计帧数,计数器计数超过10次之后产生数据有效的标志,开始采集图像。在程序的第87行开始的always块实现了8位数据转16位数据的功能。需要注意的是摄像头的图像数据是在像素时钟(cam_pclk)下输出的,因此摄像头的图像数据必须使用像素钟来采集,否则会造成数据采集错误。
当摄像头的分辨率大于LCD屏的分辨率时,需要对其进行裁剪,使存入DDR3的分辨率匹配LCD屏的尺寸。
图像裁剪模块的代码如下所示:
1    module cmos_tailor(
2        input                 rst_n            ,  //复位信号
3        input       [15:0]    lcd_id           ,  //LCD屏的ID号
4        input       [10:0]    h_disp           ,  //LCD屏水平分辨率
5        input       [10:0]    v_disp           ,  //LCD屏垂直分辨率
6        output      [10:0]    h_pixel          ,  //存入ddr3的水平分辨率
7        output      [10:0]    v_pixel          ,  //存入ddr3的屏垂直分辨率
8        output      [27:0]    ddr3_addr_max    ,  //存入ddr3的最大读写地址
9        output      [15:0]    lcd_id_a         ,  //时钟同步后的LCD屏的ID号
10       //摄像头接口
11       input                 cam_pclk         ,  //cmos 数据像素时钟
12       input                 cam_vsync        ,  //cmos 场同步信号
13       input                 cam_href         ,  //cmos 行同步信号
14       input  [15:0]         cam_data         ,
15       input                 cam_data_valid   ,
16       //用户接口
17       output                cmos_frame_valid ,  //数据有效使能信号
18       output       [15:0]   cmos_frame_data     //有效数据
19       );
20   
21   //reg define
22   reg             cam_vsync_d0     ;
23   reg             cam_vsync_d1     ;
24   reg             cam_href_d0      ;
25   reg             cam_href_d1      ;
26   reg    [10:0]   h_pixel          ;  //存入ddr3的水平分辨率
27   reg    [10:0]   v_pixel          ;  //存入ddr3的屏垂直分辨率
28   reg    [10:0]   h_cnt            ;  //对行计数
29   reg    [10:0]   v_cnt            ;  //对场计数
30   reg             cmos_frame_valid ;
31   reg    [15:0]   cmos_frame_data  ;
32   reg    [15:0]   lcd_id_a         ;  //LCD屏的ID号
33   reg    [10:0]   h_disp_a         ;  //LCD屏水平分辨率
34   reg    [10:0]   v_disp_a         ;  //LCD屏垂直分辨率
35   
36   //wire define
37   wire            pos_vsync        ;  //采输入场同步信号的上升沿
38   wire            neg_hsync        ;  //采输入行同步信号的下降沿
39   wire   [10:0]   cmos_h_pixel     ;  //CMOS水平方向像素个数
40   wire   [10:0]   cmos_v_pixel     ;  //CMOS垂直方向像素个数
41   wire   [10:0]   cam_border_pos_l ;  //左侧边界的横坐标
42   wire   [10:0]   cam_border_pos_r ;  //右侧边界的横坐标
43   wire   [10:0]   cam_border_pos_t ;  //上端边界的纵坐标
44   wire   [10:0]   cam_border_pos_b ;  //下端边界的纵坐标
45   
46   //*****************************************************
47   //**                    main code
48   //*****************************************************
49   
50   assign  ddr3_addr_max = h_pixel * v_pixel;  //存入ddr3的最大读写地址
51   assign  cmos_h_pixel = 11'd640 ;  //CMOS水平方向像素个数
52   assign  cmos_v_pixel = 11'd480 ;  //CMOS垂直方向像素个数
53   
在模块的第50行对信号ddr3_addr_max(存入DDR3的最大读写地址)进行了赋值,因为本次实验用了一片DDR3,故DDR3的数据位宽为16位,而摄像头的数据位宽为16位,所以DDR3的最大存储地址为行场分辨率的乘积。
在模块的第51和52行对两个信号cmos_h_pixel(CMOS水平方向像素个数)和cmos_v_pixel(CMOS垂直方向像素个数)进行定义,这两个信号代表了本次实验摄像头的分辨率,而这次实验的LCD的分辨率是不固定的,故可能出现LCD屏的分辨率比摄像头分辨率小的情况,而正点原子的LCD屏只有一种480x272的屏分辨率比摄像头的分辨率小,所以需要将摄像头的图像大小裁剪成和LCD屏的分辨率大小一样。
54   //采输入场同步信号的上升沿
55   assign pos_vsync = (~cam_vsync_d1) & cam_vsync_d0;
56   
57   //采输入行同步信号的下降沿
58   assign neg_hsync = (~cam_href_d0) & cam_href_d1;
59   
60   //左侧边界的横坐标计算
61   assign cam_border_pos_l  = (cmos_h_pixel - h_disp_a)/2-1;
62   
63   //右侧边界的横坐标计算
64   assign cam_border_pos_r = h_disp + (cmos_h_pixel - h_disp_a)/2-1;
65   
66   //上端边界的纵坐标计算
67   assign cam_border_pos_t  = (cmos_v_pixel - v_disp_a)/2;
68   
69   //下端边界的纵坐标计算
70   assign cam_border_pos_b = v_disp_a + (cmos_v_pixel - v_disp_a)/2;
71   
OV7725摄像头RGB44980.png
图 39.4.2 摄像头裁剪图

如上图所示,在代码的第60至70行,计算了摄像头存入DDR3的边界坐标,上边界坐标(cam_border_pos_t)是摄像头的场分辨率(480)减去LCD屏的场分辨率再除以2得到的,其他的边界也是以类似的计算方法得到。
72   always @(posedge cam_pclk or negedge rst_n) begin
73       if(!rst_n) begin
74           cam_vsync_d0 <= 1'b0;
75           cam_vsync_d1 <= 1'b0;
76           cam_href_d0  <= 1'b0;
77           cam_href_d1  <= 1'b0;
78           lcd_id_a     <= 0   ;
79           v_disp_a     <= 0   ;
80           h_disp_a     <= 0   ;
81       end
82       else begin
83           cam_vsync_d0 <= cam_vsync;
84           cam_vsync_d1 <= cam_vsync_d0;
85           cam_href_d0 <= cam_href;
86           cam_href_d1 <= cam_href_d0;
87           lcd_id_a <= lcd_id;
88           v_disp_a <= v_disp;
89           h_disp_a <= h_disp;
90       end
91   end
92   
在模块的第72至91行,对输入信号进行时钟同步,是为了减少信号的扇出和防止时序不满足。
93   //计算存入ddr3的分辨率
94   always @(posedge cam_pclk or negedge rst_n) begin
95       if(!rst_n) begin
96           h_pixel <= 11'b0;
97           v_pixel <= 11'b0;
98       end
99       else begin
100          if(lcd_id_a == 16'h4342)begin
101              h_pixel <= h_disp_a;
102              v_pixel <= v_disp_a;
103        end
104        else begin
105            h_pixel <= cmos_h_pixel;
106            v_pixel <= cmos_v_pixel;
107        end
108      end
109  end
110  
在模块的第94至109行,根据LCD屏的器件ID来判断存入DDR3的分辨率,若当LCD的ID为16'h4342时,说明LCD屏的分辨率比摄像头小,所以存入DDR3的分辨率就是LCD屏的分辨率。
111  //对行计数
112  always @(posedge cam_pclk or negedge rst_n) begin
113      if(!rst_n)
114          h_cnt <= 11'b0;
115      else begin
116          if(pos_vsync||neg_hsync)
117              h_cnt <= 11'b0;
118          else if(cam_data_valid)
119              h_cnt <= h_cnt + 1'b1;
120          else if (cam_href_d0)
121              h_cnt <= h_cnt;
122          else
123              h_cnt <= h_cnt;
124      end
125  end
126  
127  //对场计数
128  always @(posedge cam_pclk or negedge rst_n) begin
129      if(!rst_n)
130          v_cnt <= 11'b0;
131      else begin
132          if(pos_vsync)
133              v_cnt <= 11'b0;
134          else if(neg_hsync)
135              v_cnt <= v_cnt + 1'b1;
136          else
137              v_cnt <= v_cnt;
138      end
139  end
140  
141  //产生输出数据有效信号(cmos_frame_valid)
142  always @(posedge cam_pclk or negedge rst_n) begin
143      if(!rst_n)
144          cmos_frame_valid <= 1'b0;
145      else if(h_cnt[10:0]>=cam_border_pos_l && h_cnt[10:0]<cam_border_pos_r&&
146              v_cnt[10:0]>=cam_border_pos_t && v_cnt[10:0]<cam_border_pos_b)
147              cmos_frame_valid <= cam_data_valid;
148      else
149              cmos_frame_valid <= 1'b0;
150  end
151  
152  always @(posedge cam_pclk or negedge rst_n) begin
153      if(!rst_n)
154          cmos_frame_data <= 1'b0;
155      else if(h_cnt[10:0]>=cam_border_pos_l && h_cnt[10:0]<cam_border_pos_r&&
156              v_cnt[10:0]>=cam_border_pos_t && v_cnt[10:0]<cam_border_pos_b)
157              cmos_frame_data <= cam_data;
158      else
159              cmos_frame_data <= 1'b0;
160  end
161  
162  endmodule
在模块的第142至150行,根据LCD屏的器件ID来判断输出的数据使能是否有效,当器件ID为16'h4342时,必须满足行场计数器在边界的范围内有效,其他时候是无效的,同理数据也是这么判断的。
本次实验的ddr3控制模块相较于“ddr3读写测试实验”只是在子模块(ddr3读写模块)中添加了一个乒乓缓存的功能,以及在ddr3控制器fifo控制模块添加了load信号来产生一段复位电平,以满足fifo复位时序。
ddr3读写模块添加的代码如下所示:
102  //读/写结束信号上升沿
103  assign wr_end_r=wr_end_d0&&(~wr_end_d1);
104  assign rd_end_r=rd_end_d0&&(~rd_end_d1);
105  
106  //乒乓操作
107  assign axi_araddr=ddr3_pingpang_en?
{4'b0,raddr_page,axi_araddr_n[24:0],1'b0}:{6'b0,axi_araddr_n[24:0],1'b0};
108  assign axi_awaddr=ddr3_pingpang_en?
{4'b0,waddr_page,axi_awaddr_n[24:0],1'b0}:{6'b0,axi_awaddr_n[24:0],1'b0};
省略部分源代码……
279  //对信号进行打拍处理
280  always @(posedge clk or negedge rst_n)  begin
281      if(~rst_n)begin
282          rd_load_d0 <= 0;
283          rd_load_d1 <= 0;
284          wr_load_d0 <= 0;
285          wr_load_d1 <= 0;
286      end   
287      else begin
288          rd_load_d0 <= rd_load;
289          rd_load_d1 <= rd_load_d0;
290          wr_load_d0 <= wr_load;
291          wr_load_d1 <= wr_load_d0;
292      end   
293  end
294  
295  //对输入源做个帧复位标志
296  always @(posedge clk or negedge rst_n)  begin
297      if(~rst_n)
298          wr_rst <= 0;
299      else if(wr_load_d0 && !wr_load_d1)
300          wr_rst <= 1;
301      else
302          wr_rst <= 0;
303  end
304  
305  //对输出源做个帧复位标志
306  always @(posedge clk or negedge rst_n)  begin
307      if(~rst_n)
308          rd_rst <= 0;
309      else if(!rd_load_d0 && rd_load_d1)
310          rd_rst <= 1;
311      else
312          rd_rst <= 0;
313  end
314  
第296行至313行代码对输入/输出源做了帧复位标志,即每完成一帧读/写操作,就将对应的读/写复位标志拉高一个时钟,使读/写地址复位到读/写起始地址。
315  //对输出源的读地址做个帧复位脉冲
316  always @(posedge clk or negedge rst_n)  begin
317      if(~rst_n)
318          raddr_rst_h <= 1'b0;
319      else if(rd_load_d0 && !rd_load_d1)
320          raddr_rst_h <= 1'b1;
321      else if(axi_araddr_n == app_addr_rd_min)
322          raddr_rst_h <= 1'b0;
323      else
324          raddr_rst_h <= raddr_rst_h;
325  end
326  
327  //对输出源帧的读地址高位切换
328  always @(posedge clk or negedge rst_n)  begin
329      if(~rst_n)
330          raddr_page <= 2'b0;
331      else if( rd_end_r)
332          raddr_page <= waddr_page + 2;
333      else
334          raddr_page <= raddr_page;
335  end
336  
337  //对输入源帧的写地址高位切换
338  always @(posedge clk or negedge rst_n)  begin
339      if(~rst_n) begin
340          waddr_page <= 2'b1;
341          fram_done<= 1'b0;
342      end
343      else if( wr_end_r)begin
344          fram_done<= 1'b1;
345          waddr_page <= waddr_page + 1 ;
346      end
347      else
348          waddr_page <= waddr_page;
349  end
350  
第328行至349行代码对输入/输出源帧的读/写地址高位切换,即在ddr3中划分了四个大小相同的存储空间。读写模块修改后的帧切换原理如下所示:
OV7725摄像头RGB50682.png
图 39.4.3 写比读快的示意图

OV7725摄像头RGB50745.png
图 39.4.4 读比写快的示意图

在图 39.4.3中的图2是写操作完成了一帧的存储,换了一个存储空间继续写,而读操作还没有读完一帧数据,继续读原来的存储空间;因为读帧率和写帧率的帧率比小于2倍,所以写操作端的第二帧还没有写完,读操作端已经读完了第一帧,换了一个存储空间继续读,如图3所示; 图4中写操作端完成了第二帧的存储,而读操作还没有读完第二帧数据;同理图5中写操作端完成了第三帧的存储,开始第四帧的写入,而读操作将要读完第二帧数据;图6表示读操作读完第二帧数据,而写操作端没有完成第四帧的存储,这里将存储空间跳转了2个,就是为了防止写操作追上读操作,出现画面撕裂的现象。
在图 39.4.4中图2是读操作读出了一帧的数据,而本次实验是根据写存储空间来判断读存储空间的,所以继续读之前的存储空间;图3是写操作端完成了第一帧的存储,而读操作还没有读完第二帧数据;图4是读操作端读完第二帧数据,而写操作还没有完成了第二帧的存储。
351  //DDR3读写逻辑实现
352  always @(posedge clk or negedge rst_n) begin
353      if(~rst_n)
354          state_cnt    <= IDLE;
355      else begin
356          case(state_cnt)
357              IDLE:begin
358                  if(init_start)
359                      state_cnt <= DDR3_DONE ;
360                  else
361                      state_cnt <= IDLE;
362              end
363              DDR3_DONE:begin
364                  //当帧复位到来时,对寄存器进行复位
365                  if(wr_rst)
366                      state_cnt <= DDR3_DONE;
367                  //当读到结束地址对寄存器复位
368                  else if(wfifo_rcount >= wr_bust_len)
369                      state_cnt <= WRITE_ADDR;   //跳到写操作
370                  //当帧复位到来时,对寄存器进行复位
371                  else if(raddr_rst_h)
372                      state_cnt <= DDR3_DONE;
373                  //当rfifo存储数据少于一次突发长度时,并且ddr已经写入了1帧数据
374                  else if(rfifo_wcount < rd_bust_len && ddr3_read_valid && fram_done )
375                      state_cnt <= READ_ADDR;   //跳到读操作
376                  else
377                      state_cnt <= state_cnt;
378              end
379              WRITE_ADDR:begin
380                  if(axi_awvalid && axi_awready)
381                      state_cnt <= WRITE_DATA;  //跳到写数据操作
382                  else
383                      state_cnt <= state_cnt;   //条件不满足,保持当前值
384              end
385              WRITE_DATA: begin
386                  //写到设定的长度跳到等待状态
387                  if(axi_wvalid && axi_wready && init_addr == wr_bust_len - 1)
388                      state_cnt <= DDR3_DONE;  //写到设定的长度跳到等待状态
389                  else
390                      state_cnt <= state_cnt;  //写条件不满足,保持当前值
391              end
392              READ_ADDR:begin
393                  if(axi_arvalid && axi_arready)
394                      state_cnt <= READ_DATA;
395                  else
396                      state_cnt <= state_cnt;
397              end
398              READ_DATA:begin                   //读到设定的地址长度
399                  if(axi_rlast)
400                      state_cnt <= DDR3_DONE;   //则跳到空闲状态
401                  else                          //若MIG没准备好,则保持原值
402                      state_cnt   <= state_cnt; //则跳到空闲状态
403              end
404              default:begin
405                  state_cnt    <= IDLE;
406              end
407          endcase
408      end
409  end
410  
411  endmodule
DDR3读写逻辑实现部分和“DDR3读写测试实验”一致,唯一的区别在于第374行代码对读操作的开启多了一个判定条件,即ddr已经写入了1帧图像数据后才会开启。
ddr3控制器fifo控制模块添加代码如下所示:
1    module ddr3_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       ,  //从ddr3读出数据的有效使能
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          rd_load_d0        ;
23   reg  [15:0]  rd_load_d         ;  //由输出源场信号移位拼接得到
24   reg          rdfifo_rst_h      ;  //rfifo复位信号,高有效
25   reg          wr_load_d0        ;
26   reg  [15:0]  wr_load_d         ;  //由输入源场信号移位拼接得到
27   reg          wfifo_rst_h       ;  //wfifo复位信号,高有效
28   
29   //*****************************************************
30   //**                    main code
31   //*****************************************************
32   
33   //对输出源场信号取反
34   always @(posedge clk_100 or negedge rst_n) begin
35       if(!rst_n)
36           rd_load_d0 <= 1'b0;
37       else
38           rd_load_d0 <= rd_load;
39   end
40   
41   //对输出源场信号进行移位寄存
42   always @(posedge clk_100 or negedge rst_n) begin
43       if(!rst_n)
44           rd_load_d <= 1'b0;
45       else
46           rd_load_d <= {rd_load_d[14:0],rd_load_d0};
47   end
48   
49   //产生一段复位电平,满足fifo复位时序
50   always @(posedge clk_100 or negedge rst_n) begin
51       if(!rst_n)
52           rdfifo_rst_h <= 1'b0;
53       else if(rd_load_d[0] && !rd_load_d[14])
54           rdfifo_rst_h <= 1'b1;
55       else
56           rdfifo_rst_h <= 1'b0;
57   end  
58   
59   //对输入源场信号进行移位寄存
60    always @(posedge wr_clk or negedge rst_n) begin
61       if(!rst_n)begin
62           wr_load_d0 <= 1'b0;
63           wr_load_d  <= 16'b0;
64       end     
65       else begin
66           wr_load_d0 <= wr_load;
67           wr_load_d <= {wr_load_d[14:0],wr_load_d0};
68       end
69   end
70   
71   //产生一段复位电平,满足fifo复位时序
72    always @(posedge wr_clk or negedge rst_n) begin
73       if(!rst_n)
74         wfifo_rst_h <= 1'b0;
75       else if(wr_load_d[0] && !wr_load_d[15])
76         wfifo_rst_h <= 1'b1;
77       else
78         wfifo_rst_h <= 1'b0;
79   end
80   
81   rd_fifo u_rd_fifo (
82     .wr_clk         (clk_100            ), // input
83     .wr_rst         (~rst_n|rdfifo_rst_h), // input
84     .wr_en          (rfifo_wren         ), // input
85     .wr_data        (rfifo_din          ), // input [127:0]
86     .wr_full        (                   ), // output
87     .wr_water_level (rfifo_wcount       ), // output [12:0]
88     .almost_full    (                   ), // output
89     .rd_clk         (rd_clk             ), // input
90     .rd_rst         (~rst_n|rdfifo_rst_h), // input
91     .rd_en          (rdata_req          ), // input
92     .rd_data        (pic_data           ), // output [15:0]
93     .rd_empty       (                   ), // output
94     .rd_water_level (                   ), // output [12:0]
95     .almost_empty   (                   )  // output
96   );
97   
98   wr_fifo u_wr_fifo (
99     .wr_clk         (wr_clk            ), // input
100    .wr_rst         (~rst_n|wfifo_rst_h), // input
101    .wr_en          (datain_valid      ), // input
102    .wr_data        (datain            ), // input [15:0]
103    .wr_full        (                  ), // output
104    .wr_water_level (                  ), // output [12:0]
105    .almost_full    (                  ), // output
106    .rd_clk         (clk_100           ), // input
107    .rd_rst         (~rst_n|wfifo_rst_h), // input
108    .rd_en          (wfifo_rden        ), // input
109    .rd_data        (wfifo_dout        ), // output [127:0]
110    .rd_empty       (                  ), // output
111    .rd_water_level (wfifo_rcount      ), // output [12:0]
112    .almost_empty   (                  )  // output
113  );
114  
115  endmodule
在“ddr3读写测试实验”的ddr3控制器fifo控制模块的基础上,该模块添加了load信号,通过load信号来产生一段复位电平,以此来满足fifo复位时序。
LCD顶层模块例化了四个模块,分别为时钟分频模块(clk_div)、读LCD ID模块(rd_id)、lcd驱动模块(lcd_driver)和lcd显示模块(lcd_display)。
lcd驱动模块负责驱动RGB-LCD显示屏,当摄像头的分辨率大于LCD屏的分辨率时,LCD驱动模块也负责通过发出内部信号data_req(数据请求信号)输出至端口来读取DDR3读写控制模块的输出像素数据,有关LCD驱动模块的详细介绍请大家参考“RGB-LCD彩条显示实验”章节。
当摄像头的分辨率小于LCD屏的分辨率时,lcd显示模块负责通过发出内部信号data_req(数据请求信号)输出至端口来读取DDR3读写控制模块的输出像素数据,以此完成液晶屏两侧填充黑色背景,中间区域显示图像数据的功能。
时钟分频模块负责分频出对应器件ID的采样时钟。读LCD ID模块负责采集外部LCD屏的设备型号。有关LCD ID模块和时钟分频模块的详细介绍请大家参考“RGB-LCD彩条显示实验”章节。
LCD顶层模块的代码如下:
1    module lcd_rgb_top(
2        input           sys_clk      ,  //系统时钟
3        input           clk_100m     ,
4        input           sys_rst_n    ,  //复位信号
5        input           sys_init_done,
6        //lcd接口
7        output          lcd_clk      ,  //LCD驱动时钟
8        output          lcd_hs       ,  //LCD 行同步信号
9        output          lcd_vs       ,  //LCD 场同步信号
10       output          lcd_de       ,  //LCD 数据输入使能
11       inout  [23:0]   lcd_rgb      ,  //LCD RGB颜色数据
12       output          lcd_bl       ,  //LCD 背光控制信号
13       output          lcd_rst      ,  //LCD 复位信号
14       output          lcd_pclk     ,  //LCD 采样时钟
15       output  [15:0]  lcd_id       ,  //LCD屏ID
16       output  [10:0]  pixel_xpos   ,  //像素点横坐标
17       output  [10:0]  pixel_ypos   ,  //像素点纵坐标
18       output          out_vsync    ,  //帧复位,高有效
19       output  [10:0]  h_disp       ,  //LCD屏水平分辨率
20       output  [10:0]  v_disp       ,  //LCD屏垂直分辨率
21       input   [15:0]  data_in      ,  //数据输入
22       output          data_req        //请求数据输入
23      
24       );
25   
26   //wire define
27   wire [15:0]  lcd_data_w    ;  //像素点数据
28   wire         data_req_w    ;  //请求像素点颜色数据输入
29   wire [10:0]  pixel_xpos    ;  //像素点横坐标
30   wire [10:0]  pixel_ypos    ;  //像素点纵坐标
31   wire         out_vsync     ;  //帧复位,高有效
32   wire [10:0]  h_disp        ;  //LCD屏水平分辨率
33   wire [10:0]  v_disp        ;  //LCD屏垂直分辨率
34   wire         data_req_big  ;  //大于640x480分辨率lcd屏的请求信号
35   wire         data_req_small;  //小于640x480分辨率lcd屏的请求信号
36   wire [15:0]  lcd_data      ;  //选择屏后的数据
37   wire  [15:0] lcd_rgb_565   ;  //输出的16位lcd数据
38   wire  [23:0] lcd_rgb_o     ;  //LCD 输出颜色数据
39   wire  [23:0] lcd_rgb_i     ;  //LCD 输入颜色数据
40   
41   //*****************************************************
42   //**                    main code
43   //*****************************************************
44   
45   //区分大小屏的读请求
46   assign data_req = (lcd_id == 16'h4342) ? data_req_small : data_req_big;
47   
48   //区分大小屏的数据
49   assign lcd_data = (lcd_id == 16'h4342) ? data_in : lcd_data_w ;
50   
51   //将摄像头16bit数据转换为24bit的lcd数据
52   assign lcd_rgb_o = {lcd_rgb_565[15:11],3'b000,lcd_rgb_565[10:5],2'b00,
53                       lcd_rgb_565[4:0],3'b000};
54   
55   //像素数据方向切换
56   assign lcd_rgb = lcd_de ?  lcd_rgb_o :  {24{1'bz}};
57   assign lcd_rgb_i = lcd_rgb;
58   
59   //时钟分频模块
60   clk_div u_clk_div(
61       .clk                    (clk_100m ),
62       .rst_n                  (sys_rst_n),
63       .lcd_id                 (lcd_id   ),
64       .lcd_pclk               (lcd_clk  )
65       );
66   
67   //读LCD ID模块
68   rd_id u_rd_id(
69       .clk                    (sys_clk  ),
70       .rst_n                  (sys_rst_n),
71       .lcd_rgb                (lcd_rgb_i),
72       .lcd_id                 (lcd_id   )
73       );
74   
75   //lcd驱动模块
76   lcd_driver u_lcd_driver(
77       .lcd_clk        (lcd_clk                  ),
78       .sys_rst_n      (sys_rst_n & sys_init_done),
79       .lcd_id         (lcd_id                   ),
80       .lcd_hs         (lcd_hs                   ),
81       .lcd_vs         (lcd_vs                   ),
82       .lcd_de         (lcd_de                   ),
83       .lcd_rgb        (lcd_rgb_565              ),
84       .lcd_bl         (lcd_bl                   ),
85       .lcd_rst        (lcd_rst                  ),
86       .lcd_pclk       (lcd_pclk                 ),
87       .pixel_data     (lcd_data                 ),
88       .data_req       (data_req_small           ),
89       .out_vsync      (out_vsync                ),
90       .h_disp         (h_disp                   ),
91       .v_disp         (v_disp                   ),
92       .pixel_xpos     (pixel_xpos               ),
93       .pixel_ypos     (pixel_ypos               )
94       );
95   
96   //lcd显示模块
97   lcd_display u_lcd_display(
98       .lcd_clk        (lcd_clk                  ),
99       .sys_rst_n      (sys_rst_n & sys_init_done),
100      .lcd_id         (lcd_id                   ),
101      .pixel_xpos     (pixel_xpos               ),
102      .pixel_ypos     (pixel_ypos               ),
103      .h_disp         (h_disp                   ),
104      .v_disp         (v_disp                   ),
105      .cmos_data      (data_in                  ),
106      .lcd_data       (lcd_data_w               ),
107      .data_req       (data_req_big             )
108      );
109  
110  endmodule
在程序的第46行和第49行,根据器件ID来判断读请求信号的最后输出和输入数据的最后输入。当摄像头分辨率大于屏体分辨率时,读请求信号由LCD驱动模块输出,数据直接输入到LCD驱动模块,反之,读请求信号由LCD显示模块输出,数据进入LCD显示模块进行处理后再进入LCD驱动模块。
在程序的第52行,将摄像头输出的格式RGB565转换为LCD需要输出的格式RGB888,在本次实验中的做法是给各个颜色的低位补零,其中lcd_rgb_565[15:11]表示红色,lcd_rgb_565[10:5]表示绿色,lcd_rgb_565[4:0]表示蓝色。
在程序的第56行至57行,由于lcd_rgb是24位的双向引脚,所以这里对双向引脚的方向做一个切换。当lcd_de信号为高电平时,此时输出的像素数据有效,将lcd_rgb的引脚方向切换成输出,并将LCD驱动模块输出的lcd_rgb_o(像素数据)连接至lcd_rgb引脚;当lcd_de信号为低电平时,此时输出的像素数据无效,将lcd_rgb的引脚方向切换成输入。代码中将高阻状态“Z”赋值给lcd_rgb的引脚,表示此时lcd_rgb的引脚电平由外围电路决定,此时可以读取lcd_rgb的引脚电平,从而获取到LCD屏的ID。
LCD 显示模块的代码如下:
1   module lcd_display(
2       input          lcd_clk   ,  //lcd驱动时钟
3       input          sys_rst_n ,  //复位信号
4       input   [15:0] lcd_id    ,  //LCD屏ID
5       input   [10:0] pixel_xpos,  //像素点横坐标
6       input   [10:0] pixel_ypos,  //像素点纵坐标
7       input   [15:0] cmos_data ,  //CMOS传感器像素点数据
8       input   [10:0] h_disp    ,  //LCD屏水平分辨率
9       input   [10:0] v_disp    ,  //LCD屏垂直分辨率
10      output  [15:0] lcd_data  ,  //LCD像素点数据
11      output         data_req     //请求像素点颜色数据输入
12      );
13  
14  //parameter define  
15  parameter  V_CMOS_DISP = 11'd480;            //CMOS分辨率——行
16  parameter  H_CMOS_DISP = 11'd640;            //CMOS分辨率——列
17  
18  localparam BLACK  = 16'b00000_000000_00000;  //RGB565 黑色
19  
20  //reg define
21  reg             data_val            ;       //数据有效信号
22  
23  //wire define
24  wire    [10:0]  display_border_pos_l;       //左侧边界的横坐标
25  wire    [10:0]  display_border_pos_r;       //右侧边界的横坐标
26  wire    [10:0]  display_border_pos_t;       //上端边界的纵坐标
27  wire    [10:0]  display_border_pos_b;       //下端边界的纵坐标
28  
29  //*****************************************************
30  //**                    main code
31  //*****************************************************
32  
33  //左侧边界的横坐标计算
34  assign display_border_pos_l  = (h_disp - H_CMOS_DISP)/2-1;
35  
36  //右侧边界的横坐标计算
37  assign display_border_pos_r = H_CMOS_DISP + (h_disp - H_CMOS_DISP)/2-1;
38  
39  //上端边界的纵坐标计算
40  assign display_border_pos_t  = (v_disp - V_CMOS_DISP)/2;
41  
42  //下端边界的纵坐标计算
43  assign display_border_pos_b = V_CMOS_DISP + (v_disp - V_CMOS_DISP)/2;
44  
45  //请求像素点颜色数据输入 范围:79~718,共640个时钟周期
46  assign data_req = ((pixel_xpos >= display_border_pos_l) &&
47                    (pixel_xpos < display_border_pos_r) &&
48                    (pixel_ypos > display_border_pos_t) &&
49                    (pixel_ypos <= display_border_pos_b)
50                    ) ? 1'b1 : 1'b0;
51  
52  //在数据有效范围内,将摄像头采集的数据赋值给LCD像素点数据
53  assign lcd_data = data_val ? cmos_data : BLACK;
54  
55  //有效数据滞后于请求信号一个时钟周期,所以数据有效信号在此延时一拍
56  always @(posedge lcd_clk or negedge sys_rst_n) begin
57      if(!sys_rst_n)
58          data_val <= 1'b0;
59      else
60          data_val <= data_req;
61  end
62  
63  endmodule
当LCD屏的分辨率大于摄像头的分辨率时,将摄像头采集的图像放在LCD屏的中央,四周填充黑色,如下图所示。
OV7725摄像头RGB66018.png
图 39.4.5 LCD屏裁剪图

在程序的第34行至50行,分别对摄像头数据的显示区域的横纵的起始坐标和结束坐标进行计算,上边界坐标(display_border_pos_t)是LCD屏的场分辨率摄像头的场分辨率减去摄像头的场分辨率(480)再除以2得到的,其他的边界也是以类似的计算方法得到。
在程序的第53行,对有效数据以外的区域填充黑色。这里还有一点需要提醒大家注意的是我们读数据请求发送给DDR3控制器后实际上是发送给读FIFO,而读FIFO接收到数据请求到数据出来是需要一个时钟周期的延迟的,也就是说有效数据实际上比读数据请求晚了一个时钟(如果FIFO用的是另外一种数据提前模式则有效数据和读数据请求是同步的,本节实验采用的是普通模式),因此代码的55~61行对读数据请求作了一个打拍处理,生成数据有效使能信号(data_val)。
本次实验的LCD驱动模块是在“RGB-LCD彩条显示实验”中添加了一个场信号输出,本次实验只对改动的地方进行讲解。
LCD驱动模块的代码如下:
1    module lcd_driver(
2        input           lcd_clk   ,  //lcd模块驱动时钟
3        input           sys_rst_n ,  //复位信号
4        input   [15:0]  lcd_id    ,  //LCD屏ID
5        input   [15:0]  pixel_data,  //像素点数据
6        output          data_req  ,  //请求像素点颜色数据输入
7        output  [10:0]  pixel_xpos,  //像素点横坐标
8        output  [10:0]  pixel_ypos,  //像素点纵坐标
9        output          out_vsync ,  //帧复位,高有效
10       output  [10:0]  h_disp    ,  //LCD屏水平分辨率
11       output  [10:0]  v_disp    ,  //LCD屏垂直分辨率
12       //RGB LCD接口
13       output          lcd_hs    ,  //LCD 行同步信号
14       output          lcd_vs    ,  //LCD 场同步信号
15       output          lcd_de    ,  //LCD 数据输入使能
16       output  [15:0]  lcd_rgb   ,  //LCD RGB565颜色数据
17       output          lcd_bl    ,  //LCD 背光控制信号
18       output          lcd_rst   ,  //LCD 复位信号
19       output          lcd_pclk     //LCD 采样时钟
20       );
此处省略一段代码。
118  //帧复位,高有效
119  assign out_vsync = ((h_cnt <= 100) && (v_cnt == 1)) ? 1'b1 : 1'b0;
此处省略一段代码。
248  
249  endmodule
在程序的第119行,在LCD屏场消隐的时候增加了一个模拟的场信号,提供给其他模块。到这里整个工程的程序设计就讲完了。
39.5下载验证
首先将OV7725摄像头插入开发板上的摄像头扩展接口(注意摄像头镜头朝外);将FPC排线一端与正点原子的7寸RGB模块上的J1接口连接,另一端与ATK-DFPGL22G开发板上的RGB_LCD接口连接;如图 39.5.1和图 39.5.2所示。连接时,先掀开FPC连接器上的黑色翻盖,将FPC排线蓝色面朝上插入连接器,最后将黑色翻盖压下以固定FPC排线。
连接实物图如下图所示:
OV7725摄像头RGB67837.png
图 39.5.1 ATK-7’ RGBLCD模块FPC连接器

056E209C-F1CB-4f5b-AFC8-A79C28302A3B.png
图 39.5.2 ATK-DFPGL22G开发板FPC连接器

最后将下载器一端连电脑,另一端与开发板上的JTAG端口连接,连接电源线并打开电源开关。接下来我们下载程序,验证OV7725摄像头RGB-LCD实时显示功能。下载完成后观察显示器的显示图像如下图所示,说明OV7725摄像头LCD显示程序下载验证成功。
OV7725摄像头RGB68114.png
图 39.5.3 RGB-LCD实时显示图像

回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-29 19:15

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表