跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
编程语言

FPGA 实现 OV5640 摄像头视频图像显示

基于 FPGA 使用 Verilog 实现 OV5640 摄像头视频采集与 VGA 显示的系统设计。核心模块包含 SCCB 控制器用于寄存器初始化,DVP 接口负责数据采集,RAM 进行帧缓存,VGA 控制器驱动显示屏。系统支持 640x480 分辨率输入及 RGB565 格式输出,通过状态机控制初始化流程。实际显示区域为 350x218,存在轻微画面撕裂问题,建议采用乒乓缓存优化。

云间漫步发布于 2026/4/5更新于 2026/5/2322 浏览
FPGA 实现 OV5640 摄像头视频图像显示

一、工程介绍

OV5640 摄像头通过 DVP 接口输出视频图像数据,并通过 VGA 接口输出给显示器。FPGA 需要完成的功能包括:OV5640 初始化、DVP 接口数据采集、图像数据缓存、VGA 数据输出。模块设计也相应按照这四个部分进行划分。

二、Verilog 实现

(1)OV5640 初始化

(1.1)SCCB 控制器

ov5640 摄像头初始化需要向其内部配置寄存器写入数据进行配置,实现对图像数据格式、图像大小、图像反转镜像、曝光、补偿等设置。对 ov5640 寄存器的读写操作需要遵循 SCCB 通信协议,不过 SCCB 与 I2C 协议很相似,SCCB 可以兼容 I2C,只是时序细节有区别,不过我工程中还是单独设计了一个 SCCB 控制器。

`timescale 1ns / 1ps // 适用于 ov5640 SCCB 通信 寄存器(字节)地址 16 位,数据 8 位
module SCCB_ctrl(
    input wire clk,           //系统时钟 100MHz
    input wire rst_n,         //复位
    inout wire sda,           //双向数据线(inout)
    output wire scl,          //输出时钟线
    input wire rw_ctrl,       //读写使能信号(0 写 1 读)
    input wire work_start,    //SCCB 启动信号
    input wire [6:0] slave_addr,//7bit 从设备地址
    input wire [15:0] byte_addr,//16bit 字地址
    input wire [7:0] w_data,  //8bit 待写数据
    output reg [7:0] r_data,  //8bit 读取数据
    output reg work_done      //SCCB 读写完成信号
);
    //sda 传输方向控制
    reg sda_oe;               //sda 输出使能,为 1 表示 sda 作输出
    reg sda_out;              //sda 输出信号线
    wire sda_in;              //sda 输入寄存器
    assign sda_in = sda;
    assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz;
    
    //状态机参数
    reg [4:0] state;
    localparam IDLE = 5'd0,
               START = 5'd1,
               W_SLAVE_ADDR = 5'd2,
               ACK1 = 5'd3,
               W_H_BYTE_ADDR = 5'd4,
               ACK2 = 5'd5,
               W_L_BYTE_ADDR = 5'd6,
               ACK3 = 5'd7,
               STOP = 5'd8,
               W_DATA = 5'd9,
               W_ACK = 5'd10,
               STOP2 = 5'd11,
               START2 = 5'd12,
               R_SLAVE_ADDR = 5'd13,
               R_ACK = 5'd14,
               R_DATA = 5'd15,
               N_ACK = 5'd16;
    
    //计数器及参数
    reg clk_div;
    reg [7:0] cnt_clk;
    reg [3:0] cnt_bit;
    localparam cnt_max_400khz = 8'd125;
    wire scl_half_1;
    wire scl_half_0;
    wire scl_ack_jump;
    assign scl_half_1 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1);
    assign scl_half_0 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);
    assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0);
    
    //数据寄存器
    reg [7:0] w_data_buf;
    reg [7:0] r_data_buf;
    reg [7:0] w_slave_addr_buf;
    reg [7:0] r_slave_addr_buf;
    reg [7:0] H_byte_addr_buf;
    reg [7:0] L_byte_addr_buf;
    reg work_en;
    
    //*************************************** MAIN CODE ***************************************//
    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            w_slave_addr_buf <= 8'b0000_0000;
            r_slave_addr_buf <= 8'b0000_0001;
            H_byte_addr_buf <= 8'b0;
            L_byte_addr_buf <= 8'b0;
            w_data_buf <= 8'b0;
        end else if (work_start) begin
            w_slave_addr_buf [7:1] <= slave_addr;
            r_slave_addr_buf [7:1] <= slave_addr;
            w_data_buf <= w_data;
            H_byte_addr_buf <= byte_addr[15:8];
            L_byte_addr_buf <= byte_addr[7:0];
        end
    
    always @(posedge clk or negedge rst_n)
        if (!work_en || !rst_n) begin
            cnt_clk <= 8'd1;
            clk_div <= 1'b1;
        end else if (cnt_clk == cnt_max_400khz) begin
            cnt_clk <= 8'd1;
            clk_div <= ~clk_div;
        end else cnt_clk <= cnt_clk + 8'd1;
    assign scl = clk_div;
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            state <= IDLE;
            sda_oe <= 1'b0;
            sda_out <= 1'b1;
            work_en <= 1'b0;
            work_done <= 1'b0;
            cnt_bit <= 4'd0;
        end else case(state)
            IDLE: begin
                sda_oe <= 1'b0;
                sda_out <= 1'b1;
                work_done <= 1'b0;
                if (work_start) begin
                    work_en <= 1'b1;
                    state <= START;
                end
            end
            START: begin
                sda_oe <= 1'b1;
                if (scl_half_1) begin
                    sda_out <= 1'b0;
                    state <= W_SLAVE_ADDR;
                end
            end
            W_SLAVE_ADDR: begin
                sda_oe <= 1'b1;
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_slave_addr_buf[7-cnt_bit];
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state <= ACK1;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            ACK1: begin
                sda_oe <= 1'b0;
                if (scl_ack_jump) state <= W_H_BYTE_ADDR;
            end
            W_H_BYTE_ADDR: begin
                sda_oe <= 1'b1;
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= H_byte_addr_buf[7-cnt_bit];
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state <= ACK2;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            ACK2: begin
                sda_oe <= 1'b0;
                if (scl_ack_jump) state <= W_L_BYTE_ADDR;
            end
            W_L_BYTE_ADDR: begin
                sda_oe <= 1'b1;
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= L_byte_addr_buf[7-cnt_bit];
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state <= ACK3;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            ACK3: begin
                sda_oe <= 1'b0;
                if (scl_ack_jump) begin
                    state <= rw_ctrl ? STOP2 : W_DATA;
                    sda_out <= rw_ctrl ? 1'b0 : sda_out;
                end
            end
            STOP: begin
                sda_oe <= 1'b1;
                if (scl_half_1) begin
                    sda_out <= 1'b1;
                    work_done <= 1'b1;
                    work_en <= 1'b0;
                    state <= IDLE;
                end
            end
            W_DATA: begin
                sda_oe <= 1'b1;
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_data_buf[7-cnt_bit];
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state <= W_ACK;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            W_ACK: begin
                sda_oe <= 1'b0;
                if (scl_ack_jump) begin
                    sda_out <= 1'b0;
                    state <= STOP;
                end
            end
            STOP2: begin
                sda_oe <= 1'b1;
                if (scl_half_1) begin
                    sda_out <= 1'b1;
                    state <= START2;
                end
            end
            START2: begin
                sda_oe <= 1'b1;
                if (scl_half_1) begin
                    sda_out <= 1'b0;
                    state <= R_SLAVE_ADDR;
                end
            end
            R_SLAVE_ADDR: begin
                sda_oe <= 1'b1;
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= r_slave_addr_buf[7-cnt_bit];
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state <= R_ACK;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            R_ACK: begin
                sda_oe <= 1'b0;
                if (scl_ack_jump) state <= R_DATA;
            end
            R_DATA: begin
                sda_oe <= 1'b0;
                if (scl_half_1 && cnt_bit!=4'd8) begin
                    r_data_buf[7-cnt_bit] <= sda_in;
                    cnt_bit <= cnt_bit + 4'd1;
                end
                if (scl_ack_jump && cnt_bit==4'd8) begin
                    state <= N_ACK;
                    cnt_bit <= 4'd0;
                    r_data <= r_data_buf;
                end
            end
            N_ACK: begin
                sda_oe <= 1'b1;
                if (scl_half_0) sda_out <= 1'b1;
                if (scl_ack_jump) begin
                    sda_out <= 1'b0;
                    state <= STOP;
                end
            end
            default: state <= IDLE;
        endcase
end
endmodule
(1.2)ov5640 初始化数据表

有了 SCCB 控制器后,可以实现对 ov5640 寄存器的读写操作了,接下来需要关心如何配置的问题,也就是具体向什么寄存器写什么数据,具体参考 ov5640 的用户手册。

下面展示的为 ov5640 摄像头初始化模代码块,用于配置摄像头寄存器以实现分辨率 640x480 30fps(支持图像大小参数设置)、翻转镜像(支持参数控制)、60fps、RGB565 格式的图像输出。模块通过状态机控制初始化流程,依次完成复位、延时、寄存器配置操作。初始化数据存储在 data 数组中,该模块配合 SCCB 控制器模块可以依次完成 data 中所有数据的写入。初始化完成后输出 initial_done 信号。

//ov5640 寄存器初始化配置参数 : 分辨率 640*480 30fps RGB565 格式
`timescale 1ns / 1ps
module ov5640_initial_table#(
    parameter IMAGE_WIDTH = 16'd640,
    parameter IMAGE_HEIGHT = 16'd480,
    parameter IMAGE_FLIP_EN = 1'b0,
    parameter IMAGE_MIRROR_EN = 1'b0
)(
    input wire clk,
    input wire rst_n,
    input wire initial_start,
    input wire work_done,
    output reg work_start,
    output reg [23:0] initial_data,
    output reg initial_done
);
    localparam DATA_SIZE = 9'd257;
    localparam DELAY_5ms = 20'd500_000;
    localparam IMAGE_FLIP_DAT = IMAGE_FLIP_EN ? 8'h47 : 8'h41,
               IMAGE_MIRROR_DAT = IMAGE_MIRROR_EN ? 8'h01 : 8'h07;
    localparam IDLE = 2'd0,
               RESET = 2'd1,
               DELAY = 2'd2,
               DATA = 2'd3;
    reg [23:0] data [DATA_SIZE-1:0];
    reg [19:0] delay_cnt;
    reg [8:0] cnt;
    reg [1:0] state;
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            state <= IDLE;
            initial_data <= data[0];
            initial_done <= 1'b0;
            work_start <= 1'b0;
            cnt <= 9'd0;
            delay_cnt <= 20'd0;
        end else case (state)
            IDLE: begin
                if (initial_start) begin
                    initial_done <= 1'b0;
                    initial_data <= data[cnt];
                    work_start <= 1'b1;
                    cnt <= cnt + 9'd1;
                end else if (work_done) begin
                    state <= RESET;
                    initial_data <= data[cnt];
                    work_start <= 1'b1;
                    cnt <= cnt + 9'd1;
                end else work_start <= 1'b0;
            end
            RESET: begin
                if (work_done) state <= DELAY;
                else work_start <= 1'b0;
            end
            DELAY: begin
                if (delay_cnt == DELAY_5ms) begin
                    delay_cnt <= 20'd0;
                    state <= DATA;
                    initial_data <= data[cnt];
                    work_start <= 1'b1;
                    cnt <= cnt + 9'd1;
                end else begin
                    delay_cnt <= delay_cnt + 20'd1;
                    work_start <= 1'b0;
                end
            end
            DATA: begin
                if (work_done) begin
                    if (cnt <= DATA_SIZE-1) begin
                        initial_data <= data[cnt];
                        work_start <= 1'b1;
                        cnt <= cnt + 9'd1;
                    end else begin
                        initial_done <= 1'b1;
                        initial_data <= data[0];
                        cnt <= 9'd0;
                        state <= IDLE;
                    end
                end else work_start <= 1'b0;
            end
            default:; 
        endcase
    end
    
    initial begin
        data[0] = 24'h3103_11;
        data[1] = 24'h3008_82;
        data[2] = 24'h3008_42;
        data[3] = 24'h3103_03;
        data[4] = 24'h3017_ff;
        data[5] = 24'h3018_ff;
        data[6] = 24'h3034_1a;
        data[7] = 24'h3037_13;
        data[8] = 24'h3108_01;
        data[9] = 24'h3630_36;
        data[10] = 24'h3631_0e;
        data[11] = 24'h3632_e2;
        data[12] = 24'h3633_12;
        data[13] = 24'h3621_e0;
        data[14] = 24'h3704_a0;
        data[15] = 24'h3703_5a;
        data[16] = 24'h3715_78;
        data[17] = 24'h3717_01;
        data[18] = 24'h370b_60;
        data[19] = 24'h3705_1a;
        data[20] = 24'h3905_02;
        data[21] = 24'h3906_10;
        data[22] = 24'h3901_0a;
        data[23] = 24'h3731_12;
        data[24] = 24'h3600_08;
        data[25] = 24'h3601_33;
        data[26] = 24'h302d_60;
        data[27] = 24'h3620_52;
        data[28] = 24'h371b_20;
        data[29] = 24'h471c_50;
        data[30] = 24'h3a13_43;
        data[31] = 24'h3a18_00;
        data[32] = 24'h3a19_f8;
        data[33] = 24'h3635_13;
        data[34] = 24'h3636_03;
        data[35] = 24'h3634_40;
        data[36] = 24'h3622_01;
        data[37] = 24'h3c01_34;
        data[38] = 24'h3c04_28;
        data[39] = 24'h3c05_98;
        data[40] = 24'h3c06_00;
        data[41] = 24'h3c07_08;
        data[42] = 24'h3c08_00;
        data[43] = 24'h3c09_1c;
        data[44] = 24'h3c0a_9c;
        data[45] = 24'h3c0b_40;
        data[46] = 24'h3810_00;
        data[47] = 24'h3811_10;
        data[48] = 24'h3812_00;
        data[49] = 24'h3708_64;
        data[50] = 24'h4001_02;
        data[51] = 24'h4005_1a;
        data[52] = 24'h3000_00;
        data[53] = 24'h3004_ff;
        data[54] = 24'h300e_58;
        data[55] = 24'h302e_00;
        data[56] = 24'h4300_61;
        data[57] = 24'h501f_01;
        data[58] = 24'h440e_00;
        data[59] = 24'h5000_a7;
        data[60] = 24'h3a0f_30;
        data[61] = 24'h3a10_28;
        data[62] = 24'h3a1b_30;
        data[63] = 24'h3a1e_26;
        data[64] = 24'h3a11_60;
        data[65] = 24'h3a1f_14;
        data[66] = 24'h5800_23;
        data[67] = 24'h5801_14;
        data[68] = 24'h5802_0f;
        data[69] = 24'h5803_0f;
        data[70] = 24'h5804_12;
        data[71] = 24'h5805_26;
        data[72] = 24'h5806_0c;
        data[73] = 24'h5807_08;
        data[74] = 24'h5808_05;
        data[75] = 24'h5809_05;
        data[76] = 24'h580a_08;
        data[77] = 24'h580b_0d;
        data[78] = 24'h580c_08;
        data[79] = 24'h580d_03;
        data[80] = 24'h580e_00;
        data[81] = 24'h580f_00;
        data[82] = 24'h5810_03;
        data[83] = 24'h5811_09;
        data[84] = 24'h5812_07;
        data[85] = 24'h5813_03;
        data[86] = 24'h5814_00;
        data[87] = 24'h5815_01;
        data[88] = 24'h5816_03;
        data[89] = 24'h5817_08;
        data[90] = 24'h5818_0d;
        data[91] = 24'h5819_08;
        data[92] = 24'h581a_05;
        data[93] = 24'h581b_06;
        data[94] = 24'h581c_08;
        data[95] = 24'h581d_0e;
        data[96] = 24'h581e_29;
        data[97] = 24'h581f_17;
        data[98] = 24'h5820_11;
        data[99] = 24'h5821_11;
        data[100] = 24'h5822_15;
        data[101] = 24'h5823_28;
        data[102] = 24'h5824_46;
        data[103] = 24'h5825_26;
        data[104] = 24'h5826_08;
        data[105] = 24'h5827_26;
        data[106] = 24'h5828_64;
        data[107] = 24'h5829_26;
        data[108] = 24'h582a_24;
        data[109] = 24'h582b_22;
        data[110] = 24'h582c_24;
        data[111] = 24'h582d_24;
        data[112] = 24'h582e_06;
        data[113] = 24'h582f_22;
        data[114] = 24'h5830_40;
        data[115] = 24'h5831_42;
        data[116] = 24'h5832_24;
        data[117] = 24'h5833_26;
        data[118] = 24'h5834_24;
        data[119] = 24'h5835_22;
        data[120] = 24'h5836_22;
        data[121] = 24'h5837_26;
        data[122] = 24'h5838_44;
        data[123] = 24'h5839_24;
        data[124] = 24'h583a_26;
        data[125] = 24'h583b_28;
        data[126] = 24'h583c_42;
        data[127] = 24'h583d_ce;
        data[128] = 24'h5180_ff;
        data[129] = 24'h5181_f2;
        data[130] = 24'h5182_00;
        data[131] = 24'h5183_14;
        data[132] = 24'h5184_25;
        data[133] = 24'h5185_24;
        data[134] = 24'h5186_09;
        data[135] = 24'h5187_09;
        data[136] = 24'h5188_09;
        data[137] = 24'h5189_75;
        data[138] = 24'h518a_54;
        data[139] = 24'h518b_e0;
        data[140] = 24'h518c_b2;
        data[141] = 24'h518d_42;
        data[142] = 24'h518e_3d;
        data[143] = 24'h518f_56;
        data[144] = 24'h5190_46;
        data[145] = 24'h5191_f8;
        data[146] = 24'h5192_04;
        data[147] = 24'h5193_70;
        data[148] = 24'h5194_f0;
        data[149] = 24'h5195_f0;
        data[150] = 24'h5196_03;
        data[151] = 24'h5197_01;
        data[152] = 24'h5198_04;
        data[153] = 24'h5199_12;
        data[154] = 24'h519a_04;
        data[155] = 24'h519b_00;
        data[156] = 24'h519c_06;
        data[157] = 24'h519d_82;
        data[158] = 24'h519e_38;
        data[159] = 24'h5480_01;
        data[160] = 24'h5481_08;
        data[161] = 24'h5482_14;
        data[162] = 24'h5483_28;
        data[163] = 24'h5484_51;
        data[164] = 24'h5485_65;
        data[165] = 24'h5486_71;
        data[166] = 24'h5487_7d;
        data[167] = 24'h5488_87;
        data[168] = 24'h5489_91;
        data[169] = 24'h548a_9a;
        data[170] = 24'h548b_aa;
        data[171] = 24'h548c_b8;
        data[172] = 24'h548d_cd;
        data[173] = 24'h548e_dd;
        data[174] = 24'h548f_ea;
        data[175] = 24'h5490_1d;
        data[176] = 24'h5381_1e;
        data[177] = 24'h5382_5b;
        data[178] = 24'h5383_08;
        data[179] = 24'h5384_0a;
        data[180] = 24'h5385_7e;
        data[181] = 24'h5386_88;
        data[182] = 24'h5387_7c;
        data[183] = 24'h5388_6c;
        data[184] = 24'h5389_10;
        data[185] = 24'h538a_01;
        data[186] = 24'h538b_98;
        data[187] = 24'h5580_06;
        data[188] = 24'h5583_40;
        data[189] = 24'h5584_10;
        data[190] = 24'h5589_10;
        data[191] = 24'h558a_00;
        data[192] = 24'h558b_f8;
        data[193] = 24'h501d_40;
        data[194] = 24'h5300_08;
        data[195] = 24'h5301_30;
        data[196] = 24'h5302_10;
        data[197] = 24'h5303_00;
        data[198] = 24'h5304_08;
        data[199] = 24'h5305_30;
        data[200] = 24'h5306_08;
        data[201] = 24'h5307_16;
        data[202] = 24'h5309_08;
        data[203] = 24'h530a_30;
        data[204] = 24'h530b_04;
        data[205] = 24'h530c_06;
        data[206] = 24'h5025_00;
        data[207] = 24'h3008_02;
        data[208] = 24'h3035_41;
        data[209] = 24'h3036_72;
        data[210] = 24'h3c07_08;
        data[211] = {16'h3820, IMAGE_FLIP_DAT};
        data[212] = {16'h3821, IMAGE_MIRROR_DAT};
        data[213] = 24'h3814_31;
        data[214] = 24'h3815_31;
        data[215] = 24'h3800_00;
        data[216] = 24'h3801_00;
        data[217] = 24'h3802_00;
        data[218] = 24'h3803_be;
        data[219] = 24'h3804_0a;
        data[220] = 24'h3805_3f;
        data[221] = 24'h3806_06;
        data[222] = 24'h3807_e4;
        data[223] = {16'h3808, IMAGE_WIDTH [15:8]};
        data[224] = {16'h3809, IMAGE_WIDTH [7:0] };
        data[225] = {16'h380a, IMAGE_HEIGHT[15:8]};
        data[226] = {16'h380b, IMAGE_HEIGHT[7:0] };
        data[227] = 24'h380c_07;
        data[228] = 24'h380d_69;
        data[229] = 24'h380e_03;
        data[230] = 24'h380f_21;
        data[231] = 24'h3813_06;
        data[232] = 24'h3618_00;
        data[233] = 24'h3612_29;
        data[234] = 24'h3709_52;
        data[235] = 24'h370c_03;
        data[236] = 24'h3a02_09;
        data[237] = 24'h3a03_63;
        data[238] = 24'h3a14_09;
        data[239] = 24'h3a15_63;
        data[240] = 24'h4004_02;
        data[241] = 24'h3002_1c;
        data[242] = 24'h3006_c3;
        data[243] = 24'h4713_03;
        data[244] = 24'h4407_04;
        data[245] = 24'h460b_35;
        data[246] = 24'h460c_22;
        data[247] = 24'h4837_22;
        data[248] = 24'h3824_02;
        data[249] = 24'h5001_a3;
        data[250] = 24'h3503_00;
        data[251] = {16'h3821, IMAGE_MIRROR_DAT};
        data[252] = 24'h3035_21;
        data[253] = 24'h3a02_12;
        data[254] = 24'h3a03_c6;
        data[255] = 24'h3a14_12;
        data[256] = 24'h3a15_c6;
    end
endmodule

(2)DVP 数据采集

配置完寄存器后,摄像头会开始输出图像数据,对于 ov5640 来说一般是通过 DVP 接口进行数据接收采集。这一部分需要添加一个地址输出端口,因为图像数据需要依次存入 RAM。

`timescale 1ns / 1ps
module DVP_ctrl#(
    parameter PIC_CNT_MAX = 8'd10
)(
    input wire rst_n,
    input wire ov5640_pclk,
    input wire ov5640_href,
    input wire ov5640_vsync,
    input wire [7:0] ov5640_data,
    output reg [15:0] RGB565_data,
    output wire data_valid,
    output reg [16:0] w_addr
);
    reg pix_flag;
    wire pic_flag;
    reg pic_valid;
    reg [7:0] pic_cnt;
    reg [7:0] r_ov5640_data;
    reg ov5640_vsync_delay;
    reg pix_flag_delay;
    
    always@(posedge ov5640_pclk or negedge rst_n)
        if(rst_n == 1'b0) ov5640_vsync_delay <= 1'b0;
        else ov5640_vsync_delay <= ov5640_vsync;
    
    assign pic_flag = ((ov5640_vsync_delay == 1'b0) && (ov5640_vsync == 1'b1)) ? 1'b1 : 1'b0;
    
    always @(posedge ov5640_pclk or negedge rst_n)
        if (!rst_n) begin
            pic_cnt <= 8'd0;
            pic_valid <= 1'b0;
        end else if (pic_flag) begin
            if (pic_cnt == PIC_CNT_MAX) begin
                pic_cnt <= 8'd0;
                pic_valid <= 1'b1;
            end else pic_cnt <= pic_cnt + 8'd1;
        end
    
    always @(posedge ov5640_pclk or negedge rst_n)
        if (!rst_n) begin
            pix_flag <= 1'b0;
            r_ov5640_data <= 8'b0;
            RGB565_data <= 8'b0;
        end else if (ov5640_href) begin
            if (!pix_flag) begin
                r_ov5640_data <= ov5640_data;
                pix_flag <= 1'b1;
            end else begin
                RGB565_data <= {r_ov5640_data , ov5640_data};
                pix_flag <= 1'b0;
            end
        end
    
    always @(posedge ov5640_pclk or negedge rst_n)
        if (!rst_n) w_addr <= 17'h0;
        else if (pic_flag) w_addr <= 17'h0;
        else if (ov5640_href && pic_valid) if (pix_flag) w_addr <= w_addr + 17'h1;
    
    always@(posedge ov5640_pclk or negedge rst_n)
        if(rst_n == 1'b0) pix_flag_delay <= 1'b0;
        else pix_flag_delay <= pix_flag;
    
    assign data_valid = pic_valid & pix_flag_delay;
endmodule

(3)RAM 数据缓存

为什么需要缓存图像数据:

  1. 速率匹配:摄像头输出速率与显示或处理速率可能不一致,缓存可平衡两者间的速度差异。
  2. 数据完整性:缓存确保数据在传输过程中不会丢失或损坏,尤其是在异步系统中。
  3. 帧同步:缓存可以实现帧的完整存储,避免显示或处理时出现撕裂或不完整帧。
  4. 提高效率:还可以通过乒乓操作或双缓冲技术,缓存可以实现读写并行,提高系统吞吐量。

该工程使用 FPGA 上的 block ram 资源进行缓存。最终要在屏幕上显示的图像的像素大小为 350218,数据格式为 RGB565(16 位),因此需要 RAM 数据位宽 16 位,深度至少为 350218 = 76,300 才能存下一帧数据保证画面完全显示。

(4)VGA 控制器

完成数据采集和缓存后只需要将数据从 RAM 读出来,通过 VGA 控制器输出给显示屏即可。

`timescale 1ns / 1ps
module VGA_ctrl#(
    parameter DISP_WIDTH = 12'd256,
    parameter DISP_HEIGTH = 12'd160
)(
    input wire clk,
    input wire rst_n,
    input wire [15:0]data_in,
    output wire hsync,
    output wire vsync,
    output reg [15:0]RGB,
    output reg [16:0]data_addr
);
    reg [11:0] h_cnt;
    reg [11:0] v_cnt;
    wire data_valid;
    wire [11:0] pix_x;
    wire [11:0] pix_y;
    wire disp_area;
    
    localparam H_SYNC = 12'd96,
               H_BACK = 12'd40,
               H_LEFT = 12'd8,
               H_DATA = 12'd640,
               H_RIGHT = 12'd8,
               H_FRONT = 12'd8,
               H_TOTAL = H_SYNC + H_BACK + H_LEFT + H_DATA + H_RIGHT + H_FRONT,
               V_SYNC = 12'd2,
               V_BACK = 12'd25,
               V_LEFT = 12'd8,
               V_DATA = 12'd480,
               V_RIGHT = 12'd8,
               V_FRONT = 12'd2,
               V_TOTAL = V_SYNC + V_BACK + V_LEFT + V_DATA + V_RIGHT + V_FRONT;
    
    localparam RED = 16'hF800,
               GREEN = 16'h07E0,
               BLUE = 16'h001F,
               BLACK = 16'h0000,
               WHITE = 16'hFFFF;
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) h_cnt <= 12'd0;
        else if (h_cnt == H_TOTAL-1) h_cnt <= 12'd0;
        else h_cnt <= h_cnt + 12'd1;
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) v_cnt <= 12'd0;
        else if (v_cnt == V_TOTAL-1) v_cnt <= 12'd0;
        else if (h_cnt == H_TOTAL-1) v_cnt <= v_cnt + 12'd1;
    
    assign hsync = (h_cnt < H_SYNC) ? 1'b0 : 1'b1;
    assign vsync = (v_cnt < V_SYNC) ? 1'b0 : 1'b1;
    
    assign data_valid = (h_cnt >= H_SYNC + H_BACK + H_LEFT -1) && (v_cnt >= V_SYNC + V_BACK + V_LEFT -1) && (h_cnt <= H_SYNC + H_BACK + H_LEFT + H_DATA -1) && (v_cnt <= V_SYNC + V_BACK + V_LEFT + V_DATA -1);
    assign pix_x = (data_valid) ? (h_cnt - (H_SYNC + H_BACK + H_LEFT -1)) : 12'hFFF;
    assign pix_y = (data_valid) ? (v_cnt - (V_SYNC + V_BACK + V_LEFT -1)) : 12'hFFF;
    
    assign disp_area = (pix_x >= (H_DATA - DISP_WIDTH )/2 && pix_x < (H_DATA + DISP_WIDTH )/2 && pix_y >= (V_DATA - DISP_HEIGTH)/2 && pix_y < (V_DATA + DISP_HEIGTH)/2 );
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) RGB <= BLACK;
        else if (disp_area) RGB <= data_in;
        else RGB <= BLACK;
    
    always @(posedge clk or negedge rst_n)
        if (!rst_n) data_addr <= 17'h0;
        else if (v_cnt == V_TOTAL-1) data_addr <= 17'h0;
        else if (disp_area) data_addr <= data_addr + 17'h1;
endmodule

(5)顶层模块

最后是顶层模块,除去上述介绍到的模块,还有一个按键消抖模块,用于按钮控制摄像头初始化,以及一个时钟 IP 模块,用于生成准确的 VGA 驱动时钟。

`timescale 1ns / 1ps
module TOP(
    input wire sys_clk,
    input wire ov5640_pclk,
    input wire ov5640_vsync,
    input wire ov5640_href,
    input wire [7:0] ov5640_data,
    input wire rst_n,
    input wire start,
    inout wire sda,
    output wire scl,
    output wire initial_done,
    output wire hsync,
    output wire vsync,
    output wire[3:0] red,
    output wire[3:0] green,
    output wire[3:0] blue
);
    wire initial_start;
    wire work_start;
    wire work_done;
    wire [23:0] initial_data;
    wire VGA_clk;
    wire ram_w_en;
    wire [16:0] ram_w_addr;
    wire [15:0] ram_w_data;
    wire [16:0] ram_r_addr;
    wire [15:0] ram_r_data;
    wire [15:0] RGB;
    
    assign red = RGB[15:12];
    assign green = RGB[10:7];
    assign blue = RGB[4:1];
    
    ov5640_initial_table#(
        .IMAGE_WIDTH (16'd350),
        .IMAGE_HEIGHT (16'd218)
    )ov5640_initial_table(
        .clk (sys_clk),
        .rst_n (rst_n),
        .initial_start (initial_start),
        .work_done (work_done),
        .work_start (work_start),
        .initial_data (initial_data),
        .initial_done (initial_done)
    );
    
    SCCB_ctrl SCCB_ctrl(
        .clk (sys_clk),
        .rst_n (rst_n),
        .sda (sda),
        .scl (scl),
        .work_start (work_start),
        .rw_ctrl (1'b0),
        .slave_addr (7'b0111_100),
        .byte_addr (initial_data[23:8]),
        .w_data (initial_data[7:0]),
        .r_data (),
        .work_done (work_done)
    );
    
    DVP_ctrl #(
        .PIC_CNT_MAX (8'd10)
    ) DVP_ctrl (
        .ov5640_pclk (ov5640_pclk),
        .rst_n (rst_n),
        .ov5640_vsync (ov5640_vsync),
        .ov5640_href (ov5640_href),
        .ov5640_data (ov5640_data),
        .data_valid (ram_w_en),
        .w_addr (ram_w_addr),
        .RGB565_data (ram_w_data)
    );
    
    VGA_ctrl#(
        .DISP_WIDTH (12'd350),
        .DISP_HEIGTH (12'd218)
    ) VGA_ctrl(
        .clk (VGA_clk),
        .rst_n (rst_n),
        .data_addr (ram_r_addr),
        .data_in (ram_r_data),
        .hsync (hsync),
        .vsync (vsync),
        .RGB (RGB)
    );
    
    key_filter key_filter(
        .i_clk (sys_clk),
        .i_rstn (rst_n),
        .i_key (start),
        .ok (initial_start)
    );
    
    VGA_clk_gen VGA_clk_gen(
        .sys_clk (sys_clk),
        .VGA_clk (VGA_clk)
    );
    
    RAM RAM (
        .clka (ov5640_pclk),
        .wea (ram_w_en),
        .addra (ram_w_addr),
        .dina (ram_w_data),
        .clkb (VGA_clk),
        .addrb (ram_r_addr),
        .doutb (ram_r_data)
    );
endmodule

三、效果演示

VGA 以 640480@60Hz 驱动显示屏,在中心 350218 区域进行视频显示。实测降低 ov5640 输出视频帧数也能解决画面问题。此外右侧画面被部分切割到左侧,估计是帧同步没做好,所以仍有不足,有时间再继续优化。

目录

  1. 一、工程介绍
  2. 二、Verilog 实现
  3. (1)OV5640 初始化
  4. (1.1)SCCB 控制器
  5. (1.2)ov5640 初始化数据表
  6. (2)DVP 数据采集
  7. (3)RAM 数据缓存
  8. (4)VGA 控制器
  9. (5)顶层模块
  10. 三、效果演示
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • C++ std::map 容器用法详解
  • MaxKB4j 开源 RAG 知识库平台技术架构解析
  • AI 提示词零基础入门与核心概念
  • 35 岁 + 快消从业者职业危机:成因分析与破局之道
  • 基于 FPGA 的深度强化学习框架实现超音速闭环智能流动控制实验
  • ES6 核心语法全解析:let/const、箭头函数、异步处理
  • FFmpeg 在 Windows 上的安装与视频压缩实战
  • AI 智能体驾驭工程(Harness Engineering)全解析
  • Llama.cpp 整体架构分析
  • Qwen3.5 大模型单 GPU 高效部署与股票筛选应用
  • 深入 llama.cpp:llama-server 从命令行到 HTTP Server
  • 基于 Milvus 混合检索的云厂商文档智能问答系统:Java SpringBoot 实现
  • LeetCode 343 整数拆分 Java 动态规划解法
  • Python 入门指南:环境搭建、核心优势与应用场景
  • 基于 AIGC 与大模型的实时互动小游戏实现
  • LIBERO 数据集:终身机器人学习基准测试平台
  • Git 安装流程与基础使用指南
  • Python 爬取微信公众号:合法性、风险与反爬真相
  • C++ STL string 容器常见遍历方式
  • C 语言代码优化与性能调优实战:编译器、内存与算法

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online