一、工程介绍
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, //写 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);
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;//从设备地址寄存器(地址存高 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
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, DATA = 2'd3;
reg [23:0] data [DATA_SIZE-1:0]; //DATA_SIZE 个 24 位数据,每个存储一个 ov5640 寄存器地址 + 写入数据
reg [19:0] delay_cnt; //延时计数器
reg [8:0] cnt; //数据个数计数器
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
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
//*******************************************************初始设置
//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 接口进行数据接收采集,需要添加一个地址输出端口,因为图像数据需要依次存入 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 数据缓存
为什么需要缓存图像数据:
- 速率匹配:摄像头输出速率与显示或处理速率可能不一致,缓存可平衡两者间的速度差异。
- 数据完整性:缓存确保数据在传输过程中不会丢失或损坏,尤其是在异步系统中。
- 帧同步:缓存可以实现帧的完整存储,避免显示或处理时出现撕裂或不完整帧。
- 提高效率:还可以通过乒乓操作或双缓冲技术,缓存可以实现读写并行,提高系统吞吐量。
但是该工程只是跑个大致流程,所以就简单用 FPGA 上的 block ram 资源进行缓存,但一般不建议这样做,因为图像数据很大,一般不会 RAM 进行缓存,通常选择 DDR 或者 SDRAM 之类的。我最终要在屏幕上显示的图像的像素大小为 350218(因为 RAM 容量有限就没做显示屏全屏显示),数据格式为 RGB565(16 位),因此需要 RAM 数据位宽 16 位,深度至少为 350218 = 76,300 才能存下一帧数据保证画面完全显示。我的 RAM 配置如下:



(3)VGA 控制器
完成数据采集和缓存后只需要将数据从 RAM 读出来,通过 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 - (H_SYNC + H_BACK + H_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#(
.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), //ov5640:0111_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) //舍弃前 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#(
.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(
.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 区域进行视频显示,不过视频的画面有轻微的撕裂感,看上去像是有一个无形的分界线(即一个画面存在两帧或多帧图像),估计是 RAM 缓存导致的,可能加个乒乓缓存操作就能解决(实测降低 ov5640 输出视频帧数也能解决)。此外右侧画面被部分切割到左侧,估计是帧同步没做好,所以仍有不足。



