// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

//
// Interface: clk_rst_if
// Generic clock and reset interface for clock events in various utilities
// It also generates o_clk and o_rst_n signals for driving clk and rst_n in the tb. The advantage is
// clk and rst_n can be completely controlled in course of the simulation.
// This interface provides methods to set freq/period, wait for clk/rst_n, apply rst_n among other
// things. See individual method descriptions below.
// inout clk
// inout rst_n

interface clk_rst_if #(
  parameter string IfName = "main"
) (
  inout clk,
  inout rst_n
);

`ifndef VERILATOR
  // include macros and import pkgs
  `include "dv_macros.svh"
  `include "uvm_macros.svh"
  import uvm_pkg::*;
`endif

  bit drive_clk;              // enable clk generation
  logic o_clk;                // output clk

  bit drive_rst_n;            // enable rst_n generation
  logic o_rst_n;              // output rst_n

  // clk params
  bit clk_gate      = 1'b0;   // clk gate signal
  int clk_period_ps = 20_000; // 50MHz default
  real clk_freq_mhz = 50;     // 50MHz default
  int duty_cycle    = 50;     // 50% default
  int max_jitter_ps = 1000;   // 1ns default
  bit recompute     = 1'b1;   // compute half periods when period/freq/duty are changed
  int clk_hi_ps;              // half period hi in ps
  int clk_lo_ps;              // half period lo in ps
  int jitter_chance_pc = 0;   // jitter chance in percentage on clock edge - disabled by default
  bit sole_clock = 1'b0;      // if true, this is the only clock in the system

  // use IfName as a part of msgs to indicate which clk_rst_vif instance
  string msg_id = {"clk_rst_if::", IfName};

  clocking cb @(posedge clk);
  endclocking

  clocking cbn @(negedge clk);
  endclocking

  // Wait for 'n' clocks based of postive clock edge
  task automatic wait_clks(int num_clks);
    repeat (num_clks) @cb;
  endtask

  // Wait for 'n' clocks based of negative clock edge
  task automatic wait_n_clks(int num_clks);
    repeat (num_clks) @cbn;
  endtask

  // wait for rst_n to assert and then deassert
  task automatic wait_for_reset(bit wait_negedge = 1'b1, bit wait_posedge = 1'b1);
    if (wait_negedge && ($isunknown(rst_n) || rst_n === 1'b1)) @(negedge rst_n);
    if (wait_posedge && (rst_n === 1'b0)) @(posedge rst_n);
  endtask

  // set the clk frequency in khz
  function automatic void set_freq_khz(int freq_khz);
    clk_freq_mhz = $itor(freq_khz) / 1000;
    clk_period_ps = 1000_000 / clk_freq_mhz;
    recompute = 1'b1;
  endfunction

  // set the clk frequency in mhz
  function automatic void set_freq_mhz(int freq_mhz);
    set_freq_khz(freq_mhz * 1000);
  endfunction

  // call this function at t=0 (from tb top) to enable clk and rst_n to be driven
  function automatic void set_active(bit drive_clk_val = 1'b1, bit drive_rst_n_val = 1'b1);
    time t = $time;
    if (t == 0) begin
      drive_clk = drive_clk_val;
      drive_rst_n = drive_rst_n_val;
    end
    else begin
`ifdef VERILATOR
      $error({msg_id, "this function can only be called at t=0"});
`else
      `uvm_fatal(msg_id, "this function can only be called at t=0")
`endif
    end
  endfunction

  // set the clk period in ns
  function automatic void set_period_ns(int period_ps);
    clk_period_ps = period_ps;
    clk_freq_mhz  = 1000_000 / clk_period_ps;
    recompute     = 1'b1;
  endfunction

  // set the duty cycle (1-99)
  function automatic void set_duty_cycle(int duty);
    if (!(duty inside {[1:99]})) begin
`ifdef VERILATOR
      $error({msg_id, $sformatf("duty cycle %0d is not inside [1:99]", duty)});
`else
      `uvm_fatal(msg_id, $sformatf("duty cycle %0d is not inside [1:99]", duty))
`endif
    end
    duty_cycle = duty;
    recompute = 1'b1;
  endfunction

  // set maximum jitter in ps
  function automatic void set_max_jitter_ps(int jitter_ps);
    max_jitter_ps = jitter_ps;
  endfunction

  // set jitter chance in percentage (0 - 100)
  // 0 - dont add any jitter; 100 - add jitter on every clock edge
  function automatic void set_jitter_chance_pc(int jitter_chance);
    if (!(jitter_chance inside {[0:100]})) begin
`ifdef VERILATOR
      $error({msg_id, $sformatf("jitter_chance %0d is not inside [0:100]", jitter_chance)});
`else
      `uvm_fatal(msg_id, $sformatf("jitter_chance %0d is not inside [0:100]", jitter_chance))
`endif
    end
    jitter_chance_pc = jitter_chance;
  endfunction

  // Set whether this is the only clock in the system. If true, various bits of timing randomisation
  // are disabled. If there's no other clock to (de)synchronise with, this should not weaken the
  // test at all.
  function automatic void set_sole_clock(bit is_sole = 1'b1);
    sole_clock = is_sole;
  endfunction

  // start / ungate the clk
  task automatic start_clk(bit wait_for_posedge = 1'b0);
    clk_gate = 1'b0;
    if (wait_for_posedge) wait_clks(1);
  endtask

  // stop / gate the clk
  function automatic void stop_clk();
    clk_gate = 1'b1;
  endfunction

  // add jitter to clk_hi and clk_lo half periods based on jitter_chance_pc
  function automatic void add_jitter();
    int jitter_ps;
    if ($urandom_range(1, 100) <= jitter_chance_pc) begin
`ifndef VERILATOR
      `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(jitter_ps,
          jitter_ps inside {[-1*max_jitter_ps:max_jitter_ps]};, "", msg_id)
`endif
      clk_hi_ps += jitter_ps;
    end
    if ($urandom_range(1, 100) <= jitter_chance_pc) begin
`ifndef VERILATOR
      `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(jitter_ps,
          jitter_ps inside {[-1*max_jitter_ps:max_jitter_ps]};, "", msg_id)
`endif
      clk_lo_ps += jitter_ps;
    end
  endfunction

  // can be used to override clk/rst pins, e.g. at the beginning of the simulation
  task automatic drive_rst_pin(logic val = 1'b0);
    o_rst_n = val;
  endtask

  // apply reset with specified scheme
  // TODO make this enum?
  // rst_n_scheme
  // 0 - fullly synchronous reset - it is asserted and deasserted on clock edges
  // 1 - async assert, sync dessert (default)
  // 2 - async assert, async dessert
  // 3 - clk gated when reset asserted
  // Note: for power on reset, please ensure pre_reset_dly_clks is set to 0
  // TODO #2338 issue workaround - $urandom call moved from default argument value to function body
  task automatic apply_reset(int pre_reset_dly_clks   = 0,
                             integer reset_width_clks = 'x,
                             int post_reset_dly_clks  = 0,
                             int rst_n_scheme         = 1);
    int dly_ps;
    if ($isunknown(reset_width_clks)) reset_width_clks = $urandom_range(4, 20);
    dly_ps = $urandom_range(0, clk_period_ps);
    wait_clks(pre_reset_dly_clks);
    case (rst_n_scheme)
      0: begin : sync_assert_deassert
        o_rst_n <= 1'b0;
        wait_clks(reset_width_clks);
        o_rst_n <= 1'b1;
      end
      1: begin : async_assert_sync_deassert
        #(dly_ps * 1ps);
        o_rst_n <= 1'b0;
        wait_clks(reset_width_clks);
        o_rst_n <= 1'b1;
      end
      2: begin : async_assert_async_deassert
        #(dly_ps * 1ps);
        o_rst_n <= 1'b0;
        wait_clks(reset_width_clks);
        dly_ps = $urandom_range(0, clk_period_ps);
        #(dly_ps * 1ps);
        o_rst_n <= 1'b1;
      end
      default: begin
`ifdef VERILATOR
        $error({msg_id, $sformatf("rst_n_scheme %0d not supported", rst_n_scheme)});
`else
        `uvm_fatal(msg_id, $sformatf("rst_n_scheme %0d not supported", rst_n_scheme))
`endif
      end
    endcase
    wait_clks(post_reset_dly_clks);
  endtask

  // clk gen
  initial begin
    // start driving clk only after the first por reset assertion. The fork/join means that we'll
    // wait a whole number of clock periods, which means it's possible for the clock to synchronise
    // with the "expected" timestamps.
    bit done;
    fork
      begin
        wait_for_reset(.wait_posedge(1'b0));

        // Wait a short time after reset before starting to drive the clock.
        #1ps;
        o_clk = 1'b0;

        done = 1'b1;
      end
      while (!done) #(clk_period_ps * 1ps);
    join

    // If there might be multiple clocks in the system, wait another (randomised) short time to
    // desynchronise.
    if (!sole_clock) #($urandom_range(0, clk_period_ps) * 1ps);

    forever begin
      if (recompute) begin
        clk_hi_ps = clk_period_ps * duty_cycle / 100;
        clk_lo_ps = clk_period_ps - clk_hi_ps;
        recompute = 1'b0;
      end
      if (jitter_chance_pc != 0) add_jitter();
      #(clk_lo_ps * 1ps);
      // wiggle output clk if not gated
      if (!clk_gate) o_clk = 1'b1;
      #(clk_hi_ps * 1ps);
      o_clk = 1'b0;
    end
  end

  assign clk   = drive_clk   ? o_clk   : 1'bz;
  assign rst_n = drive_rst_n ? o_rst_n : 1'bz;

endinterface
