OV7670/OV7725/OV5640开发记录(3):DVP端口输出

  SCCB 配置好后,cmos 的 DVP 端口就有数据出来了,怎么设计时序获取图像呢?

一、DVP端口

cmos_capture u_cmos_capture
(
    .clk_24m                (clk_24m                ),
    .cmos_pclk              (cmos_pclk              ),
    .rst_n                  (rst_n & sccb_cfg_done  ),
    .cmos_vsync             (cmos_vsync             ),
    .cmos_href              (cmos_href              ),
    .cmos_data              (cmos_data              ),
    .frame_vsync            (                       ),
    .frame_hsync            (                       ),
    .rgb565_vld             (                       ), // 640x480
    .rgb565_data            (                       ),
    .rgb_vld                (rgb_vld                ), // 480x272
    .rgb_data               (rgb_data               ),
    .fps_rate               (fps_rate               )
);

  端口的输出包括:图像数据,图像数据使能,fps数值。其中数据和使能又分为原版 640x480 和 裁剪后的 480x272。(如果是上一讲配置的ov5640则为1024x768→480x272)

二、去除前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 再计算得结果,比较麻烦。我们可以通过简单的计数器,实现分辨率裁剪的效果。以 640x480 裁剪为 480x272 为例,取中间的部分,边上的舍去,代码如下所示:

parameter H_START            = 12'd79                ; //裁剪后的宽度起始像素
parameter H_STOP             = 12'd559               ; //裁剪后的宽度结束像素
parameter V_START            = 12'd103               ; //裁剪后的高度起始像素
parameter V_STOP             = 12'd375               ; //裁剪后的高度结束像素 
//==========================================================================
//==    分辨率裁剪:640x480 -> 480x272
//==========================================================================
//行计数
//---------------------------------------------------
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== 640-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== 480-1;

//裁剪后的数据:适配TFT屏
//---------------------------------------------------
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
//==========================================================================
//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.

原文地址:https://www.cnblogs.com/xianyufpga/p/12933592.html