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

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

基于 FPGA 实现 OV5640 摄像头视频图像显示的完整流程。主要包含四个模块:OV5640 初始化(通过 SCCB 控制器配置寄存器)、DVP 数据采集(接收像素时钟与数据)、RAM 数据缓存(平衡读写速率)、VGA 控制器输出(将数据映射至屏幕时序)。系统采用 Verilog 编写,支持 RGB565 格式输出。实际测试中发现画面存在轻微撕裂,推测为 RAM 缓存同步问题,可通过乒乓操作优化。

DebugKing发布于 2026/4/6更新于 2026/9/978 浏览
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 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1);
    wire scl_half_0 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);
    wire 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;
    
    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(支持图像大小参数设置)、翻转镜像(支持参数控制)、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 资源进行缓存,但一般不建议这样做,因为图像数据很大,一般不会 RAM 进行缓存,通常选择 DDR 或者 SDRAM 之类的。我最终要在屏幕上显示的图像的像素大小为 350218(因为 RAM 容量有限就没做显示屏全屏显示),数据格式为 RGB565(16 位),因此需要 RAM 数据位宽 16 位,深度至少为 350218 = 76,300 才能存下一帧数据保证画面完全显示。

文章配图 文章配图 文章配图

(3)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;
    localparam 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, ORANGE = 16'hFC00, YELLOW = 16'hFFE0, GREEN = 16'h07E0, CYAN = 16'h07FF, BLUE = 16'h001F, PURPPLE = 16'hF81F, BLACK = 16'h0000, WHITE = 16'hFFFF, GRAY = 16'hD69A;
    
    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 - (H_SYNC + H_BACK + H_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

(4)顶层模块

最后是顶层模块,除去上述介绍到的模块,还有一个按键消抖模块,用于按钮控制摄像头初始化,以及一个时钟 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. (3)VGA 控制器
  9. (4)顶层模块
  10. 三、效果演示

更多推荐文章

查看全部
  • OpenClaw Web Search 配置与渠道选择指南
  • 数据结构——图:遍历、最小生成树与最短路径
  • 基于 DeepSeek 和 Cursor 构建智能代码审查工具实战
  • 使用 Trae IDE 与 MCP Server 将 Figma 设计稿转为前端代码
  • 耳机阻抗与前端适配:32Ω、150Ω、300Ω 耳机的功放推力需求分析
  • WhisperLiveKit 本地部署指南:从环境配置到服务启动
  • 视觉 - 骨架双模态框架用于帕金森病步态的泛化评估
  • 贝佐斯比尔盖茨等巨头押注 NASA 工程师打造通用机器人大脑 估值 20 亿美元
  • SQL 多表查询详解:连接与子查询应用
  • 10 款主流网络爬虫工具对比与选型指南
  • 使用 Windows Machine Learning 加载 ONNX 模型并推理
  • Rokid 灵珠平台搭建旅游 AR 智能体实战指南
  • 基于 AI 辅助的学生成绩管理系统开发实战
  • 2024 年 AI 绘画现状:为何 MJ 与 SD 讨论热度变化
  • Ubuntu 22.04 更换镜像源全流程
  • 前端地图 SDK 集成实战:高德/百度/腾讯/Google Maps 接入与封装
  • AI Agent 开发:知识图谱与 Neo4j 实战指南(一)
  • Flutter 三方库 modular_core 在鸿蒙系统上的架构适配与依赖注入实践
  • JavaScript 数组扁平化:不使用 flat 方法的实现方式
  • AI 入门:人工智能发展历程与现状分析

相关免费在线工具

  • 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