// Copyright 2018 ETH Zurich and University of Bologna.
//
// Copyright and related rights are licensed under the Solderpad Hardware
// License, Version 0.51 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law
// or agreed to in writing, software, hardware and materials distributed under
// this 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.
//
// Fabian Schuiki <fschuiki@iis.ee.ethz.ch>

timeunit 0.1ns/1ps;

module cdc_fifo_tb;

  parameter bit INJECT_SRC_STALLS = 0;
  parameter bit INJECT_DST_STALLS = 0;
  parameter int UNTIL = 100000;
  parameter int DEPTH = 0;
  parameter bit GRAY = 0;

  time tck_src = 10ns;
  time tck_dst = 10ns;
  bit src_done = 0;
  bit dst_done = 0;
  bit done;
  assign done = src_done & dst_done;

  // Signals of the design under test.
  logic        src_rst_ni  = 1;
  logic        src_clk_i   = 0;
  logic [31:0] src_data_i  = 0;
  logic        src_valid_i = 0;
  logic        src_ready_o;

  logic        dst_rst_ni  = 1;
  logic        dst_clk_i   = 0;
  logic [31:0] dst_data_o;
  logic        dst_valid_o;
  logic        dst_ready_i = 0;

  assert property (@(posedge src_clk_i) !$isunknown(src_valid_i) && !$isunknown(src_ready_o));
  assert property (@(posedge dst_clk_i) !$isunknown(dst_valid_o) && !$isunknown(dst_ready_i));
  assert property (@(posedge src_clk_i) src_valid_i |-> !$isunknown(src_data_i));
  assert property (@(posedge dst_clk_i) dst_valid_o |-> !$isunknown(dst_data_o));

  // Instantiate the design under test.
  if (GRAY)
    cdc_fifo_gray #(.T(logic [31:0]), .LOG_DEPTH(DEPTH)) i_dut (.*);
  else
    cdc_fifo_2phase #(.T(logic [31:0]), .LOG_DEPTH(DEPTH)) i_dut (.*);

  // Mailbox with expected items on destination side.
  mailbox #(int) dst_mbox = new();
  int num_sent = 0;
  int num_received = 0;
  int num_failed = 0;

  // Clock generators.
  initial begin
    static int num_items, num_clks;
    num_items = 10;
    num_clks = 0;
    #10ns;
    src_rst_ni = 0;
    #10ns;
    src_rst_ni = 1;
    #10ns;
    while (!done) begin
      src_clk_i = 1;
      #(tck_src/2);
      src_clk_i = 0;
      #(tck_src/2);

      // Modulate the clock frequency.
      num_clks++;
      if (num_sent >= num_items && num_clks > 10) begin
        num_items = num_sent + 10;
        num_clks = 0;
        tck_src = $urandom_range(1000, 10000) * 1ps;
        assert(tck_src > 0);
      end
    end
  end

  initial begin
    static int num_items, num_clks;
    num_items = 10;
    num_clks = 0;
    #10ns;
    dst_rst_ni = 0;
    #10ns;
    dst_rst_ni = 1;
    #10ns;
    while (!done) begin
      dst_clk_i = 1;
      #(tck_dst/2);
      dst_clk_i = 0;
      #(tck_dst/2);

      // Modulate the clock frequency.
      num_clks++;
      if (num_received >= num_items && num_clks > 10) begin
        num_items = num_received + 10;
        num_clks = 0;
        tck_dst = $urandom_range(1000, 10000) * 1ps;
        assert(tck_dst > 0);
      end
    end
  end

  // Source side sender.
  task src_cycle_start;
    #(tck_src*0.8);
  endtask

  task src_cycle_end;
    @(posedge src_clk_i);
  endtask

  initial begin
    @(negedge src_rst_ni);
    @(posedge src_rst_ni);
    repeat(3) @(posedge src_clk_i);
    for (int i = 0; i < UNTIL; i++) begin
      static integer stimulus;
      static int cooldown;
      stimulus = $random();
      src_data_i  <= #(tck_src*0.2) stimulus;
      src_valid_i <= #(tck_src*0.2) 1;
      dst_mbox.put(stimulus);
      num_sent++;
      src_cycle_start();
      while (!src_ready_o) begin
        src_cycle_end();
        src_cycle_start();
      end
      src_cycle_end();
      src_valid_i <= #(tck_src*0.2) 0;

      // Insert a random cooldown period.
      if (INJECT_SRC_STALLS) begin
        cooldown = $urandom_range(0, 40);
        if (cooldown < 20) repeat(cooldown) @(posedge dst_clk_i);
      end
    end
    src_done = 1;
  end

  // Destination side receiver.
  task dst_cycle_start;
    #(tck_dst*0.8);
  endtask

  task dst_cycle_end;
    @(posedge dst_clk_i);
  endtask

  initial begin
    @(negedge dst_rst_ni);
    @(posedge dst_rst_ni);
    repeat(3) @(posedge dst_clk_i);
    while (!src_done || dst_mbox.num() > 0) begin
      static integer expected, actual;
      static int cooldown;
      dst_ready_i <= #(tck_dst*0.2) 1;
      dst_cycle_start();
      while (!dst_valid_o) begin
        dst_cycle_end();
        dst_cycle_start();
      end
      actual = dst_data_o;
      num_received++;
      if (dst_mbox.num() == 0) begin
        $error("unexpected transaction: data=%0h", actual);
        num_failed++;
      end else begin
        dst_mbox.get(expected);
        if (actual != expected) begin
          $error("transaction mismatch: exp=%0h, act=%0h", expected, actual);
          num_failed++;
        end
      end
      dst_cycle_end();
      dst_ready_i <= #(tck_dst*0.2) 0;

      // Insert a random cooldown period.
      if (INJECT_DST_STALLS) begin
        cooldown = $urandom_range(0, 40);
        if (cooldown < 20) repeat(cooldown) @(posedge dst_clk_i);
      end
    end

    if (num_sent != num_received) begin
      $error("%0d items sent, but %0d items received", num_sent, num_received);
    end
    if (num_failed > 0) begin
      $error("%0d/%0d items mismatched", num_failed, num_sent);
    end else begin
      $info("%0d items passed", num_sent);
    end

    dst_done = 1;
  end

endmodule
