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

发布于:2025-03-16 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

一、工程介绍

二、Verilog 实现

(1)OV5640初始化

        (1.1)SCCB控制器

        (1.2)ov5640初始化数据表

(2)DVP数据采集

(3)RAM数据缓存

(3)VGA控制器

(4)顶层模块

三、效果演示


一、工程介绍

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

        本文为学习记录笔记,旨在对设计过程做简要记录,仅供学习参考。

二、Verilog 实现

(1)OV5640初始化

        (1.1)SCCB控制器

        ov5640摄像头初始化需要向其内部配置寄存器写入数据进行配置,实现对图像数据格式、图像大小、图像反转镜像、曝光、补偿等设置。对ov5640寄存器的读写操作需要遵循 SCCB 通信协议,不过 SCCB 与 I2C 协议很相似,SCCB 可以兼容 I2C,只是时序细节有区别,不过我工程中还是单独设计了一个SCCB控制器。关于SCCB的介绍与设计代码我在另一篇笔记有介绍,这里就不再赘述:Verilog: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;    //sda作输入直接读
assign sda = sda_oe ? (sda_out ? 1'bz : 1'b0) : 1'bz; //作输入需确保总线信号互不干扰对外呈高阻态,空闲和输出1时输出高阻态,因为sda线有上拉电阻
 
//状态机参数
reg [4:0] state;        //当前状态
localparam  //--------------------------------------------公共状态
            IDLE          = 5'd0,  //空闲
            START         = 5'd1,  //起始位
            W_SLAVE_ADDR  = 5'd2,  //写7位从设备地址+写命令0
            ACK1          = 5'd3,  //应答1
            W_H_BYTE_ADDR = 5'd4,  //写高8位字地址
            ACK2          = 5'd5,  //应答2
            W_L_BYTE_ADDR = 5'd6,  //写低8位字地址
            ACK3          = 5'd7,  //应答3(状态转移时进行读写判断)
            STOP          = 5'd8,  //停止位
            //--------------------------------------------写操作特殊状态
            W_DATA        = 5'd9,  //写8位数据
            W_ACK         = 5'd10, //写应答           
            //--------------------------------------------读操作特殊状态
            STOP2         = 5'd11, //中间停止位
            START2        = 5'd12, //中间起始位                         
            R_SLAVE_ADDR  = 5'd13, //写7位从设备地址+读命令1 
            R_ACK         = 5'd14, //读应答 
            R_DATA        = 5'd15, //读8位数据位        
            N_ACK         = 5'd16; //无应答
 
//计数器及参数
reg clk_div;
reg [7:0] cnt_clk; //分频计数
reg [3:0] cnt_bit; //位计数器
localparam  cnt_max_400khz = 8'd125; //400khz分频翻转计算值
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);     //scl高电平中点(起始位、ACK接收、停止位时刻)
assign scl_half_0  = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);     //scl低电平中点(数据读写时刻)
assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0); //scl低电平中点前5clk周期---
//---(ACK状态的下一状态跳转时刻,因为跳转都是由输入转输出状态,快一周期让输出状态赶上紧跟着的第一个scl_half_0,避免错过第1位数据)
 
//数据寄存器
reg [7:0] w_data_buf;       //写入数据寄存器
reg [7:0] r_data_buf;       //读出数据寄存器
reg [7:0] w_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为写命令0)
reg [7:0] r_slave_addr_buf; //从设备地址寄存器(地址存高7位,0位为读命令1)
reg [7:0] H_byte_addr_buf;  //字地址高8位寄存器
reg [7:0] L_byte_addr_buf;  //字地址低8位寄存器
reg work_en;                //工作使能信号
 
//*************************************** MAIN CODE ***************************************//
//数据复位、开始工作时寄存数据(避免传输中途数据不稳定)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        w_slave_addr_buf <= 8'b0000_0000;//0位为写命令0
        r_slave_addr_buf <= 8'b0000_0001;//0位为读命令1
        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; //地址存高7位
        r_slave_addr_buf [7:1] <= slave_addr; //地址存高7位
        w_data_buf       <= w_data;
        H_byte_addr_buf  <= byte_addr[15:8];
        L_byte_addr_buf  <= byte_addr[7:0];
    end
end
 
//分频计数器(400khz时钟scl)
always @(posedge clk or negedge rst_n) begin
    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;
end
assign scl = clk_div;
 
//状态机
always @(posedge clk or negedge rst_n) begin 
    if (!rst_n) begin
        state      <= IDLE;
        sda_oe     <= 1'b0;//sda默认不使能输出(高阻态经上拉为1)
        sda_out    <= 1'b1;//sda默认输出1避免输出0
        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; //工作使能信号work_en(工作时持续为1)
                  state   <= START;
                end
            end 
            //--------------------起始位1--------------------//
            START: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out <= 1'b0;//sda输出起始位0
                    state   <= W_SLAVE_ADDR;
                end
            end
            //--------------7bit从地址+写命令0---------------//
            W_SLAVE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state   <= ACK1;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //--------------------应答1---------------------//
            ACK1: begin 
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_ack_jump) 
                    state <= W_H_BYTE_ADDR;
			end
            //-----------------高8bit字节地址-----------------//
            W_H_BYTE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= H_byte_addr_buf[7-cnt_bit];//sda输出字节地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state   <= ACK2;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //--------------------应答2---------------------//
            ACK2: begin 
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_ack_jump) 
					state <= W_L_BYTE_ADDR;
			end
            //-----------------低8bit字节地址-----------------//
            W_L_BYTE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= L_byte_addr_buf[7-cnt_bit];//sda输出字节地址(从高到低)
                        cnt_bit <= cnt_bit + 4'd1;
                    end else begin
                        state   <= ACK3;
                        cnt_bit <= 4'd0;
                    end
                end
            end
            //--------------------应答3---------------------//
            ACK3: begin
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_ack_jump) begin
                    state   <= rw_ctrl ? STOP2 : W_DATA; //读写操作判断(0写1读)
                    sda_out <= rw_ctrl ? 1'b0 : sda_out; //停止位要输出0跳1,转状态时提前置0
                end 
            end 
            //--------------------停止位--------------------//
            STOP: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out   <= 1'b1;
                    work_done <= 1'b1;//工作结束信号置1(在STOP转IDLE时清0)
                    work_en   <= 1'b0;//工作使能信号置0
                    state     <= IDLE;
                end
            end
            //----------------写操作特殊状态-----------------//
            //-----------------8bit写入数据-----------------//
            W_DATA: begin               
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= w_data_buf[7-cnt_bit];//sda输出写入数据(从高到低)
                        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;//sda输出失能作输入 
                if (scl_ack_jump) begin
					sda_out <= 1'b0;  //停止位要输出0跳1,转状态时提前置0
                    state   <= STOP;
                end
            end 
            //----------------读操作特殊状态-----------------//
            //------------------中间停止位------------------//
            STOP2: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out <= 1'b1;
                    state   <= START2;
                end
            end
            //-------------------起始位2--------------------//
            START2: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_1) begin
                    sda_out <= 1'b0;//sda输出起始位0
                    state   <= R_SLAVE_ADDR;
                end
            end
            //--------------7bit从地址+读命令1---------------//
            R_SLAVE_ADDR: begin
                sda_oe <= 1'b1;//sda输出使能
                if (scl_half_0) begin
                    if (cnt_bit != 4'd8) begin
                        sda_out <= r_slave_addr_buf[7-cnt_bit];//sda输出设备地址(从高到低)
                        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;//sda输出失能作输入
                if (scl_ack_jump) 
                    state <= R_DATA;
            end 
            //-----------------8bit读取数据-----------------//
            R_DATA: begin
                sda_oe <= 1'b0;//sda输出失能作输入
                if (scl_half_1 && cnt_bit!=4'd8) begin      
                    r_data_buf[7-cnt_bit] <= sda_in;//sda在scl高电平中点读取数据(从高到低)
                    cnt_bit <= cnt_bit + 4'd1;
                end 
                if (scl_ack_jump && cnt_bit==4'd8) begin //提前转状态,因为无应答会在scl_half_0输出          
                    state   <= N_ACK;
                    cnt_bit <= 4'd0;
                    r_data  <= r_data_buf;//从寄存器取出读取的数据
                end
            end
            //--------------------无应答--------------------//  
            N_ACK: begin 
                sda_oe  <= 1'b1;//sda输出使能
                if (scl_half_0)
                    sda_out <= 1'b1;
                if (scl_ack_jump) begin
                    sda_out <= 1'b0;//停止位要输出0跳1,转状态时提前置0
                    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,                     //系统时钟100MHz
    input  wire rst_n,                   //复位
    input  wire initial_start,           //初始化启动信号
    input  wire work_done,               //SCCB控制器写数据完成信号
    output  reg work_start,              //SCCB控制器写数据启动信号
    output  reg [23:0] initial_data,     //高16位为寄存器地址,低8位为写入数据
    output  reg initial_done             //初始化完成信号(可输出给LED作指示灯:进行初始化时为0,完成后置1)
);
localparam  DATA_SIZE = 9'd257;          //初始化数据总数
localparam  DELAY_5ms = 20'd500_000;     //clk 100MHz 延时5ms需要计数500_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, //5ms延时
            DATA  = 2'd3; //写初始化数据

reg [23:0] data [DATA_SIZE-1:0]; //DATA_SIZE个24位数据,每个存储一个ov5640寄存器地址+写入数据
reg [19:0] delay_cnt;            //延时计数器
reg [8:0] cnt;                   //数据个数计数器,对输出数据个数进行计数 (注意位宽要足够容纳数据总数+1)
reg [1:0] state;                 //当前状态

//***********************状态机***********************//
always @(posedge clk or negedge rst_n) begin
    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 //开始初始化时,发送启动信号写data[0]
                initial_done <= 1'b0;
                initial_data <= data[cnt];
                work_start   <= 1'b1;
                cnt          <= cnt + 9'd1;
            end else if (work_done) begin //写完data[0]后,转移状态并开始写data[1]
                state        <= RESET; 
                initial_data <= data[cnt]; 
                work_start   <= 1'b1; 
                cnt          <= cnt + 9'd1;
            end else 
                work_start   <= 1'b0; //work_start为一个clk周期的高脉冲,及时置0
        end
        RESET: begin //软件复位(等待data[1]写完)
            if (work_done)
                state <= DELAY; //写完data[1]后OV5640开始软件复位,进入延时状态
            else 
                work_start   <= 1'b0;
        end
        DELAY: begin //5ms延时计数器,软件复位后需要延时5ms
            if (delay_cnt == DELAY_5ms) begin //延时完成时,开始写data[2]
                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
    //*******************************************************初始设置
    //15fps VGA YUV output 
    // 24MHz input clock, 24MHz PCLK
    data[0] = 24'h3103_11; // system clock from pad, bit[1]
    data[1] = 24'h3008_82; // software reset, bit[7] ********软件复位
    //delay 5ms 软件复位需要延时5ms
    data[2] = 24'h3008_42; // software power down, bit[6]
    data[3] = 24'h3103_03; // system clock from PLL, bit[1]
    data[4] = 24'h3017_ff; // FREX, Vsync, HREF, PCLK, D[9:6] output enable
    data[5] = 24'h3018_ff; // D[5:0], GPIO[1:0] output enable
    data[6] = 24'h3034_1a; // MIPI 10-bit
    data[7] = 24'h3037_13; // PLL root divider, bit[4], PLL pre-divider, bit[3:0]
    data[8] = 24'h3108_01; // PCLK root divider, bit[5:4], SCLK2x root divider, bit[3:2]
                           // SCLK root divider, bit[1:0]
    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; // VCM control
    data[25] = 24'h3601_33; // VCM control
    data[26] = 24'h302d_60; // system control
    data[27] = 24'h3620_52;
    data[28] = 24'h371b_20;
    data[29] = 24'h471c_50;
    data[30] = 24'h3a13_43; // pre-gain = 1.047x
    data[31] = 24'h3a18_00; // gain ceiling
    data[32] = 24'h3a19_f8; // gain ceiling = 15.5x
    data[33] = 24'h3635_13;
    data[34] = 24'h3636_03;
    data[35] = 24'h3634_40;
    data[36] = 24'h3622_01;
    // 50/60Hz detection 50/60Hz 灯光条纹过滤
    data[37] = 24'h3c01_34; // Band auto, bit[7]
    data[38] = 24'h3c04_28; // threshold low sum
    data[39] = 24'h3c05_98; // threshold high sum
    data[40] = 24'h3c06_00; // light meter 1 threshold[15:8]
    data[41] = 24'h3c07_08; // light meter 1 threshold[7:0]
    data[42] = 24'h3c08_00; // light meter 2 threshold[15:8]
    data[43] = 24'h3c09_1c; // light meter 2 threshold[7:0]
    data[44] = 24'h3c0a_9c; // sample number[15:8]
    data[45] = 24'h3c0b_40; // sample number[7:0]
    data[46] = 24'h3810_00; // Timing Hoffset[11:8]
    data[47] = 24'h3811_10; // Timing Hoffset[7:0]
    data[48] = 24'h3812_00; // Timing Voffset[10:8]
    data[49] = 24'h3708_64;
    data[50] = 24'h4001_02; // BLC start from line 2
    data[51] = 24'h4005_1a; // BLC always update
    data[52] = 24'h3000_00; // enable blocks
    data[53] = 24'h3004_ff; // enable clocks
    data[54] = 24'h300e_58; // MIPI power down, DVP enable
    data[55] = 24'h302e_00;
    // 设置输出格式为RGB格式
    data[56] = 24'h4300_61; // RGB565   //h4300_6f(以太网模式参数)
    data[57] = 24'h501f_01; // RGB
    /*原配置
    data[56] = 24'h4300_30; // YUV 422, YUYV
    data[57] = 24'h501f_00; // YUV 422
    */
    data[58] = 24'h440e_00;
    data[59] = 24'h5000_a7; // Lenc on, raw gamma on, BPC on, WPC on, CIP on
    // AEC target 自动曝光控制
    data[60] = 24'h3a0f_30; // stable range in high
    data[61] = 24'h3a10_28; // stable range in low
    data[62] = 24'h3a1b_30; // stable range out high
    data[63] = 24'h3a1e_26; // stable range out low
    data[64] = 24'h3a11_60; // fast zone high
    data[65] = 24'h3a1f_14; // fast zone low
    // Lens correction for ? 镜头补偿
    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; // lenc BR offset
    // AWB 自动白平衡
    data[128] = 24'h5180_ff; // AWB B block
    data[129] = 24'h5181_f2; // AWB control
    data[130] = 24'h5182_00; // [7:4] max local counter, [3:0] max fast counter
    data[131] = 24'h5183_14; // AWB advanced
    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; // AWB top limit
    data[146] = 24'h5192_04; // AWB bottom limit
    data[147] = 24'h5193_70; // red limit
    data[148] = 24'h5194_f0; // green limit
    data[149] = 24'h5195_f0; // blue limit
    data[150] = 24'h5196_03; // AWB control
    data[151] = 24'h5197_01; // local limit
    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; // AWB control
    // Gamma 伽玛曲线
    data[159] = 24'h5480_01; // Gamma bias plus on, bit[0]
    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;
    // color matrix 色彩矩阵
    data[176] = 24'h5381_1e; // CMX1 for Y
    data[177] = 24'h5382_5b; // CMX2 for Y
    data[178] = 24'h5383_08; // CMX3 for Y
    data[179] = 24'h5384_0a; // CMX4 for U
    data[180] = 24'h5385_7e; // CMX5 for U
    data[181] = 24'h5386_88; // CMX6 for U
    data[182] = 24'h5387_7c; // CMX7 for V
    data[183] = 24'h5388_6c; // CMX8 for V
    data[184] = 24'h5389_10; // CMX9 for V
    data[185] = 24'h538a_01; // sign[9]
    data[186] = 24'h538b_98; // sign[8:1]
    // UV adjust UV 色彩饱和度调整
    data[187] = 24'h5580_06; // saturation on, bit[1]
    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; // enable manual offset of contrast
    // CIP 锐化和降噪
    data[194] = 24'h5300_08; // CIP sharpen MT threshold 1
    data[195] = 24'h5301_30; // CIP sharpen MT threshold 2
    data[196] = 24'h5302_10; // CIP sharpen MT offset 1
    data[197] = 24'h5303_00; // CIP sharpen MT offset 2
    data[198] = 24'h5304_08; // CIP DNS threshold 1
    data[199] = 24'h5305_30; // CIP DNS threshold 2
    data[200] = 24'h5306_08; // CIP DNS offset 1
    data[201] = 24'h5307_16; // CIP DNS offset 2
    data[202] = 24'h5309_08; // CIP sharpen TH threshold 1
    data[203] = 24'h530a_30; // CIP sharpen TH threshold 2
    data[204] = 24'h530b_04; // CIP sharpen TH offset 1
    data[205] = 24'h530c_06; // CIP sharpen TH offset 2
    data[206] = 24'h5025_00;
    data[207] = 24'h3008_02; // wake up from standby, bit[6]
    
    //*******************************************************800x480预览
    // 800x480 15 帧/秒
    // 800x480 15fps, night mode 5fps
    // input clock 24Mhz, PCLK 45.6Mhz
    data[208] = 24'h3035_41; // PLL 
    data[209] = 24'h3036_72; // PLL 
    data[210] = 24'h3c07_08; // light meter 1 threshold[7:0] 
    // 用参数进行控制镜像和翻转
    data[211] = {16'h3820, IMAGE_FLIP_DAT};   // flip
    data[212] = {16'h3821, IMAGE_MIRROR_DAT}; // mirror (镜像寄存器再最后又写了一次才能写进去,没找到原因,可能是后面其他设置将他初始化了)
    /*原配置
    data[211] = 24'h3820_41; // flip
    data[212] = 24'h3821_07; // mirror
    */ 
    data[213] = 24'h3814_31; // timing X inc 
    data[214] = 24'h3815_31; // timing Y inc 
    data[215] = 24'h3800_00; // HS 
    data[216] = 24'h3801_00; // HS 
    data[217] = 24'h3802_00; // VS 
    data[218] = 24'h3803_be; // VS 
    data[219] = 24'h3804_0a; // HW (HE)
    data[220] = 24'h3805_3f; // HW (HE)
    data[221] = 24'h3806_06; // VH (VE)
    data[222] = 24'h3807_e4; // VH (VE)
    // 用参数进行定义图像输出大小(分辨率) 
    data[223] = {16'h3808, IMAGE_WIDTH [15:8]}; // DVPHO
    data[224] = {16'h3809, IMAGE_WIDTH [7:0] }; // DVPHO
    data[225] = {16'h380a, IMAGE_HEIGHT[15:8]}; // DVPVO 
    data[226] = {16'h380b, IMAGE_HEIGHT[7:0] }; // DVPHO
    /*原配置
    data[223] = 24'h3808_03; // DVPHO (宽度设置0320h = 800)
    data[224] = 24'h3809_20; // DVPHO 
    data[225] = 24'h380a_01; // DVPVO (高度设置01e0h = 480)
    data[226] = 24'h380b_e0; // DVPVO 
    */
    data[227] = 24'h380c_07; // HTS 
    data[228] = 24'h380d_69; // HTS 
    data[229] = 24'h380e_03; // VTS 
    data[230] = 24'h380f_21; // VTS 
    data[231] = 24'h3813_06; // timing V offset 
    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; // 60Hz max exposure, night mode 5fps
    data[237] = 24'h3a03_63; // 60Hz max exposure
    // banding filters are calculated automatically in camera driver
    //data[] = 24'h3a08_00; // B50 step 
    //data[] = 24'h3a09_78; // B50 step 
    //data[] = 24'h3a0a_00; // B60 step 
    //data[] = 24'h3a0b_64; // B60 step 
    //data[] = 24'h3a0e_06; // 50Hz max band
    //data[] = 24'h3a0d_08; // 60Hz max band
    data[238] = 24'h3a14_09; // 50Hz max exposure, night mode 5fps
    data[239] = 24'h3a15_63; // 50Hz max exposure 
    data[240] = 24'h4004_02; // BLC line number 
    data[241] = 24'h3002_1c; // reset JFIFO, SFIFO, JPG 
    data[242] = 24'h3006_c3; // disable clock of JPEG2x, JPEG
    data[243] = 24'h4713_03; // JPEG mode 3 
    data[244] = 24'h4407_04; // Quantization sacle 
    data[245] = 24'h460b_35;
    data[246] = 24'h460c_22;
    data[247] = 24'h4837_22; // MIPI global timing 
    data[248] = 24'h3824_02; // PCLK manual divider
    data[249] = 24'h5001_a3; // SDE on, CMX on, AWB on
    data[250] = 24'h3503_00; // AEC/AGC on
    data[251] = {16'h3821, IMAGE_MIRROR_DAT}; // mirror(再写一次,前面由于某种原因没写进去)
    
    // 800x480 30 帧/秒
    // YUV 800x480 30fps, night mode 5fps
    // Input Clock = 24Mhz, PCLK = 91.2MHz
    // same settings as 800x480 15fps, except the following settings(在800x480 15fps基础上修改以下参数)
    data[252] = 24'h3035_21; // PLL
    data[253] = 24'h3a02_12; // 60Hz max exposure, night mode 5fps
    data[254] = 24'h3a03_c6; // 60Hz max exposure
    data[255] = 24'h3a14_12; // 50Hz max exposure, night mode 5fps
    data[256] = 24'h3a15_c6; // 50Hz max exposur

end
endmodule

(2)DVP数据采集

        配置完寄存器后,摄像头会开始输出图像数据,对于ov5640来说一般是通过DVP接口进行数据接收采集,这一部分的知识可参考我另一篇笔记 Verilog:DVP接口,不过需要添加一个地址输出端口,因为图像数据需要依次存入RAM,代码如下:

  `timescale 1ns / 1ps
module DVP_ctrl#(
    parameter PIC_CNT_MAX = 8'd10   //舍弃前10帧不稳定图像数据
)(
    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, //图像数据输出(RGB565格式)
    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;         //一像素数据结束标志位打拍

//***************************** 场同步 ****************************//
//场同步信号打拍(用于检测vsync上升沿)
always@(posedge ov5640_pclk or negedge rst_n)
    if(rst_n == 1'b0)
        ov5640_vsync_delay <= 1'b0;
    else
        ov5640_vsync_delay <= ov5640_vsync;

//一帧图像结束标志位(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) begin
    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
end

//***************************** 行同步 ****************************//
//行同步
always @(posedge ov5640_pclk or negedge rst_n) begin
    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; //先缓存高8位
            pix_flag      <= 1'b1;
        end else begin
            RGB565_data <= {r_ov5640_data , ov5640_data};//后拼接低8位输出
            pix_flag    <= 1'b0;
        end
    end
end

//更新地址
always @(posedge ov5640_pclk or negedge rst_n) begin
    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;
end

//一像素数据结束标志位打拍(用于产生像素数据有效信号)
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或者SD卡之类的。我最终要在屏幕上显示的图像的像素大小为350*218(因为RAM容量有限就没做显示屏全屏显示),数据格式为RGB565(16位),因此需要RAM数据位宽16位,深度至少为350*218 = 76,300才能存下一帧数据保证画面完全显示。我的RAM配置如下:

(3)VGA控制器

        完成数据采集和缓存后只需要将数据从RAM读出来,通过VGA控制器输出给显示屏即可,关于VGA的内容可参考笔记 Verilog:VGA控制器 

  `timescale 1ns / 1ps
module VGA_ctrl#(
    parameter DISP_WIDTH  = 12'd256, //屏幕显示区域宽度(设置为偶数,最大为分辨率宽H_DATA)
    parameter DISP_HEIGTH = 12'd160  //屏幕显示区域高度(设置为偶数,最大为分辨率高V_DATA)
)(
    input  wire clk,            //输入频率参考VGA参数表(可用时钟IP生成VGA_clk)
    input  wire rst_n,          //复位
    input  wire [15:0]data_in,  //数据输入(数据从RAM读取)
    output wire hsync,          //行同步信号
    output wire vsync,          //场同步信号
    output reg  [15:0]RGB,      //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
    output reg  [16:0]data_addr //数据地址(RAM读数据地址)
);
reg  [11:0] h_cnt; //水平计数器(行:对应像素点的行位置x)
reg  [11:0] v_cnt; //垂直计数器(场:对应像素点的列位置y)
wire data_valid;   //数据有效信号(有效数据期间为1)  
wire [11:0] pix_x; //像素点x坐标(左上角为原点)
wire [11:0] pix_y; //像素点y坐标
wire disp_area;    //显示区域信号(当像素处于显示区域时为1)

//640*480@60Hz 25.175MHz VGA时序参数(代表各区间持续的时钟周期个数,根据输出分辨率进行设置)
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; //垂直总计时间
//颜色参数  RGB565格式
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) begin
    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;
end
//垂直计数器
always @(posedge clk or negedge rst_n) begin
    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;
end

//*******************生成同步信号*******************//
assign hsync = (h_cnt < H_SYNC) ? 1'b0 : 1'b1;//输出负脉冲?\_/????????????\_/?
assign vsync = (v_cnt < V_SYNC) ? 1'b0 : 1'b1;//输出负脉冲
//assign hsync = (h_cnt < H_SYNC) ? 1'b1 : 1'b0;//输出正脉冲_/?\____________/?\_
//assign vsync = (v_cnt < V_SYNC) ? 1'b1 : 1'b0;//输出正脉冲

//*******************换算横纵坐标*******************//
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; //有效坐标范围为0 ~ H_DATA-1
assign pix_y = (data_valid) ? (v_cnt - (V_SYNC + V_BACK + V_LEFT -1)) : 12'hFFF; //有效坐标范围为0 ~ V_DATA-1

//*******************输出像素数据*******************//
//设置显示区域为中心 DISP_WIDTH * DISP_HIGHT 大小
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) begin
    if (!rst_n)
        RGB <= BLACK;
    else if (disp_area)//处于显示区域时更新数据
        RGB <= data_in;
    else
        RGB <= BLACK;
end 
//更新地址
always @(posedge clk or negedge rst_n) begin
    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; 
end       

endmodule

(4)顶层模块

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

  `timescale 1ns / 1ps
module TOP(
    input  wire sys_clk,            //系统时钟 100MHz
    input  wire ov5640_pclk,        //ov5640 像素时钟
    input  wire ov5640_vsync,       //ov5640 场同步信号
    input  wire ov5640_href,        //ov5640 行同步信号
    input  wire [7:0] ov5640_data,  //ov5640 DVP数据输入
    input  wire rst_n,              //系统复位
    input  wire start,              //初始化启动信号
    inout  wire sda,                //双向数据线(inout)
    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;            //SCCB启动信号
wire work_done;             //SCCB工作完成信号
wire [23:0] initial_data;   //初始化数据线
wire VGA_clk;               //VGA25.2MHz时钟
wire ram_w_en;              //ram写使能信号
wire [16:0] ram_w_addr;     //ram写数据地址线
wire [15:0] ram_w_data;     //ram写数据线
wire [16:0] ram_r_addr;     //ram读数据地址线
wire [15:0] ram_r_data;     //ram读数据线
wire [15:0] RGB;            //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
assign red   = RGB[15:12];  //截取红色分量高4位
assign green = RGB[10:7];   //截取绿色分量高4位
assign blue  = RGB[4:1];    //截取蓝色分量高4位

ov5640_initial_table#( //ov5640初始化数据表
    .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( //SCCB读写控制模块
    .clk         (sys_clk),       
    .rst_n       (rst_n),              
    .sda         (sda),                
    .scl         (scl), 
    .work_start  (work_start),  
    .rw_ctrl     (1'b0),                        
    .slave_addr  (7'b0111_100), //ov5640:0111_100
    .byte_addr   (initial_data[23:8]),                       
    .w_data      (initial_data[7:0]),   
    .r_data      (),  
    .work_done   (work_done)
);

DVP_ctrl #( //DVP数据采集模块
    .PIC_CNT_MAX (8'd10) //舍去前10帧图像
) 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#( //VGA控制器
    .DISP_WIDTH     (12'd350), //屏幕显示区域宽度(设置为偶数,最大为分辨率宽H_DATA)
    .DISP_HEIGTH    (12'd218)  //屏幕显示区域高度(设置为偶数,最大为分辨率高V_DATA)
) 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) //按键松开时产生一个clk周期的高电平脉冲               
);

VGA_clk_gen VGA_clk_gen( //25.175MHz时钟
    .sys_clk    (sys_clk),
    .VGA_clk    (VGA_clk)
);

RAM RAM ( //RAM IP核
    .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以 640*480@60Hz 驱动显示屏,在中心 350*218 区域进行视频显示,不过视频的画面有轻微的撕裂感,看上去像是有一个无形的分界线(即一个画面存在两帧或多帧图像),估计是RAM缓存导致的,可能加个乒乓缓存操作就能解决(实测降低ov5640输出视频帧数也能解决)。此外右侧画面被部分切割到左侧,估计是帧同步没做好,所以仍有不足,有时间再继续优化。