/*
* Copyright 2018-2022 F4PGA Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

module RAM_TEST #(
    parameter ADDR_WIDTH = 6,
    parameter DATA_WIDTH = 1,
    parameter IS_DUAL_PORT = 1,
    parameter RANDOM_ITERATION_PER_LOOP = 10,
    parameter LFSR_WIDTH = 16,
    parameter LFSR_POLY = 16'hD008,
    // How much to increment the address to move 1 full data width.
    parameter ADDRESS_STEP = 1,
    // Max address for this memory
    parameter MAX_ADDRESS = 63
) (
    input rst,
    input clk,
    // Memory connection
    input [DATA_WIDTH-1:0] read_data,
    output reg [DATA_WIDTH-1:0] write_data,
    output reg write_enable,
    output reg [ADDR_WIDTH-1:0] read_address,
    output reg [ADDR_WIDTH-1:0] write_address,
    // INIT ROM
    // When the memory is first initialized, it is expected to match the ROM
    // port.
    input [DATA_WIDTH-1:0] rom_read_data,
    output reg [ADDR_WIDTH-1:0] rom_read_address,
    // When an iteration is complete, success will be 1 for 1 cycle
    output reg loop_complete,
    // If an error occurs during a test, error will be 1 for each cycle
    // with an error.
    output reg error,
    // error_state will contain the state of test FSM when the error occured.
    output reg [7:0] error_state,
    // error_address will be the read address where the error occurred.
    output reg [ADDR_WIDTH-1:0] error_address,
    // expected_data will be the read value expected.
    output reg [DATA_WIDTH-1:0] expected_data,
    // actual_data will be the read value read.
    output reg [DATA_WIDTH-1:0] actual_data
);
    reg [7:0] state;
    reg [DATA_WIDTH-1:0] test_value;

    reg [$clog2(RANDOM_ITERATION_PER_LOOP)-1:0] rand_count;

    localparam START = 8'd1,
        VERIFY_INIT = 8'd2,
        WRITE_ZEROS = 8'd3,
        VERIFY_ZEROS = 8'd4,
        WRITE_ONES = 8'd5,
        VERIFY_ONES = 8'd6,
        WRITE_10 = 8'd7,
        VERIFY_10 = 8'd8,
        WRITE_01 = 8'd9,
        VERIFY_01 = 8'd10,
        WRITE_RANDOM = 8'd11,
        VERIFY_RANDOM = 8'd12,
        RESTART_LOOP = 8'd13;

    reg pause;
    reg lfsr_reset;
    reg wait_for_lfsr_reset;
    reg [LFSR_WIDTH-1:0] lfsr_seed;
    reg [LFSR_WIDTH-1:0] start_lfsr_seed;
    wire [LFSR_WIDTH-1:0] rand_data;

    LFSR #(
        .WIDTH(LFSR_WIDTH),
        .POLY(LFSR_POLY)
    ) lfsr (
        .rst(lfsr_reset),
        .clk(clk),
        .seed(lfsr_seed),
        .r(rand_data)
    );

    always @(posedge clk) begin
        if(rst) begin
            state <= START;
            error <= 0;
            write_enable <= 0;
            lfsr_reset <= 1;
            lfsr_seed <= 1;
        end else begin
            case(state)
                START: begin
                    lfsr_reset <= 0;
                    state <= VERIFY_INIT;
                    read_address <= 0;
                    rom_read_address <= 0;
                    write_enable <= 0;
                    error <= 0;
                end
                VERIFY_INIT: begin
                    if(rom_read_data != read_data) begin
                        error <= 1;
                        error_state <= state;
                        error_address <= read_address;
                        expected_data <= rom_read_data;
                        actual_data <= read_data;
                    end else begin
                        error <= 0;
                    end

                    if(read_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                        read_address <= read_address + ADDRESS_STEP;
                        rom_read_address <= rom_read_address + ADDRESS_STEP;
                    end else begin
                        read_address <= 0;
                        write_address <= 0;
                        write_enable <= 1;
                        write_data <= {DATA_WIDTH{1'b0}};
                        state <= WRITE_ZEROS;
                    end
                end
                WRITE_ZEROS: begin
                    loop_complete <= 0;
                    if(write_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                        write_address <= write_address + ADDRESS_STEP;
                    end else begin
                        read_address <= 0;
                        write_address <= 0;
                        write_enable <= 0;
                        pause <= 1;
                        state <= VERIFY_ZEROS;
                    end
                end
                VERIFY_ZEROS: begin
                    if(pause) begin
                        pause <= 0;
                    end else begin
                        if(read_data != {DATA_WIDTH{1'b0}}) begin
                            error <= 1;
                            error_state <= state;
                            error_address <= read_address;
                            expected_data <= {DATA_WIDTH{1'b0}};
                            actual_data <= read_data;
                        end else begin
                            error <= 0;
                        end

                        if(read_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                            read_address <= read_address + ADDRESS_STEP;
                        end else begin
                            read_address <= 0;
                            write_address <= 0;
                            write_enable <= 1;
                            write_data <= {DATA_WIDTH{1'b1}};
                            state <= WRITE_ONES;
                        end
                    end
                end
                WRITE_ONES: begin
                    // If dual port, data should still be zero.
                    if(IS_DUAL_PORT) begin
                        if(read_data != {DATA_WIDTH{1'b0}}) begin
                            error <= 1;
                            error_state <= state;
                            error_address <= read_address;
                            expected_data <= {DATA_WIDTH{1'b0}};
                            actual_data <= read_data;
                        end else begin
                            error <= 0;
                        end
                    end else begin
                        if(read_data != {DATA_WIDTH{1'b1}}) begin
                            error <= 1;
                            error_state <= state;
                            error_address <= read_address;
                            expected_data <= {DATA_WIDTH{1'b1}};
                            actual_data <= read_data;
                        end else begin
                            error <= 0;
                        end
                    end

                    if(write_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                        read_address <= read_address + ADDRESS_STEP;
                        write_address <= write_address + ADDRESS_STEP;
                    end else begin
                        read_address <= 0;
                        write_address <= 0;
                        write_enable <= 0;
                        state <= VERIFY_ONES;
                        pause <= 1;
                    end
                end
                VERIFY_ONES: begin
                    if(pause) begin
                        pause <= 0;
                    end else begin
                        if(read_data != {DATA_WIDTH{1'b1}}) begin
                            error <= 1;
                            error_state <= state;
                            error_address <= read_address;
                            expected_data <= {DATA_WIDTH{1'b1}};
                            actual_data <= read_data;
                        end else begin
                            error <= 0;
                        end

                        if(read_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                            read_address <= read_address + ADDRESS_STEP;
                        end else begin
                            state <= WRITE_RANDOM;
                            write_enable <= 1;
                            write_address <= 0;
                            lfsr_seed <= rand_data;
                            write_data <= rand_data[DATA_WIDTH-1:0];
                            read_address <= 0;
                        end
                    end
                end
                WRITE_RANDOM: begin
                    if(write_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                        write_address <= write_address + ADDRESS_STEP;
                        write_data <= rand_data[DATA_WIDTH-1:0];
                    end else begin
                        read_address <= 0;
                        write_address <= 0;
                        write_enable <= 0;
                        state <= VERIFY_RANDOM;

                        // Return LFSR to state at beginning of WRITE_RANDOM.
                        lfsr_reset <= 1;
                        wait_for_lfsr_reset <= 1;
                    end
                end
                VERIFY_RANDOM: begin
                    if(wait_for_lfsr_reset) begin
                        wait_for_lfsr_reset <= 0;
                        lfsr_reset <= 1;
                    end else begin
                        lfsr_reset <= 0;
                        if(read_data != rand_data[DATA_WIDTH-1:0]) begin
                            error <= 1;
                            error_state <= state;
                            error_address <= read_address;
                            expected_data <= rand_data[DATA_WIDTH-1:0];
                            actual_data <= read_data;
                        end else begin
                            error <= 0;
                        end

                        if(read_address + ADDRESS_STEP <= MAX_ADDRESS) begin
                            read_address <= read_address + ADDRESS_STEP;
                        end else begin
                            state <= RESTART_LOOP;
                        end
                    end
                end
                RESTART_LOOP: begin
                    loop_complete <= 1;
                    error <= 0;
                    read_address <= 0;
                    write_address <= 0;
                    write_enable <= 1;
                    write_data <= {DATA_WIDTH{1'b0}};
                    state <= WRITE_ZEROS;
                end
                default: begin
                    state <= START;
                    error <= 0;
                end
            endcase

        end
    end
endmodule
