跳到主要内容基于 FPGA 的 OV5640 摄像头视频采集与 VGA 显示设计 | 极客日志编程语言
基于 FPGA 的 OV5640 摄像头视频采集与 VGA 显示设计
综述由AI生成本方案基于 FPGA 平台,通过 SCCB 协议初始化 OV5640 摄像头,利用 DVP 接口采集数据并缓存至 BlockRAM,最终经 VGA 控制器输出至显示器。核心模块包括时序控制、寄存器配置及像素映射逻辑,解决了速率匹配与帧同步问题,实测存在轻微撕裂现象,可通过乒乓缓存优化。
RustyLab39 浏览 工程概述
本方案利用 FPGA 通过 DVP 接口读取 OV5640 摄像头数据,经内部缓存处理后,通过 VGA 接口输出至显示器。系统主要包含四个核心模块:OV5640 初始化、DVP 数据采集、RAM 图像缓存以及 VGA 时序控制。
硬件驱动实现
SCCB 控制器设计
OV5640 的配置寄存器需要通过 SCCB 协议进行读写。虽然 SCCB 与 I2C 时序相似,但在细节上存在差异,因此这里单独设计了状态机控制的 SCCB 模块。该模块支持 16 位地址寻址和 8 位数据读写,能够处理起始位、应答位及停止位的完整时序。
`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;
// 分频计数器 (400khz 时钟 scl)
reg clk_div;
reg [7:0] cnt_clk;
reg [3:0] cnt_bit;
localparam cnt_max_400khz = 8'd125;
wire scl_half_1;
wire scl_half_0;
wire scl_ack_jump;
assign scl_half_1 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b1);
assign scl_half_0 = (cnt_clk == cnt_max_400khz >> 1 && clk_div==1'b0);
assign scl_ack_jump=((cnt_clk ==(cnt_max_400khz >> 1)-5) && clk_div==1'b0);
// 数据寄存器
reg [7:0] w_data_buf;
reg [7:0] r_data_buf;
reg [7:0] w_slave_addr_buf;
reg [7:0] r_slave_addr_buf;
reg [7:0] H_byte_addr_buf;
reg [7:0] L_byte_addr_buf;
reg work_en;
// 数据复位、开始工作时寄存数据
always @(posedge clk or negedge rst_n) begin
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
end
// 分频计数器
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_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
摄像头初始化配置
SCCB 控制器就绪后,需根据 OV5640 数据手册配置寄存器。本模块实现了分辨率 640x480、30fps、RGB565 格式的初始化流程。通过状态机依次执行复位、延时及寄存器写入操作,并支持图像翻转与镜像的参数化配置。
//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) 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;
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
DVP 数据采集
摄像头输出图像数据后,需通过 DVP 接口接收。此模块负责解析像素时钟、行场同步信号,并将 YUV 或 RGB 数据转换为 RGB565 格式存入 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) 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;
pix_flag <= 1'b1;
end else begin
RGB565_data <= {r_ov5640_data , ov5640_data};
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
RAM 数据缓存
由于摄像头输出速率与 VGA 显示速率不同步,且为了防止画面撕裂,必须引入缓存机制。本例使用 FPGA 内部的 BlockRAM 作为帧缓冲。考虑到资源限制,实际显示区域设定为 350x218,数据格式为 16 位 RGB565,所需深度约为 76,300 像素。若对画质要求更高,建议扩展至 DDR/SDRAM 并使用双缓冲技术。
VGA 控制器
将缓存中的数据读出并通过 VGA 时序输出。VGA 控制器需要严格遵循 640x480@60Hz 的时序标准,生成行场同步信号,并根据当前扫描坐标从 RAM 中读取对应像素点的数据。显示区域被居中放置在屏幕中央。
`timescale 1ns / 1ps
module VGA_ctrl#(
parameter DISP_WIDTH = 12'd256,
parameter DISP_HEIGTH = 12'd160
)(
input wire clk,
input wire rst_n,
input wire [15:0]data_in,
output wire hsync,
output wire vsync,
output reg [15:0]RGB,
output reg [16:0]data_addr
);
reg [11:0] h_cnt;
reg [11:0] v_cnt;
wire data_valid;
wire [11:0] pix_x;
wire [11:0] pix_y;
wire disp_area;
localparam H_SYNC = 12'd96,
H_BACK = 12'd40,
H_LEFT = 12'd8,
H_DATA = 12'd640,
H_RIGHT = 12'd8,
H_FRONT = 12'd8,
H_TOTAL = H_SYNC + H_BACK + H_LEFT + H_DATA + H_RIGHT + H_FRONT,
V_SYNC = 12'd2,
V_BACK = 12'd25,
V_LEFT = 12'd8,
V_DATA = 12'd480,
V_RIGHT = 12'd8,
V_FRONT = 12'd2,
V_TOTAL = V_SYNC + V_BACK + V_LEFT + V_DATA + V_RIGHT + V_FRONT;
localparam RED = 16'hF800,
GREEN = 16'h07E0,
BLUE = 16'h001F,
BLACK = 16'h0000,
WHITE = 16'hFFFF;
always @(posedge clk or negedge rst_n) 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 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) 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
顶层模块集成
顶层模块将上述各功能块连接起来,包括按键消抖用于触发初始化,以及时钟 IP 核生成 VGA 所需的 25.175MHz 时钟。RAM 模块采用双端口结构,分别由 DVP 写入和 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 以 640x480@60Hz 驱动,中心区域显示视频。目前画面存在轻微撕裂感,这通常是由于单缓冲 RAM 在读写过程中发生冲突导致的。在实际工程中,建议引入乒乓缓存机制来解决帧同步问题。此外,部分画面边缘出现切割现象,可能是帧同步信号检测不够精确,后续可进一步优化时序逻辑。
相关免费在线工具
- 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