本文主要是介绍【正点原子FPGA连载】第五十五章 双目OV5640摄像头RGB-LCD显示实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1)实验平台:正点原子新起点V2开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=609758951113
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:994244016
5)关注正点原子公众号,获取最新资料更新
第五十五章 双目OV5640摄像头RGB-LCD显示实验
双目摄像头是在一个模组上集成了两个摄像头,实现双通道图像采集的功能。双目摄像头一般应用于安防监控、立体视觉测距、三维重建等领域。本试验只做最基础的工作,把双目OV5640摄像头实时采集到的图像分左右两半显示在LCD屏幕上。
本章包括以下几个部分:
1.1 简介
1.2 实验任务
1.3 硬件设计
1.4 程序设计
1.5 下载验证
1.1 简介
摄像头在日常生活中非常常见,一般分为单目摄像头、双目摄像头和多目摄像头。单目摄像头目前使用最为广泛;双目摄像头主要应用于单目摄像头无法胜任的场合,如测距领域,根据两个摄像头的视差,辅以一定的算法,人们可以计算物体的距离;当然针对一些特殊的应用,目前市场上也出现了多目摄像头,以应对更加复杂的场景。在“OV5640摄像头LCD显示实验”中对OV5640的视频传输时序、SCCB协议以及寄存器的配置信息等内容作了详细的介绍,如果大家对这部分内容不是很熟悉的话,请参考之前的实验。本次实验将在前面单目OV5640摄像头的基础上学习双目摄像头的LCD显示。
1.2 实验任务
本章实验任务是利用双目OV5640摄像头采集图像,将采集到的图像实时显示在LCD屏幕上,两幅图像分别占据LCD屏的左右半边。
1.3 硬件设计
新起点开发板上有两个扩展口,分别是P6和P7。新起点开发板的P6扩展口与LCD屏的管脚复用,故本次实验采用P7扩展口来连接双目OV5640摄像头。P7扩展口原理图如图 55.3.1所示:
图 55.3.1 P7扩展接口原理图
ATK-Dual-OV5640是正点原子推出的一款双目OV5640摄像头模块,其硬件原理图如下图所示:
图 55.3.2 ATK-Dual-OV5640原理图
该模块通过2*20排母(2.54mm间距)同外部连接,连接时将双目摄像头的排母直接插在开发板上的P7扩展口即可,模块实物连接图如图 55.3.3所示:
图 55.3.3 双目摄像头模块实物连接图
由于LCD接口和SDRAM引脚数目较多且在前面相应的章节中已经给出它们的管脚列表,这里只列出双目摄像头相关管脚分配,如下表所示:
双目摄像头TCL约束文件如下:
set_location_assignment PIN_L7 -to cam0_data[7]
set_location_assignment PIN_L6 -to cam0_data[5]
set_location_assignment PIN_K5 -to cam0_data[3]
set_location_assignment PIN_K6 -to cam0_data[1]
set_location_assignment PIN_F3 -to cam0_rst_n
set_location_assignment PIN_G5 -to cam0_href
set_location_assignment PIN_M6 -to cam0_vsync
set_location_assignment PIN_N3 -to cam0_pclk
set_location_assignment PIN_L3 -to cam0_data[6]
set_location_assignment PIN_L4 -to cam0_data[4]
set_location_assignment PIN_K8 -to cam0_data[2]
set_location_assignment PIN_G1 -to cam0_data[0]
set_location_assignment PIN_J6 -to cam0_sda
set_location_assignment PIN_F1 -to cam0_scl
set_location_assignment PIN_J1 -to cam0_pwdn
set_location_assignment PIN_A2 -to cam1_pwdn
set_location_assignment PIN_B5 -to cam1_data[7]
set_location_assignment PIN_B6 -to cam1_data[5]
set_location_assignment PIN_B7 -to cam1_data[3]
set_location_assignment PIN_A4 -to cam1_data[1]
set_location_assignment PIN_D3 -to cam1_rst_n
set_location_assignment PIN_C6 -to cam1_href
set_location_assignment PIN_E5 -to cam1_vsync
set_location_assignment PIN_A3 -to cam1_pclk
set_location_assignment PIN_A5 -to cam1_data[6]
set_location_assignment PIN_A6 -to cam1_data[4]
set_location_assignment PIN_B3 -to cam1_data[2]
set_location_assignment PIN_B4 -to cam1_data[0]
set_location_assignment PIN_F5 -to cam1_sda
set_location_assignment PIN_D6 -to cam1_scl
1.4 程序设计
根据实验任务,首先设计如图 55.4.1所示的系统框图,本章实验的系统框架延续了“OV5640摄像头LCD显示实验”的整体架构,但是做出了一定的修改,在本节实验中我们将寄存器配置模块、IIC驱动模块以及图像采集模块封装成了一个模块(OV5640驱动模块),并且对OV5640驱动模块例化了两次(因为是双目摄像头,所以需要例化两次来分别驱动两个摄像头)。整个工程包含以下5个模块:时钟模块、图像分辨率设置模块、SDRAM控制器模块、摄像头驱动模块(例化两次)和LCD顶层模块。其中时钟模块、图像分辨率设置模块和摄像头驱动模块没有做任何修改,这些模块在单目OV5640摄像头LCD显示实验中已经说明过,这里不再详述,本次实验对SDRAM控制模块和LCD顶层模块做了修改。
图 55.4.1 顶层系统框图
时钟模块(pll):时钟模块通过调用锁相环 IP核实现,共输出3路时钟,分别是SDRAM参考时钟(100Mhz)、SDRAM相位偏移时钟(100Mhz偏移120度,这个偏移角度可以适当调整。)和LCD驱动时钟(50Mhz)。其中SDRAM参考时钟不仅仅用来驱动SDRAM顶层模块还作为摄像头驱动模块的工作时钟来用。
图像分辨率设置模块(picture_size):图像尺寸配置模块用于配置摄像头输出图像尺寸的大小,此外还完成了SDRAM的读写结束地址设置。有关图像分辨率设置模块的详细介绍请大家参考单目OV5640摄像头LCD显示实验章节。
LCD顶层模块(lcd_rgb_top):LCD顶层模块负责驱动LCD屏的驱动信号的输出,同时为其他模块提供屏体参数、场同步信号和数据请求信号。
摄像头驱动模块(ov5640_dri):本模块由原先的IIC驱动模块、OV5640 寄存器配置模块和摄像头图像采集模块封装而成,这样做是为了减少顶层模块的代码量,增强可读性,同时有利于代码的维护和管理。由于本次实验连接了两个相同的摄像头,因此我们会对摄像头驱动模块例化两次,将两个驱动模块输出的数据和数据有效使能全部连接到SDRAM控制模块。有关摄像头驱动模块的详细介绍请大家参考单目OV5640摄像头LCD显示实验章节。
SDRAM控制模块(sdram_top):SDRAM读写控制器模块负责驱动SDRAM片外存储器,缓存图像传感器输出的图像数据。该模块将SDRAM复杂的读写操作封装成类似FIFO的用户接口,非常方便用户的使用。
下面是顶层模块的原理图:
图 55.4.2 顶层模块原理图
本次实验是在单目OV5640摄像头LCD显示实验的基础上作修改的,主要修改了SDRAM控制模块和LCD顶层模块,而其余模块基本相同,因此接下来只介绍修改部分的内容。前面的实验,使用读写各一个FIFO,并在SDRAM中开辟了两个缓冲区,而本实验有两个摄像头,所以读写需要各增加一个FIFO,总共四个,同时在SDRAM中再开辟两个缓冲区,达到四个缓冲区,而满足两组摄像头数据的处理需要。
下面我们就一起来看看本节实验在单目OV5640摄像头LCD显示实验的基础上作了哪些修改。首先我们先看下顶层模块,
顶层模块最大的改变就是摄像头驱动模块了,代码如下(只贴出修改部分):
125 //OV5640 0摄像头驱动
126 ov5640_dri u0_ov5640_dri(
127 .clk (clk_100m),
128 .rst_n (rst_n),
129
130 .cam_pclk (cam0_pclk ),
131 .cam_vsync (cam0_vsync),
132 .cam_href (cam0_href ),
133 .cam_data (cam0_data ),
134 .cam_rst_n (cam0_rst_n),
135 .cam_pwdn (cam0_pwdn ),
136 .cam_scl (cam0_scl ),
137 .cam_sda (cam0_sda ),
138
139 .capture_start (sdram_init_done),
140 .cmos_h_pixel (cmos_h_pixel[12:1]),
141 .cmos_v_pixel (cmos_v_pixel),
142 .total_h_pixel (total_h_pixel),
143 .total_v_pixel (total_v_pixel),
144 .cam_init_done (cam_init_done_0),
145
146 .cmos_frame_vsync (),
147 .cmos_frame_href (),
148 .cmos_frame_valid (wr0_en),
149 .cmos_frame_data (wr0_data)
150 );
摄像头驱动模块将原本的IIC驱动模块、OV5640 寄存器配置模块和摄像头图像采集模块封装成一个模块,这部分内容没什么好讲的,无非就是把端口封装一下用一个顶层模块去调用三个子模块而已。这里需要注意的是代码第140行,cmos_h_pixel是指LCD显示屏的行分辨率,在前面单目OV5640摄像头LCD显示实验中它是直接作为ov5640摄像头的行分辨率配置参数,但是本节实验不行,因为本节实验是双目,两个摄像头两幅画面需要显示在一个屏幕上,因此一个摄像头所占有的行分辨率只能是LCD显示屏行分辨率的一半,所以cmos_h_pixel这个参数需要除以2作为摄像头的行分辨率配置参数(cmos_h_pixel[12:1]这种写法相当于除以2)。
接下来我们再继续看看SDRAM控制器修改的内容。SDRAM控制器主要是修改了sdram_fifo_ctrl模块,这个模块是控制着整个SDRAM的读和写。在前面单目OV5640摄像头LCD显示实验中我们的读写原理是在SDRAM中开辟两个存储空间,一个空间正在缓存数据另一个空间就可以往外读出数据,这样交替使用。但是本节实验使用的是双目摄像头,有两个数据源,因此原本的两个存储空间肯定是不够用的,所以我们在原本的基础上将两个存储空间都扩大成四个存储空间,这样就可以容纳两个摄像头的数据了代码如下:
199 //sdram写地址0产生模块
200 always @(posedge clk_ref or negedge rst_n) begin
201 if(!rst_n)begin
202 sdram_wr_addr0 <= 24'd0;
203 rw_bank_flag0 <= 0;
204 sw_bank_en0 <= 0;
205 end
206 else if(wr_load_flag)begin //检测到写端口复位信号时,写地址复位
207 sdram_wr_addr0 <= wr_min_addr;
208 rw_bank_flag0 <= 0;
209 sw_bank_en0 <= 0;
210 end //若突发写SDRAM结束更改写地址
211 else if(write_done_flag && !wr_fifo_flag) begin
212 if(sdram_pingpang_en) begin //SDRAM 读写乒乓使能
213 //若未到达写SDRAM的结束地址写地址累加
214 if(sdram_wr_addr0[21:0] < wr_max_addr - wr_length)
215 sdram_wr_addr0 <= sdram_wr_addr0 + wr_length;
216 else begin //切换BANK
217 rw_bank_flag0 <= ~rw_bank_flag0;
218 sw_bank_en0 <= 1'b1; //拉高切换BANK使能信号
219 end
220 end //乒乓操作不使能时
221 //判断是否到达结束地址
222 else if(sdram_wr_addr0 < wr_max_addr - wr_length)
223 //没达结束地址,地址累加一个突发长度
224 sdram_wr_addr0 <= sdram_wr_addr0 + wr_length;
225 else //若已到达结束地址,则回到写起始地址
226 sdram_wr_addr0 <= wr_min_addr;
227 end
228 else if(sw_bank_en0) begin //如果bank切换使能信号有效
229 sw_bank_en0 <= 1'b0; //将使能信号置0,方便下次使用
230 if(rw_bank_flag0 == 1'b0) //根据bank标志信号切换BANK
231 sdram_wr_addr0 <= {2'b00,wr_min_addr[21:0]};
232 else
233 sdram_wr_addr0 <= {2'b01,wr_min_addr[21:0]};
234 end
235 end
236
237 //sdram写地址1产生模块
238 always @(posedge clk_ref or negedge rst_n) begin
239 if(!rst_n)begin
240 sdram_wr_addr1 <= 24'd0;
241 rw_bank_flag1 <= 0;
242 sw_bank_en1 <= 0;
243 end
244 else if(wr_load_flag)begin //检测到写端口复位信号时,写地址复位
245 rw_bank_flag1 <= 0;
246 sw_bank_en1 <= 0;
247 sdram_wr_addr1 <= wr_max_addr;
248 end //若突发写SDRAM结束,更改写地址
249 else if(write_done_flag && wr_fifo_flag) begin
250 if(sdram_pingpang_en) begin //判断若SDRAM 读写乒乓使能
251 //若未到达写SDRAM的结束地址写地址累加
252 if(sdram_wr_addr1[21:0] < wr_max_addr*2 - wr_length)
253 sdram_wr_addr1 <= sdram_wr_addr1 + wr_length;
254 else begin //切换BANK
255 rw_bank_flag1 <= ~rw_bank_flag1;
256 sw_bank_en1 <= 1'b1; //拉高切换BANK使能信号
257 end
258 end //乒乓操作不使能
259 //未到达写SDRAM的结束地址写地址累加
260 else if(sdram_wr_addr1 < wr_max_addr*2 - wr_length)
261 sdram_wr_addr1 <= sdram_wr_addr1 + wr_length;
262 else //到达写SDRAM的结束地址回到写起始地址
263 sdram_wr_addr1 <= wr_max_addr;
264 end
265 else if(sw_bank_en1) begin //如果bank切换使能信号有效
266 sw_bank_en1 <= 1'b0; //将使能信号置0,方便下次使用
267 if(rw_bank_flag1 == 1'b0) //切换BANK
268 sdram_wr_addr1 <= {2'b10,wr_max_addr[21:0]};
269 else
270 sdram_wr_addr1 <= {2'b11,wr_max_addr[21:0]};
271 end
272 end
273
274 //sdram读地址0产生模块
275 always @(posedge clk_ref or negedge rst_n) begin
276 if(!rst_n)
277 sdram_rd_addr0 <= 24'd0;
278 else if(rd_load_flag) //检测到写端口复位信号时,写地址复位
279 sdram_rd_addr0 <= rd_min_addr; //若突发读SDRAM结束,更改读地址
280 else if(read_done_flag && !rd_fifo_flag ) begin
281 if(sdram_pingpang_en) begin //判断若SDRAM 读写乒乓使能
282 //若未到达SDRAM的结束地址则地址累加
283 if(sdram_rd_addr0[21:0] < rd_max_addr - rd_length)
284 sdram_rd_addr0 <= sdram_rd_addr0 + rd_length;
285 else begin //到达读SDRAM的结束地址,回到读起始
286 if(rw_bank_flag0 == 1'b0) //根据rw_bank_flag的值切换读BANK地址
287 sdram_rd_addr0 <= {2'b01,rd_min_addr[21:0]};
288 else
289 sdram_rd_addr0 <= {2'b00,rd_min_addr[21:0]};
290 end
291 end //若乒乓操作未使能
292 //未到达SDRAM的结束地址地址累加
293 else if(sdram_rd_addr0 < rd_max_addr - rd_length)
294 sdram_rd_addr0 <= sdram_rd_addr0 + rd_length;
295 else //若到达SDRAM的结束地址回到起始地址
296 sdram_rd_addr0 <= rd_min_addr;
297 end
298 end
299
300 //sdram读地址1产生模块
301 always @(posedge clk_ref or negedge rst_n) begin
302 if(!rst_n)
303 sdram_rd_addr1 <= 24'd0;
304 else if(rd_load_flag) //检测到复位信号时地址复位
305 sdram_rd_addr1 <= rd_max_addr;
306 //判断若突发读SDRAM结束
307 else if(read_done_flag && rd_fifo_flag) begin
308 if(sdram_pingpang_en) begin //若SDRAM 读写乒乓使能
309 //若未到达SDRAM的结束地址则地址累加
310 if(sdram_rd_addr1[21:0] < rd_max_addr*2 - rd_length)
311 sdram_rd_addr1 <= sdram_rd_addr1 + rd_length;
312 else begin //到达读SDRAM的结束地址
313 if(rw_bank_flag1 == 1'b0) //根据rw_bank_flag的值切换BANK地址
314 sdram_rd_addr1 <= {2'b11,rd_max_addr[21:0]};
315 else
316 sdram_rd_addr1 <= {2'b10,rd_max_addr[21:0]};
317 end
318 end //如果乒乓操作没有使能
319 //未到达SDRAM的结束地址地址累加
320 else if(sdram_rd_addr1 < rd_max_addr*2 - rd_length)
321 sdram_rd_addr1 <= sdram_rd_addr1 + rd_length;
322 else //若已到达SDRAM的结束地址回到起始地址
323 sdram_rd_addr1 <= rd_max_addr;
324 end
325 end
由于FIFO控制模块代码太长,因此我一段一段的讲解,上面这段代码就是读写地址控制,相比较于单目摄像头LCD显示实验可以看到读写地址都增加了一帧的空间。代码200235行跟之前的单目一样就是在SDRAM中开辟一个空间用来存储一帧图片,写满一帧后BANK地址切换继续写下一帧,但是本节是经验就不一样了,代码237272行又开辟了一帧的存储空间,这一帧的存储空间跟上一帧不在同一BANK上。像这样开辟两帧的空间刚好可以用来分别存储两个摄像头的数据,当两个摄像头的数据都各存一帧后就可以切换BANK继续去存储两个摄像头下一帧的数据了。接下来就进入读地址了,读地址和写地址是相对应的,当写地址完成一个BANK的数据存储切换到下一个BANK后,读地址开始去读这个已经存储了数据的BANK,当写地址写完下一个BANK又切换回来则读地址切换到另一个BANK上去,这样周而复始的交替进行读写就可以实现摄像头数据的缓存了。接下来我们再来看看读写状态是如何切换的,代码如下所示:
326
327 //读写端四个FIFO的判断逻辑
328 always@(posedge clk_ref or negedge rst_n) begin
329 if(!rst_n) begin
330 sdram_wr_req <= 0;
331 sdram_wr_addr <= sdram_wr_addr0;
332 wr_fifo_flag <= 0;
333
334 sdram_rd_req <= 0;
335 rd_fifo_flag <= 0;
336 sdram_rd_addr <= sdram_rd_addr0;
337 state <= idle; //复位处于空闲状态,不操作任何FIFO
338 end
339 else begin
340 case(state)
341 idle:begin
342 if(sdram_init_done)
343 state <= sdram_done;//SDRAM初始化完成进入sdram_done状态
344 end
345 sdram_done:begin //在sdram_done状态对四个FIFO的读写操作进行判断
346 if(wrf_use0 >= wr_length*2) begin //进入写端FIFO0的读状态状态
347 sdram_wr_req <= 1;
348 sdram_wr_addr <= sdram_wr_addr0;
349 wr_fifo_flag <= 0;
350
351 sdram_rd_req <= 0;
352 sdram_rd_addr <= sdram_rd_addr0;
353 rd_fifo_flag <= 0;
354 state <= wr_keep;
355
356 end
357
358 else if(wrf_use1 >= wr_length*2) begin//进入写端FIFO1的读状态状态
359 sdram_wr_req <= 1;
360 sdram_wr_addr <= sdram_wr_addr1;
361 wr_fifo_flag <= 1;
362
363 sdram_rd_req <= 0;
364 sdram_rd_addr <= sdram_rd_addr0;
365 rd_fifo_flag <= 0;
366
367 state <= wr_keep;
368 end
369 else if((rdf_use0 < rd_length*2)//进入读端FIFO0的写状态状态
370 ) begin
371 sdram_wr_req <= 0;
372 sdram_wr_addr <= sdram_wr_addr0;
373 wr_fifo_flag <= 0;
374
375 sdram_rd_req <= 1;
376 sdram_rd_addr <= sdram_rd_addr0;
377 rd_fifo_flag <= 0;
378 state <= rd_keep;
379 end
380 else if((rdf_use1 < rd_length*2)//进入读端FIFO1的写状态状态
381 ) begin
382 sdram_wr_req <= 0;
383 sdram_wr_addr <= sdram_wr_addr0;
384 wr_fifo_flag <= 0;
385
386 sdram_rd_req <= 1;
387 sdram_rd_addr <= sdram_rd_addr1;
388 rd_fifo_flag <= 1;
389 state <= rd_keep;
390 end
391 end
392 wr_keep:begin
393 if(write_done_flag) begin //保持写状态
394 sdram_wr_req <= 0;
395 sdram_wr_addr <= sdram_wr_addr0;
396 wr_fifo_flag <= 0;
397 state <= sdram_done;
398 end
399 end
400 rd_keep:begin
401 if(read_done_flag) begin //保持读状态
402 sdram_rd_req <= 0;
403 sdram_rd_addr <= sdram_rd_addr0;
404 rd_fifo_flag <= 0;
405 state <= sdram_done;
406 end
407 end
408 default : state <= idle; //默认停在空闲状态
409 endcase
410 end
411 end
412
上面这段代码就是读写切换了,在前文我们说到这次我们在一个BANK上开辟了两帧数据大小的存储空间,用来分别存储两个摄像头的数据,所以在写SDRAM这端我们添加了两个写FIFO,两个摄像头的数据分别往自己的FIFO中写入数据,而当FIFO中存储的数据量大于2倍突发长度时就会开启SDRAM写操作,将数据写入SDRAM中如代码346~368所示。这里尤其要注意的是FIFO的容量一定要大于三倍突发长度,因为两个FIFO开启SDRAM写操作的判断条件是相同的,都是存储的数据量大于2倍突发长度时就会开启,双目摄像头几乎是同时开始工作的,这也就意味着两个写FIFO几乎是同时满足开启SDRAM写操作的判断条件(虽然宏观上几乎同时,但是肯定是有一个先一个后的),这就意味着其中一个FIFO满足条件后先开启SDRAM写操作,开启后状态机跳转到保持状态如代码354行和367行所示,这个时候必须完成一次突发长度的写操作才能跳出保持状态,那么另外一个FIFO此时的数据也已经超过了2倍突发长度,它必须等待当前FIFO完成写操作后,它才能开启SDRAM写操作,所以他要有足够的缓冲容量来等待当前FIFO完成写操作。因此我们将FIFO的容量设置为2048,刚好是四倍突发长度,有足够的缓冲容量来等待。
讲完了写再来看看读操作,如代码369~391行所示,其实读跟写完全是一模一样的机制,都是当读FIFO中存储的数据不满足两倍突发长度就开启SDRAM读操作,同样其中一个开启另一个就需要等待。
明白了SDRAM控制器的运行机制之后我们再来看看FIFO的例化,代码如下:
413 //例化写端口FIFO0
414 wrfifo u_wrfifo0(
415 //用户接口
416 .wrclk (clk_write0), //写时钟
417 .wrreq (wrf_wrreq0), //写请求
418 .data (wrf_din0), //写数据
419 //sdram接口
420 .rdclk (clk_ref), //读时钟
421 .rdreq (sdram_wr_ack0), //读请求
422 .q (sdram_din0), //读数据
423
424 .rdusedw (wrf_use0), //FIFO中的数据量
425 .aclr (~rst_n | wr_load_flag) //异步清零信号
426 );
427
428 //例化写端口FIFO1
429 wrfifo u_wrfifo1(
430 //用户接口
431 .wrclk (clk_write1), //写时钟
432 .wrreq (wrf_wrreq1), //写请求
433 .data (wrf_din1), //写数据
434 //sdram接口
435 .rdclk (clk_ref), //读时钟
436 .rdreq (sdram_wr_ack1), //读请求
437 .q (sdram_din1), //读数据
438
439 .rdusedw (wrf_use1), //FIFO中的数据量
440 .aclr (~rst_n | wr_load_flag) //异步清零信号
441 );
442
443 //例化读端口FIFO0
444 rdfifo u_rdfifo1(
445 //sdram接口
446 .wrclk (clk_ref), //写时钟
447 .wrreq (sdram_rd_ack1), //写请求
448 .data (sdram_dout1), //写数据
449
450 //用户接口
451 .rdclk (clk_read), //读时钟
452 .rdreq (rdf_rdreq1), //读请求
453 .q (rdf_dout1), //读数据
454
455 .wrusedw (rdf_use1), //FIFO中的数据量
456 .aclr (~rst_n | rd_load_flag) //异步清零信号
457 );
458 //例化读端口FIFO1
459 rdfifo u_rdfifo0(
460 //sdram接口
461 .wrclk (clk_ref), //写时钟
462 .wrreq (sdram_rd_ack0), //写请求
463 .data (sdram_dout0), //写数据
464
465 //用户接口
466 .rdclk (clk_read), //读时钟
467 .rdreq (rdf_rdreq0), //读请求
468 .q (rdf_dout0), //读数据
469
470 .wrusedw (rdf_use0), //FIFO中的数据量
471 .aclr (~rst_n | rd_load_flag) //异步清零信号
472 );
473 endmodule
FIFO例化是一个FIFO IP核被例化两次,当两个FIFO来用。SDRAM控制模块的代码到这里就讲完了,为了方便大家更好的去理解整个状态的跳转,下面给出了FIFO控制模块的原理示意图:
图 55.4.3 FIFO控制模块原理图
从上图中来分析FIFO控制模块的运行机制就简单的多了,双目摄像头分别往两个FIFO中写数据,FIFO数据存满两个突发长度后,其中FIFO1会交替往SDRAM的BANK0和BANK1中写数据,而FIFO2会交替往SDRAM的BANK2和BANK3中写数据,一个FIFO对应一个摄像头。读FIFO也一样,一个FIFO对应一个摄像头,比如“读FIFO1”它对应摄像头1,因此它就不断交替的从BANK0和BANK1的“空间”中读取数据,“读FIFO2”就不断从两个BANK2和BANK3的“空间”中读取数据,读取出来的数据最终传输到LCD显示屏上去显示,一个读FIFO的数据只占半个屏幕,这样两个摄像头的数据就能在同一个屏幕上一左一右的显示出来了。FIFO控制模块的整个运行机制就是这样的。
最后我们再来看看LCD显示模块作了哪些修改,修改代码如下:
1 module lcd_disply(
2 input lcd_clk, //lcd模块驱动时钟
3 input sys_rst_n, //复位信号
4 //RGB LCD接口
5 input [ 10:0] pixel_xpos, //像素点横坐标
6 input [ 10:0] pixel_ypos, //像素点纵坐标
7 input [15:0] lcd_id , //LCD的ID
8 input [15:0] rd_data, //图像像素值
9 input [12:0] rd_h_pixel, //摄像头输出的水平方向分辨率
10 output reg [15:0] pixel_data //像素点数据,
11 );
12
13 //LCD的ID
14 parameter ID_4342 = 16'h4342;
15 parameter ID_7084 = 16'h7084;
16 parameter ID_7016 = 16'h7016;
17 parameter ID_1018 = 16'h1018;
18 parameter ID_4384 = 16'h4384;
19 //颜色定义
20 localparam RED = 16'b11111_000000_00000; //字符颜色
21 localparam BLUE = 16'b00000_000000_11111; //字符区域背景色
22 localparam BLACK = 16'b00000_000000_00000; //屏幕背景色
23 //reg define
24 reg [63:0] char0[15:0]; //字符数组0
25 reg [63:0] char1[15:0]; //字符数组1
26 reg [127:0] char2[32:0]; //字符数组2
27 reg [127:0] char3[32:0]; //字符数组3
28
29 //给字符数组0的赋值:OV5640 0 (16*64)
30 always @(posedge lcd_clk) begin
31 char0[0] <= 64'h0000000000000000 ;
32 char0[1] <= 64'h0000000000000000 ;
33 char0[2] <= 64'h0000000000000000 ;
34 char0[3] <= 64'h38E77E1804180008 ;
35 char0[4] <= 64'h444240240C240038 ;
36 char0[5] <= 64'h824240400C420008 ;
37 char0[6] <= 64'h8244404014420008 ;
38 char0[7] <= 64'h8224785C24420008 ;
39 char0[8] <= 64'h8224446224420008 ;
40 char0[9] <= 64'h8228024244420008 ;
41 char0[10] <= 64'h822802427F420008 ;
42 char0[11] <= 64'h8218424204420008 ;
43 char0[12] <= 64'h4410442204240008 ;
44 char0[13] <= 64'h3810381C1F18003E ;
45 char0[14] <= 64'h0000000000000000 ;
46 char0[15] <= 64'h0000000000000000 ;
47 end
48
49 //给字符数组1的赋值: OV5640 1 (16*64)
50 always @(posedge lcd_clk) begin
51 char1[0] <= 64'h0000000000000000 ;
52 char1[1] <= 64'h0000000000000000 ;
53 char1[2] <= 64'h0000000000000000 ;
54 char1[3] <= 64'h38E77E180418003C ;
55 char1[4] <= 64'h444240240C240042 ;
56 char1[5] <= 64'h824240400C420042 ;
57 char1[6] <= 64'h8244404014420042 ;
58 char1[7] <= 64'h8224785C24420002 ;
59 char1[8] <= 64'h8224446224420004 ;
60 char1[9] <= 64'h8228024244420008 ;
61 char1[10] <= 64'h822802427F420010 ;
62 char1[11] <= 64'h8218424204420020 ;
63 char1[12] <= 64'h4410442204240042 ;
64 char1[13] <= 64'h3810381C1F18007E ;
65 char1[14] <= 64'h0000000000000000 ;
66 char1[15] <= 64'h0000000000000000 ;
67 end
68
69 //给字符数组2的赋值: OV5640 0 (32*128)
70 always @(posedge lcd_clk) begin
71 char2[0] <= 128'h00000000000000000000000000000000;
72 char2[1] <= 128'h00000000000000000000000000000000;
73 char2[2] <= 128'h00000000000000000000000000000000;
74 char2[3] <= 128'h00000000000000000000000000000000;
75 char2[4] <= 128'h00000000000000000000000000000000;
76 char2[5] <= 128'h00000000000000000000000000000000;
77 char2[6] <= 128'h03C07C1E0FFC01E0006003C000000080;
78 char2[7] <= 128'h0C30180C0FFC06180060062000000180;
79 char2[8] <= 128'h1818180810000C1800E00C3000001F80;
80 char2[9] <= 128'h100818081000081800E0181800000180;
81 char2[10] <= 128'h300C1808100018000160181800000180;
82 char2[11] <= 128'h300C0C10100010000160180800000180;
83 char2[12] <= 128'h60040C10100010000260300C00000180;
84 char2[13] <= 128'h60060C10100030000460300C00000180;
85 char2[14] <= 128'h60060C1013E033E00460300C00000180;
86 char2[15] <= 128'h60060C20143036300860300C00000180;
87 char2[16] <= 128'h60060620181838180860300C00000180;
88 char2[17] <= 128'h60060620100838081060300C00000180;
89 char2[18] <= 128'h60060620000C300C3060300C00000180;
90 char2[19] <= 128'h60060640000C300C2060300C00000180;
91 char2[20] <= 128'h60060340000C300C4060300C00000180;
92 char2[21] <= 128'h20060340000C300C7FFC300C00000180;
93 char2[22] <= 128'h300C0340300C300C0060180800000180;
94 char2[23] <= 128'h300C0380300C180C0060181800000180;
95 char2[24] <= 128'h10080180201818080060181800000180;
96 char2[25] <= 128'h1818018020180C1800600C3000000180;
97 char2[26] <= 128'h0C30010018300E3000600620000003C0;
98 char2[27] <= 128'h03C0010007C003E003FC03C000001FF8;
99 char2[28] <= 128'h00000000000000000000000000000000;
100 char2[29] <= 128'h00000000000000000000000000000000;
101 char2[30] <= 128'h00000000000000000000000000000000;
102 char2[31] <= 128'h00000000000000000000000000000000;
103 end
104
105 //给字符数组3的赋值: OV5640 1 (32*128)
106 always @(posedge lcd_clk) begin
107 char3[0] <= 128'h00000000000000000000000000000000;
108 char3[1] <= 128'h00000000000000000000000000000000;
109 char3[2] <= 128'h00000000000000000000000000000000;
110 char3[3] <= 128'h00000000000000000000000000000000;
111 char3[4] <= 128'h00000000000000000000000000000000;
112 char3[5] <= 128'h00000000000000000000000000000000;
113 char3[6] <= 128'h03C07C1E0FFC01E0006003C0000007E0;
114 char3[7] <= 128'h0C30180C0FFC06180060062000000838;
115 char3[8] <= 128'h1818180810000C1800E00C3000001018;
116 char3[9] <= 128'h100818081000081800E018180000200C;
117 char3[10] <= 128'h300C180810001800016018180000200C;
118 char3[11] <= 128'h300C0C1010001000016018080000300C;
119 char3[12] <= 128'h60040C10100010000260300C0000300C;
120 char3[13] <= 128'h60060C10100030000460300C0000000C;
121 char3[14] <= 128'h60060C1013E033E00460300C00000018;
122 char3[15] <= 128'h60060C20143036300860300C00000018;
123 char3[16] <= 128'h60060620181838180860300C00000030;
124 char3[17] <= 128'h60060620100838081060300C00000060;
125 char3[18] <= 128'h60060620000C300C3060300C000000C0;
126 char3[19] <= 128'h60060640000C300C2060300C00000180;
127 char3[20] <= 128'h60060340000C300C4060300C00000300;
128 char3[21] <= 128'h20060340000C300C7FFC300C00000200;
129 char3[22] <= 128'h300C0340300C300C0060180800000404;
130 char3[23] <= 128'h300C0380300C180C0060181800000804;
131 char3[24] <= 128'h10080180201818080060181800001004;
132 char3[25] <= 128'h1818018020180C1800600C300000200C;
133 char3[26] <= 128'h0C30010018300E300060062000003FF8;
134 char3[27] <= 128'h03C0010007C003E003FC03C000003FF8;
135 char3[28] <= 128'h00000000000000000000000000000000;
136 char3[29] <= 128'h00000000000000000000000000000000;
137 char3[30] <= 128'h00000000000000000000000000000000;
138 char3[31] <= 128'h00000000000000000000000000000000;
139 end
140
141 //显示逻辑判断
142 always@(*) begin
143 if ( pixel_ypos >= 0 && pixel_ypos < 33)begin
144 if(pixel_xpos < (rd_h_pixel[12:2]+64)
145 && pixel_xpos >= (rd_h_pixel[12:2]-64) )begin
146 if(char2[pixel_ypos][127-(pixel_xpos-rd_h_pixel[12:2]+64)])
147 pixel_data =BLUE;
148 else
149 pixel_data = rd_data;
150 end
151 else if(pixel_xpos < (rd_h_pixel[12:2]*3+64)
152 && pixel_xpos >= (rd_h_pixel[12:2]*3-64))begin
153 if(char3[pixel_ypos][63-pixel_xpos+(rd_h_pixel[12:2])*3])
154 pixel_data =BLUE;
155 else
156 pixel_data = rd_data;
157 end
158 else
159 pixel_data = rd_data;
160 end
161 else
162 pixel_data = rd_data;
163 end
164
165 endmodule
其实LCD显示部分主要就是加了一个字符显示,因为我们的两个摄像头是一左一右显示在LCD屏幕上的,为了确定哪个摄像头显示在哪边我们会在显示画面上标注“OV5640 1”和“OV5640 2”字样。所以我们在LCD显示部分中加入了字符显示模块(lcd_disply)。代码30~139行就是生成“OV5640 1”和“OV5640 2”这两个字符串的字模,代码142~163行就是把“OV5640 1”和“OV5640 2”这两个字符串显示到屏幕上去。具体的显示原理这里就不再详细介绍了,如果大家有不懂的可以去看前面LCD字符显示实验和RTC实时时钟显示实验。
到这里本节双目摄像头LCD显示实验的程序设计部分就讲解完了,接下来就可以把程序下载到开发板上去验证了。
1.5 下载验证
首先我们将双目摄像头插到开发板的P7扩展口,然后连接LCD显示屏,最后打开电源,下载sof文件到开发板上去,硬件连接图如下所示:
图 55.5.1 硬件连接
显示的效果如上图所示。
这篇关于【正点原子FPGA连载】第五十五章 双目OV5640摄像头RGB-LCD显示实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!