• 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.

  • 相关阅读:
    各种redis的介绍:ServiceStack.Redis,StackExchange.Redis,CSRedis
    nginx 配置web服务
    安装Office Online Server
    买房哪些事?
    微服务演变:微服务架构介绍
    VUE 前端调用Base64加密,后端c#调用Base64解密
    程序员35岁前必须做完的事
    Vue 开发流程
    小程序快速认证
    vue页面打印成pdf
  • 原文地址:https://www.cnblogs.com/xianyufpga/p/12933592.html
Copyright © 2020-2023  润新知