FPGA采集摄像头数据,经过中间缓存,最后输出到屏幕上,这个工程几乎是所有FPGAer都要经历的工程。曾听人说过,如果能独立的做出摄像头显示工程,那么就代表他的FPGA终于入门了。
这次,我准备将目前市面上最常用的三款摄像头——OV7670、OV7725、OV5640的开发全过程全部记录下来,并且提供所有代码,若日后需要做相关项目,也方便自己回顾,迅速捡起来。OV7670和OV7725都是30w像素级摄像头,其典型输出为640x480@30fps(VGA),各方面时序也完全一致,仅仅是摄像头配置不同,OV7725可以说是OV7670的升级版,前者比后者的成像效果好很多。而OV5640为500w像素级摄像头,最高支持 2592x1944@15fps(QSXGA)的图像输出。
一、硬件电路(by小梅哥AC620)
如下是常用的CMOS硬件电路。
共有20个引脚,其解释如下所示:
二、内部结构
内部结构有些小复杂,直接说重点。摄像头采集图像,经过内部一系列的处理,最终通过端口输出,输出端口有几种,如DVP、MIPI、LVDS、CSI等,我们一般用的是DVP接口,有些模块的DVP是10位的,我们取高8位即可,舍弃掉了低2位。
三、上电配置时序分析
1、OV7670/OV7725
OV7670 和 OV7725 的数据手册中并没有出现上电的时序图,但是给出了一条信息:Setting time after software/hardware reset:1ms。所谓软件复位说的是寄存器复位,摄像头寄存器很多,有一个寄存器有复位功能,对其写入复位操作可以达到软件复位效果。而硬件复位指的就是硬件电路图上的 RST 信号的复位操作。
在一本名为《OV7670 照相模组硬件应用指南》的PDF文件中倒是给出了上电时序图,但是感觉参数和原版的 datasheet 有些出入,所以我没有采用。
经过上板我得出一些结论:
(1)cmos_pwdn 信号直接赋 0 即可。
(2)cmos_rst_n 信号直接赋 1 即可。
(3)rst_n 赋 1 后,必须延时 1ms 后再进行 SCCB 配置。
2、OV5640
OV5640的上电时序在其 datasheet 中明确给出了,如下所示:
注意 DOVDD 和 AVDD 是 OV5640 器件内部就已经设计好的,不用自己设计。
经过上板发现,cmos_pwdn 信号不延时直接赋 0 也是可以的。总结如下:
(1)cmos_pwdn直接赋0即可。
(2)cmos_rst_n 信号延时1ms后赋 1 即可。
(3)cmos_rst_n信号赋 1 后,延时 20ms 后才能再进行SCCB配置。
3、时钟Xclk
OV7670、OV7725、OV5640的输入时钟Xclk,一般都建议为 24Mhz,用 FPGA 的 PLL 分频到 24Mhz 给它就行,摄像头内部有自己的 PLL,会按照内部设计供给其内部各个模块使用,使得摄像头能正常工作。关于Pclk,我们后面再说。
四、代码展现
1、总体架构
总体架构如上所示,解释如下:
(1)pll:时钟分频模块
(2)ov7725_top:摄像头的顶层模块
(3)sdram_top:SDRAM图像缓存模块
(4)TFT_driver:TFT屏显示模块
(5)SEG_driver:数码管显示帧率模块
本系列模块只重点讨论第二个 ov7725_top 模块,其他模块前面的博客都有说过,其实就是搭积木而已。
2、工程顶层代码
顶层模块都差不多,就是端口和 pll ,其他的都是别的部分了。
1 //************************************************************************** 2 // *** 名称 : top.v 3 // *** 作者 : xianyu_FPGA 4 // *** 博客 : https://www.cnblogs.com/xianyufpga/ 5 // *** 日期 : 2020-5-20 6 // *** 工具 : Quartus 13.0 7 // *** 芯片 : Cyclone IV E 8 // *** 型号 : EP4CE10F17C8 9 // *** 描述 : 工程的顶层模块 10 //************************************************************************** 11 module top 12 //========================< 端口 >========================================== 13 ( 14 input clk , 15 input rst_n , 16 //ov7670 -------------------------------------------- 17 input cmos_pclk , 18 output cmos_xclk , 19 input cmos_vsync , 20 input cmos_href , 21 input [ 7:0] cmos_data , 22 output cmos_pwdn , 23 output cmos_rst_n , 24 output cmos_scl , 25 inout cmos_sda , 26 //sdram --------------------------------------------- 27 output sdram_clk , 28 output sdram_cke , 29 output sdram_cs_n , 30 output sdram_we_n , 31 output sdram_cas_n , 32 output sdram_ras_n , 33 output [ 1:0] sdram_dqm , 34 output [ 1:0] sdram_ba , 35 output [12:0] sdram_addr , 36 inout [15:0] sdram_dq , 37 //TFT ----------------------------------------------- 38 output TFT_clk , 39 output TFT_de , 40 output TFT_pwm , 41 output TFT_hsync , 42 output TFT_vsync , 43 output [15:0] TFT_data , 44 //Segment ------------------------------------------- 45 output SH_CP , 46 output ST_CP , 47 output DS 48 ); 49 //========================< 信号 >========================================== 50 wire clk_100m ; 51 wire clk_100m_shift ; 52 wire clk_24m ; 53 wire clk_10m ; 54 //SDRAM --------------------------------------------- 55 wire sdram_init_done ; 56 wire wr_en ; //SDRAM 写使能 57 wire [15:0] wr_data ; //SDRAM 写数据 58 wire rd_en ; //SDRAM 读使能 59 wire [15:0] rd_data ; //SDRAM 读数据 60 //Segment ------------------------------------------- 61 wire [ 7:0] fps_rate ; 62 //========================================================================== 63 //== PLL 64 //========================================================================== 65 pll pll 66 ( 67 .inclk0 (clk ), 68 .c0 (clk_24m ), //CMOS xclk 69 .c1 (clk_100m ), //SDRAM 70 .c2 (clk_100m_shift ), //SDRAM 71 .c3 (clk_10m ) //TFT 72 ); 73 //========================================================================== 74 //== ov7725,已裁剪为480x272 75 //========================================================================== 76 ov7725_top u_ov7725_top 77 ( 78 .clk_24m (clk_24m ), 79 .rst_n (rst_n & sdram_init_done), //SDRAM复位后 80 //cmos ------------------------------------------ 81 .cmos_pclk (cmos_pclk ), 82 .cmos_xclk (cmos_xclk ), 83 .cmos_vsync (cmos_vsync ), 84 .cmos_href (cmos_href ), 85 .cmos_data (cmos_data ), 86 .cmos_rst_n (cmos_rst_n ), 87 .cmos_pwdn (cmos_pwdn ), 88 .cmos_scl (cmos_scl ), 89 .cmos_sda (cmos_sda ), 90 //rgb565 ---------------------------------------- 91 .rgb_vld (wr_en ), //rgb数据指示 92 .rgb_data (wr_data ), //rgb数据 93 .fps_rate (fps_rate ) //fps帧率 94 ); 95 //========================================================================== 96 //== SDRAM 97 //========================================================================== 98 sdram_top u_sdram_top 99 ( 100 .ref_clk (clk_100m ), //SDRAM 控制器参考时钟 101 .out_clk (clk_100m_shift ), //给SDRAM器件的偏移时钟 102 .rst_n (rst_n ), //系统复位 103 //用户写端口 ------------------------------------ 104 .wr_clk (cmos_pclk ), //写端口FIFO: 写时钟 105 .wr_en (wr_en ), //写端口FIFO: 写使能 106 .wr_data (wr_data ), //写端口FIFO: 写数据 107 .wr_min_addr (24'd0 ), //写SDRAM的起始地址 108 .wr_max_addr (480*272 ), //写SDRAM的结束地址 109 .wr_len (10'd512 ), //写SDRAM时的数据突发长度 110 .wr_load (~rst_n ), //写端口复位: 复位写地址,清空写FIFO 111 //用户读端口 ------------------------------------ 112 .rd_clk (clk_10m ), //读端口FIFO: 读时钟 113 .rd_en (rd_en ), //读端口FIFO: 读使能 114 .rd_data (rd_data ), //读端口FIFO: 读数据 115 .rd_min_addr (24'd0 ), //读SDRAM的起始地址 116 .rd_max_addr (480*272 ), //读SDRAM的结束地址 117 .rd_len (10'd512 ), //从SDRAM中读数据时的突发长度 118 .rd_load (~rst_n ), //读端口复位: 复位读地址,清空读FIFO 119 //用户控制端口 ---------------------------------- 120 .sdram_init_done (sdram_init_done ), //SDRAM 初始化完成标志 121 .sdram_pingpang_en (1'b1 ), //SDRAM 乒乓操作使能,1开0关 122 //SDRAM 芯片接口 -------------------------------- 123 .sdram_clk (sdram_clk ), //SDRAM 芯片时钟 124 .sdram_cke (sdram_cke ), //SDRAM 时钟有效 125 .sdram_cs_n (sdram_cs_n ), //SDRAM 片选 126 .sdram_ras_n (sdram_ras_n ), //SDRAM 行有效 127 .sdram_cas_n (sdram_cas_n ), //SDRAM 列有效 128 .sdram_we_n (sdram_we_n ), //SDRAM 写有效 129 .sdram_ba (sdram_ba ), //SDRAM Bank地址 130 .sdram_addr (sdram_addr ), //SDRAM 行/列地址 131 .sdram_dq (sdram_dq ), //SDRAM 数据 132 .sdram_dqm (sdram_dqm ) //SDRAM 数据掩码 133 ); 134 //========================================================================== 135 //== TFT 136 //========================================================================== 137 TFT_driver u_TFT_driver 138 ( 139 .clk (clk_10m ), 140 .rst_n (rst_n ), 141 .TFT_req (rd_en ), 142 .TFT_x ( ), 143 .TFT_y ( ), 144 .TFT_din (rd_data ), 145 .TFT_clk (TFT_clk ), 146 .TFT_de (TFT_de ), 147 .TFT_pwm (TFT_pwm ), 148 .TFT_hsync (TFT_hsync ), 149 .TFT_vsync (TFT_vsync ), 150 .TFT_data (TFT_data ) 151 ); 152 //========================================================================== 153 //== 数码管显示帧率 154 //========================================================================== 155 SEG_driver u_SEG_driver 156 ( 157 .clk (clk_100m ), 158 .rst_n (rst_n ), 159 .en (1 ), 160 .value (fps_rate ), //fps帧率值 161 .SH_CP (SH_CP ), 162 .ST_CP (ST_CP ), 163 .DS (DS ) 164 ); 165 166 167 168 169 endmodule
这里补充一个知识点:FPGA时钟为50Mhz,摄像头需要 24Mhz,SDRAM需要两个100Mhz,而 TFT 屏的推荐时钟是 9Mhz,但 pll 已经无法分出 9Mhz 了,因此我分了一个近视的 10Mhz 代替 9Mhz,最终显示也没有任何问题。
给出的是 ov7725_top,其实 ov7670、ov5640的顶层例化也是完全一样的。
很多人的摄像头工程喜欢把一些简单的信号如 cmos_pwdn 和 cmos_rst_n 信号,在顶层模块中就直接赋出去,这也是可以的。而我的本次设计中,工程顶层模块中没有任何代码,不管信号复杂与否都严格分块设计,全都写进了内部模块里,这样生成的 rtl 视图更简洁,各个模块的配合也更直观。
3、摄像头顶层代码
(1)OV7670和OV7725
前面说过多次,这两货是一样的,cmos_pwdn 信号和 cmos_rst_n 信号都可以直接赋值,而 cmos_rst_n 信号拉高后,必须延时 1ms 后才能进行 SCCB 配置,代码如下所示:
//========================================================================== //== cmos简单信号 //========================================================================== assign cmos_xclk = clk_24m; //24MHz CMOS XCLK output assign cmos_pwdn = 1'b0; //非节电模式,即正常模式 assign cmos_rst_n = 1'b1; //复位信号,可不用延时 /* always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cmos_rst_n <= 1'b0; else cmos_rst_n <= 1'b1; end */ //========================================================================== //== SCCB驱动和配置 //========================================================================== //延时1ms再进行SCCB配置 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) delay_cnt <= 'b0; else if(delay_cnt <= 24000) delay_cnt <= delay_cnt + 1'b1; end assign sccb_vld = delay_cnt == 24001;
(2)OV5640
OV5640的 cmos_pwdn 信号可以直接赋值,cmos_rst_n 信号却必须延时 1ms 后才能拉高,拉高后再延时 20ms 后才能进行 SCCB 配置,代码如下所示:
//========================================================================== //== cmos简单信号 //========================================================================== //24MHz CMOS XCLK output //--------------------------------------------------- assign cmos_xclk = clk_24m; //非节电模式,即正常模式,不延时也有用 //--------------------------------------------------- assign cmos_pwdn = 1'b0; //延时计数器 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) delay_cnt <= 'b0; else if(delay_cnt <= 504000) delay_cnt <= delay_cnt + 1'b1; end //复位信号,至少延时1ms //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cmos_rst_n <= 1'b0; else if(delay_cnt==240000) cmos_rst_n <= 1'b1; end //========================================================================== //== SCCB驱动和配置 //========================================================================== //至少再延时20ms再进行SCCB配置 //--------------------------------------------------- assign sccb_vld = delay_cnt==504001;
OK,本篇博客就到这,下一篇讲解 SCCB 配置是怎么回事。
参考资料:
[1]正点原子FPGA教程
[2]小梅哥《OV5640图像采集从原理到应用》
[3]开源骚客《SDRAM那些事儿》
[4]韩彬, 于潇宇, 张雷鸣. FPGA设计技巧与案例开发详解[M]. 电子工业出版社, 2014.