// fifo_uvm_example.sv: a complete, minimal UVM (IEEE 1800.2) testbench for an 8-deep synchronous FIFO.
// Illustrative example from origindatapartners.com/guides/uvm-testbench-example. Compile it with a simulator
// that ships the Accellera UVM library and select the test with +UVM_TESTNAME=fifo_random_test.
`timescale 1ns/1ps

// Design under test: first-word-fall-through FIFO, 8 entries of 8 bits.
module sync_fifo #(parameter int DEPTH = 8, parameter int WIDTH = 8) (
  input  logic             clk, rst_n,
  input  logic             wr_en, rd_en,
  input  logic [WIDTH-1:0] wdata,
  output logic [WIDTH-1:0] rdata,
  output logic             full, empty
);
  logic [WIDTH-1:0]         mem [DEPTH];
  logic [$clog2(DEPTH):0]   count;
  logic [$clog2(DEPTH)-1:0] wptr, rptr;
  assign full  = (count == DEPTH);
  assign empty = (count == 0);
  assign rdata = mem[rptr];
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wptr <= '0; rptr <= '0; count <= '0;
    end else begin
      if (wr_en && !full) begin
        mem[wptr] <= wdata;
        wptr      <= wptr + 1'b1;
      end
      if (rd_en && !empty) rptr <= rptr + 1'b1;
      count <= count + (wr_en && !full) - (rd_en && !empty);
    end
  end
endmodule

interface fifo_if (input logic clk);
  logic       rst_n, wr_en, rd_en, full, empty;
  logic [7:0] wdata, rdata;
endinterface

package fifo_pkg;
  `include "uvm_macros.svh"
  import uvm_pkg::*;

  typedef enum bit [1:0] {IDLE, WRITE, READ} fifo_op_e;

  // Transaction: one cycle of FIFO activity. A uvm_object, registered with the factory.
  class fifo_item extends uvm_sequence_item;
    rand fifo_op_e op;
    rand bit [7:0] data;
    constraint c_op { op dist {WRITE := 5, READ := 4, IDLE := 1}; }
    `uvm_object_utils_begin(fifo_item)
      `uvm_field_enum(fifo_op_e, op, UVM_ALL_ON)
      `uvm_field_int(data, UVM_ALL_ON)
    `uvm_object_utils_end
    function new(string name = "fifo_item");
      super.new(name);
    endfunction
  endclass

  // Nine writes (the last one meets a full FIFO), then nine reads (the last one meets an empty FIFO).
  class fill_drain_seq extends uvm_sequence #(fifo_item);
    `uvm_object_utils(fill_drain_seq)
    function new(string name = "fill_drain_seq");
      super.new(name);
    endfunction
    task body();
      repeat (9) send(WRITE);
      repeat (9) send(READ);
    endtask
    task send(fifo_op_e kind);
      fifo_item it = fifo_item::type_id::create("it");
      start_item(it);
      if (!it.randomize() with { op == kind; }) `uvm_error("SEQ", "randomize failed")
      finish_item(it);
    endtask
  endclass

  // Random mix of writes, reads and idle cycles.
  class random_rw_seq extends uvm_sequence #(fifo_item);
    `uvm_object_utils(random_rw_seq)
    rand int unsigned n_items = 200;
    function new(string name = "random_rw_seq");
      super.new(name);
    endfunction
    task body();
      repeat (n_items) begin
        fifo_item it = fifo_item::type_id::create("it");
        start_item(it);
        if (!it.randomize()) `uvm_error("SEQ", "randomize failed")
        finish_item(it);
      end
    endtask
  endclass
  // Driver: turns each item into one clock cycle of pin activity; drives idle when no item is waiting.
  class fifo_driver extends uvm_driver #(fifo_item);
    `uvm_component_utils(fifo_driver)
    virtual fifo_if vif;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif))
        `uvm_fatal("NOVIF", "virtual interface not set for the driver")
    endfunction
    task run_phase(uvm_phase phase);
      vif.wr_en <= 1'b0;
      vif.rd_en <= 1'b0;
      vif.wdata <= '0;
      wait (vif.rst_n === 1'b1);
      forever begin
        @(posedge vif.clk);
        seq_item_port.try_next_item(req);
        if (req == null) begin
          vif.wr_en <= 1'b0;
          vif.rd_en <= 1'b0;
        end else begin
          vif.wr_en <= (req.op == WRITE);
          vif.rd_en <= (req.op == READ);
          vif.wdata <= req.data;
          seq_item_port.item_done();
        end
      end
    endtask
  endclass

  // Monitor: samples the pins at each clock edge and reports accepted writes and reads on two analysis ports.
  class fifo_monitor extends uvm_monitor;
    `uvm_component_utils(fifo_monitor)
    virtual fifo_if vif;
    uvm_analysis_port #(fifo_item) wr_ap;  // each write the FIFO accepted
    uvm_analysis_port #(fifo_item) rd_ap;  // each word the FIFO gave out
    function new(string name, uvm_component parent);
      super.new(name, parent);
      wr_ap = new("wr_ap", this);
      rd_ap = new("rd_ap", this);
    endfunction
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif))
        `uvm_fatal("NOVIF", "virtual interface not set for the monitor")
    endfunction
    task run_phase(uvm_phase phase);
      wait (vif.rst_n === 1'b1);
      forever begin
        @(posedge vif.clk);
        if (vif.wr_en && !vif.full) begin
          fifo_item t = fifo_item::type_id::create("wr");
          t.op   = WRITE;
          t.data = vif.wdata;
          wr_ap.write(t);
        end
        if (vif.rd_en && !vif.empty) begin
          fifo_item t = fifo_item::type_id::create("rd");
          t.op   = READ;
          t.data = vif.rdata;
          rd_ap.write(t);
        end
      end
    endtask
  endclass

  // Agent: sequencer, driver and monitor for the FIFO ports. A passive agent builds only the monitor.
  class fifo_agent extends uvm_agent;
    `uvm_component_utils(fifo_agent)
    uvm_sequencer #(fifo_item) sqr;
    fifo_driver                drv;
    fifo_monitor               mon;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      mon = fifo_monitor::type_id::create("mon", this);
      if (get_is_active() == UVM_ACTIVE) begin
        sqr = uvm_sequencer #(fifo_item)::type_id::create("sqr", this);
        drv = fifo_driver::type_id::create("drv", this);
      end
    endfunction
    function void connect_phase(uvm_phase phase);
      if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
  endclass
  `uvm_analysis_imp_decl(_wr)
  `uvm_analysis_imp_decl(_rd)

  // Scoreboard: a queue is the reference model. Push on each accepted write; pop and compare on each read.
  class fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(fifo_scoreboard)
    uvm_analysis_imp_wr #(fifo_item, fifo_scoreboard) wr_imp;
    uvm_analysis_imp_rd #(fifo_item, fifo_scoreboard) rd_imp;
    bit [7:0] model[$];
    int unsigned n_compared;
    function new(string name, uvm_component parent);
      super.new(name, parent);
      wr_imp = new("wr_imp", this);
      rd_imp = new("rd_imp", this);
    endfunction
    function void write_wr(fifo_item t);
      model.push_back(t.data);
    endfunction
    function void write_rd(fifo_item t);
      bit [7:0] exp;
      if (model.size() == 0) begin
        `uvm_error("SB", "Read observed while the model is empty")
        return;
      end
      exp = model.pop_front();
      n_compared++;
      if (t.data !== exp)
        `uvm_error("SB", $sformatf("Mismatch: expected %0h, got %0h", exp, t.data))
    endfunction
    function void check_phase(uvm_phase phase);
      if (n_compared == 0) `uvm_error("SB", "No reads were compared")
    endfunction
    function void report_phase(uvm_phase phase);
      `uvm_info("SB", $sformatf("%0d reads compared, %0d words left in the FIFO", n_compared, model.size()), UVM_LOW)
    endfunction
  endclass

  // Coverage: fill level before each accepted operation, crossed with the operation.
  class fifo_coverage extends uvm_subscriber #(fifo_item);
    `uvm_component_utils(fifo_coverage)
    int       level;
    fifo_op_e op;
    covergroup cg;
      cp_level: coverpoint level {
        bins empty = {0}; bins one = {1}; bins mid = {[2:6]}; bins almost_full = {7}; bins full = {8};
      }
      cp_op: coverpoint op { bins wr = {WRITE}; bins rd = {READ}; }
      level_x_op: cross cp_level, cp_op {
        ignore_bins no_write_when_full = binsof(cp_level.full)  && binsof(cp_op.wr);
        ignore_bins no_read_when_empty = binsof(cp_level.empty) && binsof(cp_op.rd);
      }
    endgroup
    function new(string name, uvm_component parent);
      super.new(name, parent);
      cg = new();
    endfunction
    function void write(fifo_item t);
      op = t.op;
      cg.sample();
      level += (t.op == WRITE) ? 1 : -1;
    endfunction
  endclass

  // Environment: builds the agent, scoreboard and coverage, and connects the monitor to both.
  class fifo_env extends uvm_env;
    `uvm_component_utils(fifo_env)
    fifo_agent      agent;
    fifo_scoreboard sb;
    fifo_coverage   cov;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      agent = fifo_agent::type_id::create("agent", this);
      sb    = fifo_scoreboard::type_id::create("sb", this);
      cov   = fifo_coverage::type_id::create("cov", this);
    endfunction
    function void connect_phase(uvm_phase phase);
      agent.mon.wr_ap.connect(sb.wr_imp);
      agent.mon.rd_ap.connect(sb.rd_imp);
      agent.mon.wr_ap.connect(cov.analysis_export);
      agent.mon.rd_ap.connect(cov.analysis_export);
    endfunction
  endclass

  class fifo_base_test extends uvm_test;
    `uvm_component_utils(fifo_base_test)
    fifo_env env;
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
    function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      env = fifo_env::type_id::create("env", this);
    endfunction
  endclass

  // The test raises an objection so run_phase lasts until both sequences finish, then lets the last cycles settle.
  class fifo_random_test extends fifo_base_test;
    `uvm_component_utils(fifo_random_test)
    function new(string name, uvm_component parent);
      super.new(name, parent);
    endfunction
    task run_phase(uvm_phase phase);
      fill_drain_seq fd = fill_drain_seq::type_id::create("fd");
      random_rw_seq  rw = random_rw_seq::type_id::create("rw");
      phase.raise_objection(this);
      fd.start(env.agent.sqr);
      if (!rw.randomize() with { n_items inside {[100:300]}; }) `uvm_error("TEST", "randomize failed")
      rw.start(env.agent.sqr);
      #100ns;
      phase.drop_objection(this);
    endtask
  endclass
endpackage

// Top: clock, reset, DUT, interface; the virtual interface goes into the configuration database; run_test starts UVM.
module tb_top;
  import uvm_pkg::*;
  import fifo_pkg::*;
  logic clk = 1'b0;
  always #5ns clk = ~clk;
  fifo_if vif (clk);
  sync_fifo dut (
    .clk(clk), .rst_n(vif.rst_n), .wr_en(vif.wr_en), .rd_en(vif.rd_en),
    .wdata(vif.wdata), .rdata(vif.rdata), .full(vif.full), .empty(vif.empty)
  );
  initial begin
    vif.rst_n = 1'b0;
    repeat (2) @(posedge clk);
    vif.rst_n = 1'b1;
  end
  initial begin
    uvm_config_db#(virtual fifo_if)::set(null, "uvm_test_top.*", "vif", vif);
    run_test("fifo_random_test");
  end
endmodule
