CMOS摄像头(3):DVP端口输出
SCCB 配置好后,cmos 的 DVP 端口就有数据出来了,怎么设计时序获取图像呢?
一、DVP端口
//========================================================================== //== 获得摄像头数据,转RGB565并裁剪分辨率 //========================================================================== cmos_capture #( .CMOS_H_PIXEL (CMOS_H_PIXEL ), //CMOS输出宽度 .CMOS_V_PIXEL (CMOS_V_PIXEL ), //CMOS输出高度 .H_DISP (H_DISP ), //图像宽度 .V_DISP (V_DISP ) //图像高度 ) u_cmos_capture ( .clk_24m (clk_24m ), //24Mhz .cmos_pclk (cmos_pclk ), //cmos数据像素时钟 .rst_n (rst_n & sccb_cfg_done ), //复位,低电平有效 //cmos ------------------------------------------ .cmos_href (cmos_href ), //CMOS 行同步信号 .cmos_vsync (cmos_vsync ), //CMOS 场同步信号 .cmos_data (cmos_data ), //CMOS 像素数据 //RGB ------------------------------------------- .RGB_vld (RGB_vld ), //RGB数据使能 .RGB_data (RGB_data ), //RGB数据 .FPS_rate (FPS_rate ) //FPS帧率 );
端口的输出包括:图像数据,图像数据使能,FPS数值。
CMOS_H_PIXEL:CMOS输出的图像宽度;
CMOS_V_PIXEL:CMOS输出的图像高度;
H_DISP:最终希望显示出的图像宽度,由top层传入;
V_DISP:最终希望显示出的图像高度,由top层传入。
二、去除前10帧
根据数据手册说的,前10帧图像数据不稳定,因此一般都是丢掉。代码设计很简单,找到 cmos_vsync 的上升沿计数,只计一次,计满 10 帧后的数据才是有效数据,如下所示:
//========================================================================== //== 打拍,以供后面程序使用 //========================================================================== always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin cmos_vsync_r1 <= 1'b0; cmos_vsync_r2 <= 1'b0; cmos_href_r1 <= 1'b0; cmos_href_r2 <= 1'b0; end else begin cmos_vsync_r1 <= cmos_vsync; cmos_vsync_r2 <= cmos_vsync_r1; cmos_href_r1 <= cmos_href; cmos_href_r2 <= cmos_href_r1; end end //========================================================================== //== 前10帧图像数据不稳定,丢弃掉 //========================================================================== //vsync上升沿 //--------------------------------------------------- assign cmos_vsync_pos = (~cmos_vsync_r1 & cmos_vsync); //帧有效信号,去除前10帧 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin frame_cnt <= 'd0; end else if(cmos_vsync_pos && frame_vld==1'b0) begin frame_cnt <= frame_cnt + 1'b1; end end assign frame_vld = (frame_cnt >= WAIT) ? 1'b1 : 1'b0;
其中,WAIT的数值为 10,frame_vld 为有效数据指示信号,前10帧时为低,之后一直为高。
三、数据拼接
ov7725 的数据手册中有如下一张图,说明了配置成 RGB565 时的时序情况。摄像头的数据为 8bit,是按照 RAW 像素设计的,当我们配置成 RGB565 格式输出时,需要两个 8bit 才能表示一个 16bit 的 RGB565 像素,因此需要对输出的像素进行人为的拼接,典型的时序如下所示:
RGB565的排列顺序也是通过寄存器设置的,也有别的排列顺序,我这设置的就是如图的格式。时序设计如下所示:
我们设计一个指示信号 byte_flag,不断的对原始数据 ov_5640_data 进行拼接,并设计 vld 信号表明输出有效标志。这个时序图是我之前学习开源骚客的教学视频时画的,和我下面贴出的代码信号名字有一点点不一样,但大体是相同的。代码如下所示:
//========================================================================== //== 两个原始数据拼成一个RGB565像素 //========================================================================== //字节指示 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin byte_flag <= 1'b0; end else if(cmos_href) begin byte_flag <= ~byte_flag; end else begin byte_flag <= 1'b0; end end //rgb_data //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin rgb565_data <= 'h0; end else if(byte_flag == 1'b0) begin //first byte rgb565_data <= {cmos_data, rgb565_data[7:0]}; end else if(byte_flag == 1'b1) begin //second byte rgb565_data <= {rgb565_data[15:8], cmos_data}; end end //rgb_vld //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) begin rgb565_vld <= 1'b0; end else if(frame_vld && byte_flag) begin rgb565_vld <= 1'b1; end else begin rgb565_vld <= 1'b0; end end
四、行场有效信号
经过处理,行场信号需要改变才能和处理后的数据完美对齐,代码如下所示:
//========================================================================== //== 输出行场有效信号 //========================================================================== assign frame_vsync = frame_vld ? cmos_vsync_r2 : 1'b0; assign frame_hsync = frame_vld ? cmos_href_r2 : 1'b0;
五、分辨率裁剪
上一讲博客中提到,可以通过寄存器的配置改变输出的图像分辨率,但寄存器的配置需要查询 datasheet 再计算得结果,比较麻烦。我们可以通过简单的计数器,实现分辨率裁剪的效果。从传入的参数,进行简单计算,取中间的部分,边上的舍去,代码如下所示:
//========================< 参数 >========================================== #( parameter CMOS_H_PIXEL = 12'd640 , //CMOS输出宽度 parameter CMOS_V_PIXEL = 12'd480 , //CMOS输出高度 parameter H_DISP = 12'd480 , //图像宽度 parameter V_DISP = 12'd272 //图像高度 ) //========================================================================== //== 分辨率裁剪 //========================================================================== //行计数 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) cnt_h <= 'd0; else if(add_cnt_h) begin if(end_cnt_h) cnt_h <= 'd0; else cnt_h <= cnt_h + 1'b1; end end assign add_cnt_h = RGB565_vld; assign end_cnt_h = add_cnt_h && cnt_h== CMOS_H_PIXEL-1; //场计数 //--------------------------------------------------- always @(posedge cmos_pclk or negedge rst_n) begin if(!rst_n) cnt_v <= 'd0; else if(add_cnt_v) begin if(end_cnt_v) cnt_v <= 'd0; else cnt_v <= cnt_v + 1'b1; end end assign add_cnt_v = end_cnt_h; assign end_cnt_v = add_cnt_v && cnt_v== CMOS_V_PIXEL-1; //裁剪后的数据:适配显示屏 //--------------------------------------------------- assign RGB_data = RGB565_data; assign RGB_vld = RGB565_vld && (cnt_h >= H_START) && (cnt_h < H_STOP) && (cnt_v >= V_START) && (cnt_v < V_STOP);
这样得到的 rgb_data 和 rgb_vld 就是裁剪后的数据了,且只是裁剪,数据本身没有移位,总体时序没有变化。
六、帧率FPS计算
通过巧妙的设计,我们就能够实时的得到当前 fps 的数值。其思想很简单,即计算 1s 时间内,来了多少次 cmos_vsync 即可。具体设计思想如下所示:
(1)通过一个确定的时钟对 cmos_vsync 进行打拍,求得其上升沿;
(2)通过该确定的时钟再进行 1s 时间的计数,每当计满 1s 则清0重新计数;
(3)对(1)求得的 cmos_vsync 上升沿进行计数,每当(2)的 1s 计满时,清 0 重新计数;
(4)对(3)在 1s 计满时那一刻的 cmos_vsync 上升沿数目寄存并输出,得到 fps 值。
这样看文字比较麻烦,好像很难一样,上代码吧:
//========================================================================== //== 帧率计算,不能用pclk时钟,需重新捕捉vsync_pos //========================================================================== //输出行场有效信号 //--------------------------------------------------- assign frame_vsync = frame_vld ? cmos_vsync_r2 : 1'b0; assign frame_hsync = frame_vld ? cmos_href_r2 : 1'b0; //vsync上升沿 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) frame_vsync_r <= 1'b0; else frame_vsync_r <= frame_vsync; end assign frame_vsync_pos = (~frame_vsync_r & frame_vsync); //1s时间 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cnt_1s <= 'd0; else if(add_cnt_1s) begin if(end_cnt_1s) cnt_1s <= 'd0; else cnt_1s <= cnt_1s + 1'b1; end end assign add_cnt_1s = frame_vld; assign end_cnt_1s = add_cnt_1s && cnt_1s== TIME_1S-1; //1s时间内的vsync次数 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) cnt_FPS <= 'd0; else if(end_cnt_1s) begin cnt_FPS <= 'd0; end else if(frame_vld && frame_vsync_pos)begin cnt_FPS <= cnt_FPS + 'd1; end end //实时更新帧率值 //--------------------------------------------------- always @(posedge clk_24m or negedge rst_n) begin if(!rst_n) begin FPS_rate <= 'd0; end else if(end_cnt_1s) begin FPS_rate <= cnt_FPS; end end
注释中特别提到,不能用 Pclk 来计算,必须是外部引入的确定的时钟。这点在 CrazyBingo 韩彬的代码中没有处理好,算是一个小 bug。最后我们将帧率值 fps_rate 引到端口,输送给数码管显示模块,就能够实时的知道当前采集的图像帧率值了。
至此,摄像头模块的部分算是讲解完了,讲得很粗,大把的贴代码。一是因为网上关于这方面的资料是在是太多了,讲得都比我总结的好,二是这些代码其实大部分都是我改的别家的,不算原创,所以贴出来。
还没有结束,下一讲我再整理一下摄像头显示工程中,摄像头以外的一些关键点。
参考资料:
[1]正点原子FPGA教程
[2]小梅哥《OV5640图像采集从原理到应用》
[3]开源骚客《SDRAM那些事儿》
[4]韩彬, 于潇宇, 张雷鸣. FPGA设计技巧与案例开发详解[M]. 电子工业出版社, 2014.