1)实验平台:正点原子新起点V2开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=609758951113
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子FPGA感兴趣的同学可以加群讨论:994244016
4)关注正点原子公众号,获取最新资料更新
在OV7725摄像头RGB-LCD显示实验中,成功地在LCD屏上实时显示出了摄像头采集的图像。本章将使用FPGA开发板实现对OV7725的数字图像采集并在HDMI显示器上实时显示。
本章包括以下几个部分:
3939.1简介
39.2实验任务
39.3硬件设计
39.4程序设计
39.5下载验证
在“OV7725摄像头RGB-LCD显示实验”中对OV7725的视频传输时序、SCCB协议以及寄存器的配置信息等内容作了详细的介绍,如果大家对这部分内容不是很熟悉的话,请参考“OV7725摄像头RGB-LCD显示实验”中的OV7725简介部分。
40.2实验任务
本节实验任务是使用新起点开发板及OV7725摄像头实现图像采集,通过HDMI接口驱动HDMI显示器,并实时显示出图像。
40.3硬件设计
摄像头扩展接口原理图及OV7725模块说明与“OV7725摄像头RGB-LCD显示实验”完全相同,请参考“OV7725摄像头RGB-LCD显示实验”硬件设计部分。HDMI接口部分的硬件设计请参考“HDMI彩条显示实验”中的硬件设计部分。
由于OV7725、HDMI接口和SDRAM引脚数目较多且在前面相应的章节中已经给出它们的管脚列表,这里不再列出管脚分配。
图 40.4.1是根据本章实验任务画出的系统框图。对比“OV7725摄像头RGB-LCD显示实验”的系统框图可以发现,本次实验只是把LCD顶层模块替换成了HDMI顶层模块,将图像采集顶层模块中的图像裁剪模块去掉了,其余模块(除时钟模块外)完全相同。之所以在本节实验中把裁剪模块去掉了是因为本次实验所用的HDMI显示器的分辨率大于摄像头的分辨率相同,所以只需要在HDMI驱动模块稍微改一下读数据请求即可,改动部分下文会给大家讲解。时钟模块(由两个pll锁相环组成,一个负责提供LCD、摄像头和IIC模块的参考时钟;另一个负责提供HDMI模块的像素时钟与5倍频时钟。这里之所以用两个锁相环是因为HDMI所用的时钟71Mhz与SDRAM模块使用的时钟不是整数倍,使用一个锁相环不符合设计要求)用于为I2C驱动模块、HDMI顶层模块以及SDRAM控制模块提供驱动时钟;I2C驱动模块和I2C配置模块用于初始化OV7725图像传感器;摄像头采集模块负责采集摄像头图像数据,并且把图像数据写入SDRAM控制模块中;SDRAM控制模块负责将用户数据写入和读出片外SDRAM存储器;HDMI顶层模块负责驱动HDMI显示器。本章使用的HDMI显示器其分辨率为1280800,而OV7725传感器的最大分辨率为640480,所以HDMI显示器需要填充黑色的像素点。
图 40.4.1 顶层系统框图
由上图可知,本节实验使用了两个时钟模块(pll_clk、pll_hdmi),其中pll_clk为SDRAM控制模块以及I2C驱动模块提供驱动时钟,pll_hdmi 主要是为HDMI模块提供像素时钟和5倍像素时钟。I2C配置模块和I2C驱动模块控制着传感器初始化的开始与结束,传感器初始化完成后图像采集模块将采集到的数据写入SDRAM控制模块,HDMI顶层模块从SDRAM控制模块中读出数据并驱动显示器显示,这时整个系统才完成了数据的采集、缓存与显示。需要注意的是图像数据采集模块是在SDRAM和传感器都初始化完成之后才开始输出数据的,避免了在SDRAM初始化过程中向里面写入数据。
顶层模块的代码如下所示:
1 module ov7725_hdmi(
2 input sys_clk , //系统时钟
3 input sys_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_sgm_ctrl, //cmos 时钟选择信号, 1:使用摄像头自带的晶振
11 output cam_scl , //cmos SCCB_SCL线
12 inout cam_sda , //cmos SCCB_SDA线
13 //SDRAM接口
14 output sdram_clk , //SDRAM 时钟
15 output sdram_cke , //SDRAM 时钟有效
16 output sdram_cs_n , //SDRAM 片选
17 output sdram_ras_n , //SDRAM 行有效
18 output sdram_cas_n , //SDRAM 列有效
19 output sdram_we_n , //SDRAM 写有效
20 output [1:0] sdram_ba , //SDRAM Bank地址
21 output [1:0] sdram_dqm , //SDRAM 数据掩码
22 output [12:0] sdram_addr , //SDRAM 地址
23 inout [15:0] sdram_data , //SDRAM 数据
24 //HDMI接口
25 output tmds_clk_p, // TMDS 时钟通道
26 output tmds_clk_n,
27 output [2:0] tmds_data_p, // TMDS 数据通道
28 output [2:0] tmds_data_n
29 );
30
31 //parameter define
32 parameter SLAVE_ADDR = 7'h21 ; //OV7725的器件地址7'h21
33 parameter BIT_CTRL = 1'b0 ; //OV7725的字节地址为8位 0:8位 1:16位
34 parameter CLK_FREQ = 26'd50_000_000; //i2c_dri模块的驱动时钟频率
35 parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率,不超过400KHz
36 parameter V_CMOS_DISP = 11'd480 ; //CMOS分辨率--行
37 parameter H_CMOS_DISP = 11'd640 ; //CMOS分辨率--列
38
39 //wire define
40 wire clk_100m ; //100mhz时钟,SDRAM操作时钟
41 wire clk_100m_shift ; //100mhz时钟,SDRAM相位偏移时钟
42 wire clk_50m ; //50mhz时钟,提供给lcd驱动时钟
43 wire hdmi_clk ;
44 wire hdmi_clk_5 ;
45 wire locked ;
46 wire locked_hdmi ;
47 wire rst_n ;
48
49 wire i2c_exec ; //I2C触发执行信号
50 wire [15:0] i2c_data ; //I2C配置地址与数据(高8位地址,低8位数据)
51 wire cam_init_done ; //摄像头初始化完成
52 wire i2c_done ; //I2C寄存器配置完成信号
53 wire i2c_dri_clk ; //I2C操作时钟
54 wire wr_en ; //sdram_ctrl模块写使能
55 wire [15:0] wr_data ; //sdram_ctrl模块写数据
56 wire rd_en ; //sdram_ctrl模块读使能
57 wire sdram_init_done ; //SDRAM初始化完成
58 wire rdata_req ; //SDRAM控制器模块读使能
59 wire [15:0] rd_data ; //SDRAM控制器模块读数据
60 wire cmos_frame_valid ; //数据有效使能信号
61 wire init_calib_complete ; //SDRAM初始化完成init_calib_complete
62 wire sys_init_done ; //系统初始化完成(SDRAM初始化+摄像头初始化)
63 wire clk_200m ; //SDRAM参考时钟
64 wire cmos_frame_vsync ; //输出帧有效场同步信号
65 wire cmos_frame_href ; //输出帧有效行同步信号
66 wire [23:0] video_rgb ;
67 wire video_hs ;
68 wire video_vs ;
69 wire video_de ;
70 wire [23:0] pixel_data_w ;
71 wire [10:0] h_disp ; //HDMI显示器水平分辨率
72 wire [10:0] v_disp ; //HDMI显示器垂直分辨率
73
74
75 //*****************************************************
76 //** main code
77 //*****************************************************
78
79 assign rst_n = sys_rst_n & locked & locked_hdmi;
80 //系统初始化完成:SDRAM和摄像头都初始化完成
81 //避免了在SDRAM初始化过程中向里面写入数据
82 assign sys_init_done = sdram_init_done & cam_init_done;
83 //不对摄像头硬件复位,固定高电平
84 assign cam_rst_n = 1'b1;
85 //cmos 时钟选择信号, 1:使用摄像头自带的晶振
86 assign cam_sgm_ctrl = 1'b1;
87
88 //锁相环
89 pll_clk u_pll_clk(
90 .areset (~sys_rst_n),
91 .inclk0 (sys_clk),
92 .c0 (clk_100m),
93 .c1 (clk_100m_shift),
94 .c2 (clk_50m),
95 .locked (locked)
96 );
97 pll_hdmi pll_hdmi_inst (
98 .areset ( ~sys_rst_n ),
99 .inclk0 ( clk_50m ),
100 .c0 ( hdmi_clk ),
101 .c1 ( hdmi_clk_5 ),
102 .locked ( locked_hdmi )
103 );
104
105 //I2C配置模块
106 i2c_ov7725_rgb565_cfg u_i2c_cfg(
107 .clk (i2c_dri_clk),
108 .rst_n (rst_n),
109 .i2c_done (i2c_done),
110 .i2c_exec (i2c_exec),
111 .i2c_data (i2c_data),
112 .init_done (cam_init_done)
113 );
114
115 //I2C驱动模块
116 i2c_dri
117 #(
118 .SLAVE_ADDR (SLAVE_ADDR), //参数传递
119 .CLK_FREQ (CLK_FREQ ),
120 .I2C_FREQ (I2C_FREQ )
121 )
122 u_i2c_dri(
123 .clk (clk_50m ),
124 .rst_n (rst_n ),
125 //i2c interface
126 .i2c_exec (i2c_exec ),
127 .bit_ctrl (BIT_CTRL ),
128 .i2c_rh_wl (1'b0), //固定为0,只用到了IIC驱动的写操作
129 .i2c_addr (i2c_data[15:8]),
130 .i2c_data_w (i2c_data[7:0]),
131 .i2c_data_r (),
132 .i2c_done (i2c_done ),
133 .scl (cam_scl ),
134 .sda (cam_sda ),
135 //user interface
136 .dri_clk (i2c_dri_clk) //I2C操作时钟
137 );
138
139 //CMOS图像数据采集模块
140 cmos_capture_data u_cmos_capture_data(
141 .rst_n (rst_n & sys_init_done), //系统初始化完成之后再开始采集数据
142 .cam_pclk (cam_pclk),
143 .cam_vsync (cam_vsync),
144 .cam_href (cam_href),
145 .cam_data (cam_data),
146 .cmos_frame_vsync (cmos_frame_vsync),
147 .cmos_frame_href (cmos_frame_href),
148 .cmos_frame_valid (cmos_frame_valid), //数据有效使能信号
149 .cmos_frame_data (wr_data) //有效数据
150 );
151
152 //SDRAM 控制器顶层模块,封装成FIFO接口
153 //SDRAM 控制器地址组成: {bank_addr[1:0],row_addr[12:0],col_addr[8:0]}
154 sdram_top u_sdram_top(
155 .ref_clk (clk_100m), //sdram 控制器参考时钟
156 .out_clk (clk_100m_shift), //用于输出的相位偏移时钟
157 .rst_n (rst_n), //系统复位
158
159 //用户写端口
160 .wr_clk (cam_pclk), //写端口FIFO: 写时钟
161 .wr_en (cmos_frame_valid), //写端口FIFO: 写使能
162 .wr_data (wr_data), //写端口FIFO: 写数据
163 .wr_min_addr (24'd0), //写SDRAM的起始地址
164 .wr_max_addr (V_CMOS_DISP*H_CMOS_DISP-1), //写SDRAM的结束地址
165 .wr_len (10'd512), //写SDRAM时的数据突发长度
166 .wr_load (~rst_n), //写端口复位: 复位写地址,清空写FIFO
167
168 //用户读端口
169 .rd_clk (hdmi_clk), //读端口FIFO: 读时钟
170 .rd_en (rdata_req), //读端口FIFO: 读使能
171 .rd_data (rd_data), //读端口FIFO: 读数据
172 .rd_min_addr (24'd0), //读SDRAM的起始地址
173 .rd_max_addr (V_CMOS_DISP*H_CMOS_DISP-1), //读SDRAM的结束地址
174 .rd_len (10'd512), //从SDRAM中读数据时的突发长度
175 .rd_load (~rst_n), //读端口复位: 复位读地址,清空读FIFO
176
177 //用户控制端口
178 .sdram_read_valid (1'b1), //SDRAM 读使能
179 .sdram_pingpang_en (1'b1), //SDRAM 乒乓操作使能
180 .sdram_init_done (sdram_init_done), //SDRAM 初始化完成标志
181
182 //SDRAM 芯片接口
183 .sdram_clk (sdram_clk), //SDRAM 芯片时钟
184 .sdram_cke (sdram_cke), //SDRAM 时钟有效
185 .sdram_cs_n (sdram_cs_n), //SDRAM 片选
186 .sdram_ras_n (sdram_ras_n), //SDRAM 行有效
187 .sdram_cas_n (sdram_cas_n), //SDRAM 列有效
188 .sdram_we_n (sdram_we_n), //SDRAM 写有效
189 .sdram_ba (sdram_ba), //SDRAM Bank地址
190 .sdram_addr (sdram_addr), //SDRAM 行/列地址
191 .sdram_data (sdram_data), //SDRAM 数据
192 .sdram_dqm (sdram_dqm) //SDRAM 数据掩码
193 );
194
195 //例化HDMI顶层模块
196 hdmi_top u_hdmi_top(
197 .hdmi_clk (hdmi_clk ),
198 .hdmi_clk_5 (hdmi_clk_5 ),
199 .rst_n (rst_n ),
200
201 .rd_data (rd_data ),
202 .rd_en (rdata_req),
203 .h_disp (h_disp ),
204 .v_disp (v_disp ),
205 .pixel_xpos (),
206 .pixel_ypos (),
207 .video_vs (),
208 .tmds_clk_p (tmds_clk_p ),
209 .tmds_clk_n (tmds_clk_n ),
210 .tmds_data_p (tmds_data_p ),
211 .tmds_data_n (tmds_data_n )
212 );
213 endmodule
从上面顶层模块代码中可以看出本节实验就是在原本的“OV7725摄像头RGB-LCD显示实验”的基础上修改过来的,这里主要就是将原本的LCD顶层模块替换成了HDMI显示模块,然后时钟模块又添加了一个锁相环(之所以又添加一个锁相环是因为HDMI使用的71Mhz像素时钟与IIC、SDRAM模块的时钟不是整数倍,只使用一个锁相环不符合设计要求)。锁相环没什么好介绍的,下面我们来看看HDMI模块。
HDMI模块一共分成两个子模块,分别是显示驱动模块(video_driver)和HDMI驱动模块(dvi_transmitter_top),其中HDMI驱动模块(dvi_transmitter_top)主要就是进行串并转换和TMDS编码,这部分内容在前面的HDMI彩条实验中已经给大家讲解过了,这里不再重复赘述了,下面我们来看看显示驱动模块。
显示驱动模块(video_driver):显示驱动模块的作用就是生成HDMI显示器的驱动时序,用来驱动HDMI显示器,其中包括行场同步信号和DE像素有效信号。这里的显示驱动模块(video_driver)跟HDMI彩条显示实验的驱动模块还是有区别的,一个是分辨率不同,在彩条实验当中用的是720P,而本节实验用的是1280*800的分辨率。另一个就是读数据请求作了修改,因为摄像头采集数据模块我们将图片裁剪模块删掉了,但是OV7725摄像头的分辨率又小于显示器的分辨率,所以我们需要配合读数据请求信号来给显示器填充黑边,其代码如下:
1 module video_driver(
2 input pixel_clk ,
3 input sys_rst_n ,
4
5 //RGB接口
6 output video_hs , //行同步信号
7 output video_vs , //场同步信号
8 output video_de , //数据使能
9 output [23:0] video_rgb , //RGB888颜色数据
10 output data_req ,
11
12 input [15:0] video_rgb_565 , //像素点数据
13 output [10:0] pixel_xpos , //像素点横坐标
14 output [10:0] pixel_ypos , //像素点纵坐标
15 output [10:0] h_disp ,
16 output [10:0] v_disp
17 );
18
19 //parameter define
20
21 //1280*800 分辨率时序参数
22 parameter H_SYNC = 11'd32; //行同步
23 parameter H_BACK = 11'd80; //行显示后沿
24 parameter H_DISP = 11'd1280; //行有效数据
25 parameter H_FRONT = 11'd48; //行显示前沿
26 parameter H_TOTAL = 11'd1440; //行扫描周期
27
28 parameter V_SYNC = 11'd6; //场同步
29 parameter V_BACK = 11'd14; //场显示后沿
30 parameter V_DISP = 11'd800; //场有效数据
31 parameter V_FRONT = 11'd3; //场显示前沿
32 parameter V_TOTAL = 11'd823; //场扫描周期
33
34 //reg define
35 reg [10:0] cnt_h;
36 reg [10:0] cnt_v;
37
38 //wire define
39 wire video_en ;
40 reg data_en ;
41 wire [23:0]pixel_data;
42 //*****************************************************
43 //** main code
44 //*****************************************************
45
46 assign video_de = video_en;
47
48 assign video_hs = ( cnt_h < H_SYNC ) ? 1'b0 : 1'b1; //行同步信号赋值
49 assign video_vs = ( cnt_v < V_SYNC ) ? 1'b0 : 1'b1; //场同步信号赋值
50
51 //使能RGB数据输出
52 assign video_en = (((cnt_h >= H_SYNC+H_BACK) && (cnt_h < H_SYNC+H_BACK+H_DISP))
53 &&((cnt_v >= V_SYNC+V_BACK) && (cnt_v < V_SYNC+V_BACK+V_DISP)))
54 ? 1'b1 : 1'b0;
55
56 //RGB888数据输出
57 assign video_rgb = data_en ? pixel_data : 24'd0;
58
59 //请求像素点颜色数据输入
60 assign data_req = (((cnt_h >= H_SYNC+H_BACK+320) &&
61 (cnt_h < H_SYNC+H_BACK+H_DISP-320))
62 && ((cnt_v >= V_SYNC+V_BACK+160) && (cnt_v < V_SYNC+V_BACK+V_DISP-160)))
63 ? 1'b1 : 1'b0;
64
65 always@(posedge pixel_clk or negedge sys_rst_n)begin
66 if(!sys_rst_n)
67 data_en <= 0;
68 else //因为FIFO出来数据要比请求晚一个时钟,所以数据有效使能要打一拍
69 data_en <= data_req;
70 end
71
72 //像素点坐标
73 assign pixel_xpos = data_req ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 11'd0;
74 assign pixel_ypos = data_req ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 11'd0;
75
76 assign pixel_data = {video_rgb_565[15:11],3'b000,video_rgb_565[10:5],2'b00,
77 video_rgb_565[4:0],3'b000};
78
79 //行场分辨率
80 assign h_disp = H_DISP;
81 assign v_disp = V_DISP;
82
83 //行计数器对像素时钟计数
84 always @(posedge pixel_clk ) begin
85 if (!sys_rst_n)
86 cnt_h <= 11'd0;
87 else begin
88 if(cnt_h < H_TOTAL - 1'b1)
89 cnt_h <= cnt_h + 1'b1;
90 else
91 cnt_h <= 11'd0;
92 end
93 end
94
95 //场计数器对行计数
96 always @(posedge pixel_clk ) begin
97 if (!sys_rst_n)
98 cnt_v <= 11'd0;
99 else if(cnt_h == H_TOTAL - 1'b1) begin
100 if(cnt_v < V_TOTAL - 1'b1)
101 cnt_v <= cnt_v + 1'b1;
102 else
103 cnt_v <= 11'd0;
104 end
105 end
106
107 endmodule
代码的60~63行可以看到读数据请求不再像HDMI彩条实验那样从行同步后延之后就拉高而是延迟了320个像素点,同样保持时间也不再是一个完整的行显示时间(H_DISP)了,而是提前320个像素点就拉低。之所以这样做是因为,摄像头的行分辨率是640,而显示器的分辨率是1280,我们想把图片显示在显示器的正中间,所以就要从显示区域的第320个像素点开始读取摄像头数据,然后保持640个时钟周期后拉低读数据请求(这个显示区域也可以不再正中间,根据自己需求可以调整位置)。同理场区域也可以像行区域这样处理,不过摄像头场分辨率是480,而显示器是800,那么想让图片在列方向显示在显示器的正中间那么读数据请求就要延迟160行拉高(显示区域根据需求可以自行调整,不是有效显示区域部分我们填充黑边),然后持续480个时钟周期后拉低。
这里还有一点需要提醒大家注意的是我们读数据请求发送给SDRAM控制器后实际上是发送给读FIFO,而读FIFO接收到数据请求到数据出来是需要一个时钟周期的延迟的,也就是说有效数据实际上比读数据请求晚了一个时钟(如果FIFO用的是另外一种数据提前模式则有效数据和读数据请求是同步的,本节实验采用的是普通模式),因此代码的65~70行对读数据请求作了一个打拍处理,生成数据有效使能信号(data_en),当data_en信号拉高代表数据有效如代码第57行所示,data_en信号为低时代表数据无效,此时给HDMI显示屏填充黑边,到这里OV7725 HDMI显示实验的代码就讲完了。
因为本节实验例程的所有代码在“SDRAM读写实验”、“ov7725_lcd显示实验”和“HDMI彩条显示实验”都有过详细的讲解,我们在本节实验只不过是将这些模块拿过来重新拼接一下,所以就不再重复的去详细讲解代码了,大家有不懂的地方可以翻看前面的例程。
40.5下载验证
编译完工程之后就可以开始下载程序了。将OV7725摄像头模块插在新起点开发板的“OLED/CAMERA”插座上,并将HDMI电缆一端连接到开发板上的HDMI插座、另一端连接到显示器。将下载器一端连电脑,另一端与开发板上的JTAG端口连接,连接电源线并打开电源开关。接下来我们下载程序,验证OV7725 HDMI实时显示功能。下载完成后观察HDMI显示器显示的图案如下图所示,说明OV7725 HDMI实时显示程序下载验证成功。
图 40.5.1 HDMI实时显示图像