This full UVM testbench puts every component of the register-block testbench together in one complete listing: the DUT and its interface, the transaction, driver, monitor, coverage collector, sequencer, sequence, agent, scoreboard, environment, test and top-level module. Each piece has its own article that explains it in detail. Here they appear in the order each one depends on the one before it, ready to compile and run.

Seeing all the code in one place makes it easier to follow a transaction from the sequence through the DUT and back to the scoreboard, and to understand how the pieces are wired together.

10 min read | Intermediate Level

What You'll Learn

  • Assemble every class of a UVM testbench into one compilable project
  • Trace the connections from sequence to DUT and from DUT to scoreboard and coverage
  • Follow the clock-by-clock timing of a write and a read
  • Recognise what a passing run should report

How the Pieces Connect

The test starts reg_sequence on the agent's sequencer. The driver pulls each transaction from the sequencer and drives it onto reg_if0. The monitor watches the same pins, rebuilds each transfer as a transaction, and writes it to one analysis port that feeds two subscribers: the coverage collector inside the agent and the scoreboard in the environment.

The code is usually split over a few files. The interface and DUT are design files, all classes go into one package so they share the UVM imports, and the top module ties everything together:


// reg_if.sv     - interface reg_if
// reg_block.sv  - module reg_block (the DUT)
// reg_pkg.sv    - package reg_pkg: every class from section 2 to 11
// tb_top.sv     - module tb_top

package reg_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  // reg_transaction, reg_driver, reg_monitor, reg_coverage, reg_sequencer,
  // reg_sequence, reg_agent, reg_scoreboard, reg_env, reg_test
endpackage

The Complete Testbench, in Build Order

1. The interface and the DUT

reg_block has sixteen 8-bit registers. A write stores wdata on the clock edge where wr_en is high. A read loads rdata on the edge where rd_en is high, so the value can be sampled from the next edge on.


interface reg_if (input bit clk);
  logic        rst_n;
  logic        wr_en;
  logic        rd_en;
  logic [3:0]  addr;
  logic [7:0]  wdata;
  logic [7:0]  rdata;
endinterface

module reg_block (reg_if vif);

  logic [7:0] mem [0:15];

  always @(posedge vif.clk or negedge vif.rst_n) begin
    if (!vif.rst_n) begin
      vif.rdata <= 8'h00;
    end
    else begin
      if (vif.wr_en) begin
        mem[vif.addr] <= vif.wdata;
      end
      else if (vif.rd_en) begin
        vif.rdata <= mem[vif.addr];
      end
    end
  end

endmodule

2. The transaction

One transaction is one write or one read. The constraint makes sure exactly one of wr_en and rd_en is set.


class reg_transaction extends uvm_sequence_item;

  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [3:0] addr;
  rand bit [7:0] wdata;
       bit [7:0] rdata;

  constraint wr_xor_rd_c {
    wr_en != rd_en;
  }

  `uvm_object_utils_begin(reg_transaction)
    `uvm_field_int(wr_en,  UVM_ALL_ON)
    `uvm_field_int(rd_en,  UVM_ALL_ON)
    `uvm_field_int(addr,   UVM_ALL_ON)
    `uvm_field_int(wdata,  UVM_ALL_ON)
    `uvm_field_int(rdata,  UVM_ALL_ON)
  `uvm_object_utils_end

  function new(string name = "reg_transaction");
    super.new(name);
  endfunction

endclass

3. The driver

The driver applies each transaction for one clock and then clears wr_en and rd_en, so the DUT and the monitor see every transfer exactly once. For a read it waits one more edge and copies rdata back into the transaction.


class reg_driver extends uvm_driver #(reg_transaction);

  `uvm_component_utils(reg_driver)

  virtual reg_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
      `uvm_fatal(get_type_name(), "Didn't get handle to virtual interface reg_if")
    end
  endfunction

  virtual task run_phase(uvm_phase phase);
    reg_transaction tr;

    forever begin
      seq_item_port.get_next_item(tr);

      @(posedge vif.clk);
      vif.wr_en <= tr.wr_en;
      vif.rd_en <= tr.rd_en;
      vif.addr  <= tr.addr;
      vif.wdata <= tr.wdata;

      @(posedge vif.clk);           // the DUT acts on this edge
      vif.wr_en <= 0;
      vif.rd_en <= 0;

      if (tr.rd_en) begin
        @(posedge vif.clk);         // rdata is stable from here
        tr.rdata = vif.rdata;
      end

      seq_item_port.item_done();
    end
  endtask

endclass

4. The monitor

The monitor checks the bus on every edge. A write is complete as soon as it is seen. For a read, it waits one more edge for rdata, exactly like the driver.


class reg_monitor extends uvm_monitor;

  `uvm_component_utils(reg_monitor)

  virtual reg_if vif;
  uvm_analysis_port #(reg_transaction) mon_analysis_port;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    mon_analysis_port = new("mon_analysis_port", this);
    if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
      `uvm_fatal(get_type_name(), "Didn't get handle to virtual interface reg_if")
    end
  endfunction

  virtual task run_phase(uvm_phase phase);
    reg_transaction tr;

    forever begin
      @(posedge vif.clk);

      if (vif.wr_en) begin
        tr = reg_transaction::type_id::create("tr", this);
        tr.wr_en = 1;
        tr.addr  = vif.addr;
        tr.wdata = vif.wdata;
        mon_analysis_port.write(tr);
      end
      else if (vif.rd_en) begin
        tr = reg_transaction::type_id::create("tr", this);
        tr.rd_en = 1;
        tr.addr  = vif.addr;

        @(posedge vif.clk);
        tr.rdata = vif.rdata;

        mon_analysis_port.write(tr);
      end
    end
  endtask

endclass

5. Coverage

The coverage collector is a uvm_subscriber, which already contains an analysis export and calls write() for every transaction the monitor sends.


class reg_coverage extends uvm_subscriber #(reg_transaction);

  `uvm_component_utils(reg_coverage)

  reg_transaction trans;

  covergroup cg_reg_transaction;
    coverpoint trans.addr;
    coverpoint trans.wr_en;
  endgroup

  function new(string name, uvm_component parent);
    super.new(name, parent);
    cg_reg_transaction = new();
  endfunction

  virtual function void write(reg_transaction t);
    trans = t;
    cg_reg_transaction.sample();
  endfunction

endclass

6. The sequencer


class reg_sequencer extends uvm_sequencer #(reg_transaction);

  `uvm_component_utils(reg_sequencer)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

endclass

7. The sequence

The sequence sends five random transactions. Its pre_body() and post_body() raise and drop an objection only when the sequence was started as a phase's default sequence. Here the test starts it directly, so get_starting_phase() returns null and the test's own objection keeps the phase alive.


class reg_sequence extends uvm_sequence #(reg_transaction);

  `uvm_object_utils(reg_sequence)

  function new(string name = "reg_sequence");
    super.new(name);
  endfunction

  virtual task pre_body();
    uvm_phase starting_phase = get_starting_phase();
    if (starting_phase != null)
      starting_phase.raise_objection(this);
  endtask

  virtual task body();
    repeat (5) begin
      req = reg_transaction::type_id::create("req");
      start_item(req);
      if (!req.randomize()) `uvm_error(get_type_name(), "Randomization failed")
      finish_item(req);
    end
  endtask

  virtual task post_body();
    uvm_phase starting_phase = get_starting_phase();
    if (starting_phase != null)
      starting_phase.drop_objection(this);
  endtask

endclass

8. The agent

An active agent builds the sequencer and driver. The monitor and coverage collector are always built, so a passive copy of this agent could still observe and measure the bus.


class reg_agent extends uvm_agent;

  `uvm_component_utils(reg_agent)

  reg_driver    m_driver;
  reg_monitor   m_monitor;
  reg_sequencer m_sequencer;
  reg_coverage  m_coverage;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if (get_is_active()) begin
      m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
      m_driver    = reg_driver::type_id::create("m_driver", this);
    end

    m_monitor  = reg_monitor::type_id::create("m_monitor", this);
    m_coverage = reg_coverage::type_id::create("m_coverage", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    if (get_is_active())
      m_driver.seq_item_port.connect(m_sequencer.seq_item_export);

    m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
  endfunction

endclass

9. The scoreboard

The scoreboard keeps its own copy of the registers. Writes update it, and reads are compared against it. A read from an address that has never been written is reported and skipped, because the DUT does not reset its registers.


class reg_scoreboard extends uvm_scoreboard;

  `uvm_component_utils(reg_scoreboard)

  uvm_analysis_imp #(reg_transaction, reg_scoreboard) ap_imp;

  bit [7:0] expected_mem [16];
  bit       written [16];

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap_imp = new("ap_imp", this);
  endfunction

  virtual function void write(reg_transaction tr);
    if (tr.wr_en) begin
      expected_mem[tr.addr] = tr.wdata;
      written[tr.addr]      = 1;
    end
    else if (tr.rd_en) begin
      if (!written[tr.addr]) begin
        `uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
      end
      else if (tr.rdata != expected_mem[tr.addr]) begin
        `uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
      end
      else begin
        `uvm_info(get_type_name(), $sformatf("addr=%0d matched: 0x%0h", tr.addr, tr.rdata), UVM_HIGH)
      end
    end
  endfunction

endclass

10. The environment


class reg_env extends uvm_env;

  `uvm_component_utils(reg_env)

  reg_agent      m_agent;
  reg_scoreboard m_scoreboard;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_agent      = reg_agent::type_id::create("m_agent", this);
    m_scoreboard = reg_scoreboard::type_id::create("m_scoreboard", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    m_agent.m_monitor.mon_analysis_port.connect(m_scoreboard.ap_imp);
  endfunction

endclass

11. The test

The test builds the environment, prints the component tree once elaboration is complete, and runs the sequence between its own raise_objection() and drop_objection().


class reg_test extends uvm_test;

  `uvm_component_utils(reg_test)

  reg_env m_top_env;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_top_env = reg_env::type_id::create("m_top_env", this);
  endfunction

  virtual function void end_of_elaboration_phase(uvm_phase phase);
    uvm_top.print_topology();
  endfunction

  virtual task run_phase(uvm_phase phase);
    reg_sequence m_seq = reg_sequence::type_id::create("m_seq");

    phase.raise_objection(this);
    m_seq.start(m_top_env.m_agent.m_sequencer);
    phase.drop_objection(this);
  endtask

endclass

12. Testbench top

The top module generates the clock, applies reset, puts the interface into the configuration database for everything below the test, and calls run_test(). The scope "uvm_test_top.*" is what lets the driver and monitor, several levels down, find the interface.


module tb_top;
  import uvm_pkg::*;
  import reg_pkg::*;
  `include "uvm_macros.svh"

  bit clk;
  always #10 clk = ~clk;

  reg_if    reg_if0 (clk);
  reg_block dut0    (reg_if0);

  initial begin
    reg_if0.rst_n = 0;
    reg_if0.wr_en = 0;
    reg_if0.rd_en = 0;
    #25 reg_if0.rst_n = 1;
  end

  initial begin
    uvm_config_db #(virtual reg_if)::set(null, "uvm_test_top.*", "vif", reg_if0);
    run_test("reg_test");
  end

endmodule

Timing of One Write and One Read

Following a transfer edge by edge shows why the driver and monitor are written the way they are:

  • Edge 1: the driver puts the transaction on the bus.
  • Edge 2: the DUT acts on it, storing the write or loading rdata for a read. The monitor sees the request on this edge, and the driver clears wr_en and rd_en. A write is now complete.
  • Edge 3 (reads only): rdata holds the value loaded on edge 2. The driver and monitor both sample it, and the monitor sends the read to the scoreboard.

Because the request is cleared after edge 2, the monitor never sees the same transfer twice. Without that, a write left on the bus for two edges would be reported twice, inflating coverage.

Running It

Compile the four files with a UVM-capable simulator and run with +UVM_TESTNAME=reg_test, or rely on the name passed to run_test(). A passing run shows:

  • The component tree from print_topology(), with m_top_env, m_agent and its four children, and m_scoreboard, matching the diagram above.
  • For any random read of an address that was not written earlier in the run, a "read before any write" message from the scoreboard.
  • A report summary with zero UVM_ERROR and zero UVM_FATAL messages.

The "matched" messages are at UVM_HIGH, so they appear only when you raise the verbosity with +UVM_VERBOSITY=UVM_HIGH. With only five random transactions, many reads will hit unwritten addresses. Raising the count in the sequence makes read-after-write checks far more frequent.

Quick Check

Q: If reg_scoreboard reported a mismatch, should you start debugging in the scoreboard or somewhere else?

Show Answer

Somewhere else, usually. The scoreboard only compares values the monitor captured; it does not drive or sample anything. A mismatch points to the driver, the DUT or the monitor's capture timing. Check the scoreboard's own logic only after those are ruled out.

Common Mistakes

Mistake #1: Leaving the request asserted after the transfer

If the driver never clears wr_en and rd_en, the DUT repeats the operation and the monitor reports it again on every following edge. Clear the request once the DUT has acted on it.

Mistake #2: Setting the interface with too narrow a scope

Setting the virtual interface with the scope "uvm_test_top" makes it visible only to the test itself. The driver and monitor then fail their get(). Use "uvm_test_top.*", or have the test pass the handle further down.

Mistake #3: Forgetting to import the class package in the top module

run_test("reg_test") finds the test through the factory, but the test class is registered only if its package is compiled and imported. A missing import reg_pkg::* usually ends in a fatal "cannot find test" error at time zero.

Summary

In this article, you learned:

  • How all twelve pieces of the register testbench fit together, in build order
  • How one monitor analysis port feeds both the coverage collector and the scoreboard
  • The edge-by-edge timing that the driver and monitor follow for writes and reads
  • Why the driver clears the request after each transfer
  • What a passing run of the testbench reports

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5