FPGA 기반 OV5640 카메라 이미지 처리 및 VGA 출력 구현

프로젝트 개요

OV5640 카메라 모듈은 DVP 인터페이스를 통해 비디오 데이터를 전송하며, 이 데이터를 FPGA에서 처리하여 VGA 인터페이스로 디스플레이에 출력하는 시스템을 구현한다. 주요 기능은 다음과 같다:

  • OV5640 초기화 설정
  • DVP 인터페이스를 통한 이미지 데이터 수집
  • 수집된 이미지 데이터의 메모리 버퍼링
  • VGA 인터페이스를 통한 디스플레이 출력

Verilog 구현

OV5640 초기화

SCCB 통신 컨트롤러

OV5640 카메라는 SCCB(Serial Camera Control Bus) 프로토콜을 사용하여 내부 레지스터를 구성한다. SCCB는 I2C와 유사하지만 약간의 타이밍 차이가 있으므로 별도의 컨트롤러를 설계하였다.

`timescale 1ns / 1ps
module SCCB_controller(
    input  wire clk,
    input  wire rst_n,
    inout  wire sda,
    output wire scl,
    input  wire read_write,
    input  wire start_operation,
    input  wire [6:0] device_address,
    input  wire [15:0] register_address,
    input  wire [7:0] write_data,
    output reg  [7:0] read_data,
    output reg  operation_complete
);

reg sda_output_enable;
reg sda_output_value;
wire sda_input_value;
assign sda_input_value = sda;
assign sda = sda_output_enable ? (sda_output_value ? 1'bz : 1'b0) : 1'bz;

// 상태 머신 정의
reg [4:0] current_state;
localparam IDLE_STATE = 5'd0,
           START_BIT = 5'd1,
           WRITE_DEVICE_ADDR = 5'd2,
           ACK_RECEIVE = 5'd3,
           WRITE_HIGH_ADDR = 5'd4,
           ACK2_RECEIVE = 5'd5,
           WRITE_LOW_ADDR = 5'd6,
           ACK3_RECEIVE = 5'd7,
           STOP_BIT = 5'd8,
           WRITE_DATA_STATE = 5'd9,
           WRITE_ACK = 5'd10,
           INTERMEDIATE_STOP = 5'd11,
           RESTART_BIT = 5'd12,
           READ_DEVICE_ADDR = 5'd13,
           READ_ACK = 5'd14,
           READ_DATA_STATE = 5'd15,
           NO_ACK = 5'd16;

// 클럭 분주 및 카운터
reg clock_divider;
reg [7:0] clock_counter;
reg [3:0] bit_counter;
localparam CLOCK_DIV_VALUE = 8'd125;
wire half_clock_high = (clock_counter == CLOCK_DIV_VALUE >> 1 && clock_divider == 1'b1);
wire half_clock_low = (clock_counter == CLOCK_DIV_VALUE >> 1 && clock_divider == 1'b0);
wire ack_transition = ((clock_counter == (CLOCK_DIV_VALUE >> 1) - 5) && clock_divider == 1'b0);

// 데이터 레지스터
reg [7:0] tx_data_buffer;
reg [7:0] rx_data_buffer;
reg [7:0] device_addr_write;
reg [7:0] device_addr_read;
reg [7:0] high_byte_addr;
reg [7:0] low_byte_addr;
reg enable_operation;

// 데이터 준비
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        device_addr_write <= 8'b0;
        device_addr_read <= 8'b1;
        high_byte_addr <= 8'b0;
        low_byte_addr <= 8'b0;
        tx_data_buffer <= 8'b0;
    end else if (start_operation) begin
        device_addr_write[7:1] <= device_address;
        device_addr_read[7:1] <= device_address;
        tx_data_buffer <= write_data;
        high_byte_addr <= register_address[15:8];
        low_byte_addr <= register_address[7:0];
    end
end

// 클럭 생성
always @(posedge clk or negedge rst_n) begin
    if (!enable_operation || !rst_n) begin
        clock_counter <= 8'd1;
        clock_divider <= 1'b1;
    end else if (clock_counter == CLOCK_DIV_VALUE) begin
        clock_counter <= 8'd1;
        clock_divider <= ~clock_divider;
    end else 
        clock_counter <= clock_counter + 8'd1;
end
assign scl = clock_divider;

// 상태 머신 로직
always @(posedge clk or negedge rst_n) begin 
    if (!rst_n) begin
        current_state <= IDLE_STATE;
        sda_output_enable <= 1'b0;
        sda_output_value <= 1'b1;
        enable_operation <= 1'b0;
        operation_complete <= 1'b0;
        bit_counter <= 4'd0;
    end else
        case(current_state)
            IDLE_STATE: begin
                sda_output_enable <= 1'b0;
                sda_output_value <= 1'b1; 
                operation_complete <= 1'b0; 
                if (start_operation) begin
                    enable_operation <= 1'b1;
                    current_state <= START_BIT;
                end
            end 
            START_BIT: begin
                sda_output_enable <= 1'b1;
                if (half_clock_high) begin
                    sda_output_value <= 1'b0;
                    current_state <= WRITE_DEVICE_ADDR;
                end
            end
            WRITE_DEVICE_ADDR: begin
                sda_output_enable <= 1'b1;
                if (half_clock_low) begin
                    if (bit_counter != 4'd8) begin
                        sda_output_value <= device_addr_write[7-bit_counter];
                        bit_counter <= bit_counter + 4'd1;
                    end else begin
                        current_state <= ACK_RECEIVE;
                        bit_counter <= 4'd0;
                    end
                end
            end
            ACK_RECEIVE: begin 
                sda_output_enable <= 1'b0;
                if (ack_transition) 
                    current_state <= WRITE_HIGH_ADDR;
            end
            WRITE_HIGH_ADDR: begin
                sda_output_enable <= 1'b1;
                if (half_clock_low) begin
                    if (bit_counter != 4'd8) begin
                        sda_output_value <= high_byte_addr[7-bit_counter];
                        bit_counter <= bit_counter + 4'd1;
                    end else begin
                        current_state <= ACK2_RECEIVE;
                        bit_counter <= 4'd0;
                    end
                end
            end
            ACK2_RECEIVE: begin 
                sda_output_enable <= 1'b0;
                if (ack_transition) 
                    current_state <= WRITE_LOW_ADDR;
            end
            WRITE_LOW_ADDR: begin
                sda_output_enable <= 1'b1;
                if (half_clock_low) begin
                    if (bit_counter != 4'd8) begin
                        sda_output_value <= low_byte_addr[7-bit_counter];
                        bit_counter <= bit_counter + 4'd1;
                    end else begin
                        current_state <= ACK3_RECEIVE;
                        bit_counter <= 4'd0;
                    end
                end
            end
            ACK3_RECEIVE: begin
                sda_output_enable <= 1'b0;
                if (ack_transition) begin
                    current_state <= read_write ? INTERMEDIATE_STOP : WRITE_DATA_STATE;
                    sda_output_value <= read_write ? 1'b0 : sda_output_value;
                end 
            end 
            STOP_BIT: begin
                sda_output_enable <= 1'b1;
                if (half_clock_high) begin
                    sda_output_value <= 1'b1;
                    operation_complete <= 1'b1;
                    enable_operation <= 1'b0;
                    current_state <= IDLE_STATE;
                end
            end
            WRITE_DATA_STATE: begin               
                sda_output_enable <= 1'b1;
                if (half_clock_low) begin
                    if (bit_counter != 4'd8) begin
                        sda_output_value <= tx_data_buffer[7-bit_counter];
                        bit_counter <= bit_counter + 4'd1;
                    end else begin
                        current_state <= WRITE_ACK;
                        bit_counter <= 4'd0;
                    end
                end
            end
            WRITE_ACK: begin 
                sda_output_enable <= 1'b0;
                if (ack_transition) begin
                    sda_output_value <= 1'b0;
                    current_state <= STOP_BIT;
                end
            end 
            INTERMEDIATE_STOP: begin
                sda_output_enable <= 1'b1;
                if (half_clock_high) begin
                    sda_output_value <= 1'b1;
                    current_state <= RESTART_BIT;
                end
            end
            RESTART_BIT: begin
                sda_output_enable <= 1'b1;
                if (half_clock_high) begin
                    sda_output_value <= 1'b0;
                    current_state <= READ_DEVICE_ADDR;
                end
            end
            READ_DEVICE_ADDR: begin
                sda_output_enable <= 1'b1;
                if (half_clock_low) begin
                    if (bit_counter != 4'd8) begin
                        sda_output_value <= device_addr_read[7-bit_counter];
                        bit_counter <= bit_counter + 4'd1;
                    end else begin
                        current_state <= READ_ACK;
                        bit_counter <= 4'd0;
                    end
                end
            end
            READ_ACK: begin 
                sda_output_enable <= 1'b0;
                if (ack_transition) 
                    current_state <= READ_DATA_STATE;
            end 
            READ_DATA_STATE: begin
                sda_output_enable <= 1'b0;
                if (half_clock_high && bit_counter != 4'd8) begin      
                    rx_data_buffer[7-bit_counter] <= sda_input_value;
                    bit_counter <= bit_counter + 4'd1;
                end 
                if (ack_transition && bit_counter == 4'd8) begin
                    current_state <= NO_ACK;
                    bit_counter <= 4'd0;
                    read_data <= rx_data_buffer;
                end
            end
            NO_ACK: begin 
                sda_output_enable <= 1'b1;
                if (half_clock_low)
                    sda_output_value <= 1'b1;
                if (ack_transition) begin
                    sda_output_value <= 1'b0;
                    current_state <= STOP_BIT;
                end 
            end
            default: current_state <= IDLE_STATE;
        endcase 
end
endmodule

OV5640 초기화 데이터 테이블

SCCB 컨트롤러를 통해 OV5640 레지스터를 구성하기 위한 초기화 데이터 테이블이다. 이 모듈은 640x480 해상도, 30fps, RGB565 형식으로 이미지를 출력하도록 설정된다.

// OV5640 레지스터 초기화 설정: 640*480 30fps RGB565 형식
`timescale 1ns / 1ps
module ov5640_config_table#(
    parameter IMG_WIDTH = 16'd640,
    parameter IMG_HEIGHT = 16'd480,
    parameter FLIP_ENABLE = 1'b0,
    parameter MIRROR_ENABLE = 1'b0
)(
    input  wire clk,
    input  wire rst_n,
    input  wire init_start,
    input  wire sccb_done,
    output reg  sccb_start,
    output reg  [23:0] config_data,
    output reg  init_complete
);

localparam DATA_COUNT = 9'd257;
localparam DELAY_5MS = 20'd500_000;
localparam FLIP_VALUE = FLIP_ENABLE ? 8'h47 : 8'h41,
           MIRROR_VALUE = MIRROR_ENABLE ? 8'h01 : 8'h07;
localparam STATE_IDLE = 2'd0,
           STATE_RESET = 2'd1,
           STATE_DELAY = 2'd2,
           STATE_CONFIG = 2'd3;

reg [23:0] config_array [DATA_COUNT-1:0];
reg [19:0] delay_counter;
reg [8:0] data_index;
reg [1:0] current_state;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        current_state <= STATE_IDLE;
        config_data <= config_array[0];
        init_complete <= 1'b0;
        sccb_start <= 1'b0;
        data_index <= 9'd0;
        delay_counter <= 20'd0;
    end else case (current_state)
        STATE_IDLE: begin
            if (init_start) begin
                init_complete <= 1'b0;
                config_data <= config_array[data_index];
                sccb_start <= 1'b1;
                data_index <= data_index + 9'd1;
            end else if (sccb_done) begin
                current_state <= STATE_RESET; 
                config_data <= config_array[data_index]; 
                sccb_start <= 1'b1; 
                data_index <= data_index + 9'd1;
            end else 
                sccb_start <= 1'b0;
        end
        STATE_RESET: begin
            if (sccb_done)
                current_state <= STATE_DELAY;
            else 
                sccb_start <= 1'b0;
        end
        STATE_DELAY: begin
            if (delay_counter == DELAY_5MS) begin
                delay_counter <= 20'd0;
                current_state <= STATE_CONFIG;
                config_data <= config_array[data_index];
                sccb_start <= 1'b1;
                data_index <= data_index + 9'd1;
            end else begin
                delay_counter <= delay_counter + 20'd1;
                sccb_start <= 1'b0;
            end
        end
        STATE_CONFIG: begin
            if (sccb_done) begin
                if (data_index <= DATA_COUNT-1) begin
                    config_data <= config_array[data_index];
                    sccb_start <= 1'b1;
                    data_index <= data_index + 9'd1;   
                end else begin
                    init_complete <= 1'b1;
                    config_data <= config_array[0];
                    data_index <= 9'd0;   
                    current_state <= STATE_IDLE; 
                end
            end else 
                sccb_start <= 1'b0;
        end
        default:;
    endcase
end

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

DVP 데이터 수집

OV5640에서 출력되는 이미지 데이터를 DVP 인터페이스를 통해 수집하는 모듈이다. 수집된 데이터는 RGB565 형식으로 변환되어 메모리에 저장된다.

`timescale 1ns / 1ps
module dvp_data_capture#(
    parameter FRAME_SKIP_COUNT = 8'd10
)(
    input  wire reset_n,
    input  wire pixel_clock,
    input  wire line_sync,
    input  wire frame_sync,
    input  wire [7:0] pixel_data,
    output reg  [15:0] rgb565_output,
    output wire valid_data,
    output reg  [16:0] write_address
);

reg  pixel_complete;
wire frame_complete;
reg  frame_valid;
reg  [7:0] frame_counter;
reg  [7:0] delayed_pixel_data;
reg  delayed_frame_sync;
reg  delayed_pixel_complete;

always@(posedge pixel_clock or negedge reset_n)
    if(reset_n == 1'b0)
        delayed_frame_sync <= 1'b0;
    else
        delayed_frame_sync <= frame_sync;

assign frame_complete = ((delayed_frame_sync == 1'b0) &&
                        (frame_sync == 1'b1)) ? 1'b1 : 1'b0;

always @(posedge pixel_clock or negedge reset_n) begin
    if (!reset_n) begin
        frame_counter <= 8'd0;
        frame_valid <= 1'b0;
    end else if (frame_complete) begin
        if (frame_counter == FRAME_SKIP_COUNT) begin
            frame_counter <= 8'd0;
            frame_valid <= 1'b1;
        end else
            frame_counter <= frame_counter + 8'd1;
    end
end

always @(posedge pixel_clock or negedge reset_n) begin
    if (!reset_n) begin
        pixel_complete <= 1'b0;
        delayed_pixel_data <= 8'b0;
        rgb565_output <= 8'b0;
    end else if (line_sync) begin
        if (!pixel_complete) begin
            delayed_pixel_data <= pixel_data;
            pixel_complete <= 1'b1;
        end else begin
            rgb565_output <= {delayed_pixel_data, pixel_data};
            pixel_complete <= 1'b0;
        end
    end
end

always @(posedge pixel_clock or negedge reset_n) begin
    if (!reset_n) 
        write_address <= 17'h0;
    else if (frame_complete)
        write_address <= 17'h0;
    else if (line_sync && frame_valid) 
        if (pixel_complete)
            write_address <= write_address + 17'h1;
end

always@(posedge pixel_clock or negedge reset_n)
    if(reset_n == 1'b0)
        delayed_pixel_complete <= 1'b0;
    else
        delayed_pixel_complete <= pixel_complete;

assign valid_data = frame_valid & delayed_pixel_complete;

endmodule

RAM 데이터 버퍼

수집된 이미지 데이터를 임시로 저장하기 위한 RAM 버퍼이다. FPGA 내부 블록 RAM을 사용하여 구현되며, 최소한의 용량만 사용하여 전체 프레임을 저장할 수 있도록 설계되었다.

VGA 컨트롤러

버퍼에 저장된 이미지 데이터를 VGA 인터페이스를 통해 디스플레이에 출력하는 컨트롤러이다. 640x480 해상도, 60Hz 주사율로 동작하도록 설정된다.

`timescale 1ns / 1ps
module vga_controller#(
    parameter DISPLAY_WIDTH = 12'd256,
    parameter DISPLAY_HEIGHT = 12'd160
)(
    input  wire clock,
    input  wire reset_n,
    input  wire [15:0] video_data,
    output wire horizontal_sync,
    output wire vertical_sync,
    output reg  [15:0] rgb_output,
    output reg  [16:0] read_address
);

reg  [11:0] horizontal_counter;
reg  [11:0] vertical_counter;
wire data_valid_signal;
wire [11:0] pixel_x;
wire [11:0] pixel_y;
wire display_area;

localparam H_SYNC_PULSE = 12'd96,
           H_BACK_PORCH = 12'd40,
           H_LEFT_BORDER = 12'd8,
           H_ACTIVE_PIXELS = 12'd640,
           H_RIGHT_BORDER = 12'd8,
           H_FRONT_PORCH = 12'd8,
           H_TOTAL_CYCLES = H_SYNC_PULSE + H_BACK_PORCH + H_LEFT_BORDER + H_ACTIVE_PIXELS + H_RIGHT_BORDER + H_FRONT_PORCH,
           V_SYNC_PULSE = 12'd2,
           V_BACK_PORCH = 12'd25,
           V_LEFT_BORDER = 12'd8,
           V_ACTIVE_LINES = 12'd480,
           V_RIGHT_BORDER = 12'd8,
           V_FRONT_PORCH = 12'd2,
           V_TOTAL_LINES = V_SYNC_PULSE + V_BACK_PORCH + V_LEFT_BORDER + V_ACTIVE_LINES + V_RIGHT_BORDER + V_FRONT_PORCH;

localparam RED_COLOR = 16'hF800,
           GREEN_COLOR = 16'h07E0,
           BLUE_COLOR = 16'h001F,
           BLACK_COLOR = 16'h0000;

always @(posedge clock or negedge reset_n) begin
    if (!reset_n) 
        horizontal_counter <= 12'd0;
    else if (horizontal_counter == H_TOTAL_CYCLES-1)
        horizontal_counter <= 12'd0;
    else
        horizontal_counter <= horizontal_counter + 12'd1;
end

always @(posedge clock or negedge reset_n) begin
    if (!reset_n)
        vertical_counter <= 12'd0;
    else if (vertical_counter == V_TOTAL_LINES-1)
        vertical_counter <= 12'd0;
    else if (horizontal_counter == H_TOTAL_CYCLES-1)
        vertical_counter <= vertical_counter + 12'd1;
end

assign horizontal_sync = (horizontal_counter < H_SYNC_PULSE) ? 1'b0 : 1'b1;
assign vertical_sync = (vertical_counter < V_SYNC_PULSE) ? 1'b0 : 1'b1;

assign data_valid_signal = (horizontal_counter >= H_SYNC_PULSE + H_BACK_PORCH + H_LEFT_BORDER -1) && 
                          (vertical_counter >= V_SYNC_PULSE + V_BACK_PORCH + V_LEFT_BORDER -1) && 
                          (horizontal_counter <= H_SYNC_PULSE + H_BACK_PORCH + H_LEFT_BORDER + H_ACTIVE_PIXELS -1) &&
                          (vertical_counter <= V_SYNC_PULSE + V_BACK_PORCH + V_LEFT_BORDER + V_ACTIVE_LINES -1);

assign pixel_x = (data_valid_signal) ? (horizontal_counter - (H_SYNC_PULSE + H_BACK_PORCH + H_LEFT_BORDER -1)) : 12'hFFF;
assign pixel_y = (data_valid_signal) ? (vertical_counter - (V_SYNC_PULSE + V_BACK_PORCH + V_LEFT_BORDER -1)) : 12'hFFF;

assign display_area = (pixel_x >= (H_ACTIVE_PIXELS - DISPLAY_WIDTH)/2 && pixel_x < (H_ACTIVE_PIXELS + DISPLAY_WIDTH)/2 && 
                      pixel_y >= (V_ACTIVE_LINES - DISPLAY_HEIGHT)/2 && pixel_y < (V_ACTIVE_LINES + DISPLAY_HEIGHT)/2);

always @(posedge clock or negedge reset_n) begin
    if (!reset_n)
        rgb_output <= BLACK_COLOR;
    else if (display_area)
        rgb_output <= video_data;
    else
        rgb_output <= BLACK_COLOR;
end 

always @(posedge clock or negedge reset_n) begin
    if (!reset_n) 
        read_address <= 17'h0;
    else if (vertical_counter == V_TOTAL_LINES-1)
        read_address <= 17'h0;
    else if (display_area)
        read_address <= read_address + 17'h1; 
end       

endmodule

탑 레벨 모듈

전체 시스템을 통합하는 탑 레벨 모듈이다. SCCB 컨트롤러, DVP 데이터 수집기, VGA 컨트롤러, RAM 버퍼 등 모든 서브 모듈을 연결하고 제어한다.

`timescale 1ns / 1ps
module top_level(
    input  wire system_clock,
    input  wire camera_pixel_clock,
    input  wire camera_frame_sync,
    input  wire camera_line_sync,
    input  wire [7:0] camera_data,
    input  wire reset_n,
    input  wire start_button,
    inout  wire sccb_data,
    output wire sccb_clock,
    output wire initialization_complete,
    output wire horizontal_sync,
    output wire vertical_sync,
    output wire[3:0] red_channel,
    output wire[3:0] green_channel,
    output wire[3:0] blue_channel
);

wire init_start;
wire sccb_operation_start;
wire sccb_operation_done;
wire [23:0] init_data;
wire vga_clock;
wire ram_write_enable;
wire [16:0] ram_write_address;
wire [15:0] ram_write_data;
wire [16:0] ram_read_address;
wire [15:0] ram_read_data;
wire [15:0] rgb_data;
assign red_channel = rgb_data[15:12];
assign green_channel = rgb_data[10:7];
assign blue_channel = rgb_data[4:1];

ov5640_config_table#(
    .IMG_WIDTH(16'd350),
    .IMG_HEIGHT(16'd218)
) ov5640_init_table(
    .clk(system_clock),
    .rst_n(reset_n),
    .init_start(init_start),
    .sccb_done(sccb_operation_done),
    .sccb_start(sccb_operation_start),
    .config_data(init_data),
    .init_complete(initialization_complete)
);

SCCB_controller sccb_ctrl(
    .clk(system_clock),
    .rst_n(reset_n),
    .sda(sccb_data),
    .scl(sccb_clock),
    .read_write(1'b0),
    .start_operation(sccb_operation_start),
    .device_address(7'b0111_100),
    .register_address(init_data[23:8]),
    .write_data(init_data[7:0]),
    .read_data(),
    .operation_complete(sccb_operation_done)
);

dvp_data_capture #(
    .FRAME_SKIP_COUNT(8'd10)
) dvp_capture(
    .pixel_clock(camera_pixel_clock),
    .reset_n(reset_n),
    .line_sync(camera_line_sync),
    .frame_sync(camera_frame_sync),
    .pixel_data(camera_data),
    .valid_data(ram_write_enable),
    .write_address(ram_write_address),
    .rgb565_output(ram_write_data)
);

vga_controller#(
    .DISPLAY_WIDTH(12'd350),
    .DISPLAY_HEIGHT(12'd218)
) vga_ctrl(
    .clock(vga_clock),
    .reset_n(reset_n),
    .read_address(ram_read_address),
    .video_data(ram_read_data),
    .horizontal_sync(horizontal_sync),
    .vertical_sync(vertical_sync),
    .rgb_output(rgb_data)
);

button_debounce button_filter(
    .i_clk(system_clock),
    .i_rstn(reset_n),
    .i_key(start_button),
    .ok(init_start)
);

vga_clock_generator clock_gen(
    .sys_clk(system_clock),
    .vga_clk(vga_clock)
);

image_ram ram_instance(
    .clka(camera_pixel_clock),
    .wea(ram_write_enable),
    .addra(ram_write_address),
    .dina(ram_write_data),
    .clkb(vga_clock),
    .addrb(ram_read_address),
    .doutb(ram_read_data)
);
endmodule

실행 결과

VGA 인터페이스를 통해 640x480@60Hz 해상도로 디스플레이에 실시간 영상을 출력하였다. 중심 영역 350x218 픽셀 크기로 표시되며, 약간의 화면 깨짐 현상이 발생하나 전반적으로 안정적인 동작을 확인할 수 있었다.

태그: FPGA Verilog OVA5640 DVP VGA

6월 15일 00:57에 게시됨