How to Build UVM Environment Part – 4

In short: This is the final part of building a UVM environment for the pipe design. Here we add the test, pass the real interface down to every component through the configuration database, add a coverage collector, build the top module that ties the design to the testbench, and finally run the first simulation. By the end you have a complete, running UVM testbench. All code follows the IEEE 1800.2 rules.

In part three we added the monitor, the agent, and the scoreboard, and grouped everything into an environment. The pieces are all built. What is left is to start them from a test, connect the design pins, and press go. That is what we do now.

A simple way to picture it

Think of opening night at a theatre. Every actor, prop, and light is ready from the earlier rehearsals. The director now picks the running order, makes sure the stage is wired to the lighting desk, and calls action. In UVM the test is the director, the configuration database is the wiring that connects the stage to the controls, and the top module is the theatre building that holds both the stage and the audience. This part is opening night.

The base test

The test sits at the top of the component tree. It creates the environment and, importantly, it is where you set the interface handle into the configuration database so every component below can fetch it. A base test holds the shared setup; specific tests extend it.

class base_test extends uvm_test;
  `uvm_component_utils(base_test)

  dut_env           env;
  virtual pipe_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);
    env = dut_env::type_id::create("env", this);
    // get the interface the top module placed for the test
    if (!uvm_config_db #(virtual pipe_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface for base_test")
    // pass it down to every component that asks for "vif"
    uvm_config_db #(virtual pipe_if)::set(this, "*", "vif", vif);
  endfunction
endclass

Why the set uses a wildcard path

The "*" in the set call means every component below this test can fetch the handle with the name vif. That is why the driver and monitor in the earlier parts could simply get vif and it worked. The test provides once, and the whole tree receives.

A specific test

A real test extends the base and starts a sequence in the run phase. It raises an objection so the run phase stays open, starts the sequence on the right sequencer, then drops the objection when done. Compare this small class to the base: almost everything is reused, and only the stimulus choice changes.

class random_test extends base_test;
  `uvm_component_utils(random_test)

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

  virtual task run_phase(uvm_phase phase);
    multiple_random_sequence seq;
    seq = multiple_random_sequence::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(env.penv_in.agent.sequencer);  // drive the input side
    phase.drop_objection(this);
  endtask
endclass

To make a second test, you extend the base again and start a different sequence, for example the directed one from part two. Because the base test holds all the setup, each new test is only a few lines. This is the reuse the whole series has been building toward.

Adding coverage

A test can pass without proving you tried enough cases. Functional coverage answers the question, did we actually exercise the interesting scenarios. A coverage collector is a subscriber: it listens to the same monitor stream as the scoreboard and samples a covergroup for each packet.

class pipe_coverage extends uvm_subscriber #(data_packet);
  `uvm_component_utils(pipe_coverage)

  data_packet pkt;

  covergroup cg;
    coverpoint pkt.cf;                 // did we hit every control value?
    coverpoint pkt.enable;             // both enabled and disabled?
    cross pkt.cf, pkt.enable;          // every mix of the two?
  endgroup

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

  // uvm_subscriber gives us this write for free
  virtual function void write(data_packet t);
    pkt = t;
    cg.sample();
  endfunction
endclass

The coverpoint on cf checks that the test used every control value, and the cross checks every mix of control value and enable. If coverage is low after a run, you know to add more or better stimulus. For more on this, see our guide on types of coverage metrics.

The package

SystemVerilog packages keep the class files together and control the order they compile in. All the testbench classes go into one package, included in dependency order, so anything that uses a class is compiled after that class is defined.

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

  `include "data_packet.sv"
  `include "pipe_sequencer.sv"
  `include "pipe_driver.sv"
  `include "pipe_monitor.sv"
  `include "pipe_agent.sv"
  `include "pipe_scoreboard.sv"
  `include "pipe_coverage.sv"
  `include "pipe_env.sv"
  `include "dut_env.sv"
  `include "base_test.sv"
  `include "random_test.sv"
endpackage

Order matters here: data_packet comes first because almost everything uses it, and the tests come last because they use everything else.

The top module

The top module is the piece that connects the design to the testbench. It creates the clock, instantiates the pipe design and the interface, places the interface into the configuration database, and calls run_test to start UVM.

module top;
  import uvm_pkg::*;
  import pipe_pkg::*;

  bit clk;
  bit rst_n;

  // generate a clock
  initial clk = 0;
  always #5 clk = ~clk;

  // apply reset at the start
  initial begin
    rst_n = 0;
    #12 rst_n = 1;
  end

  // interface and design under test
  pipe_if intf (.clk(clk), .rst_n(rst_n));

  pipe dut (
    .clk     (clk),
    .rst_n   (rst_n),
    .i_cf    (intf.cf),
    .i_en    (intf.enable),
    .i_data1 (intf.data_in1),
    .i_data2 (intf.data_in2),
    .o_data1 (intf.data_out1),
    .o_data2 (intf.data_out2)
  );

  initial begin
    // hand the interface to the test through the config DB
    uvm_config_db #(virtual pipe_if)::set(null, "uvm_test_top", "vif", intf);
    run_test("random_test");   // start UVM with this test
  end
endmodule

How the interface reaches every component

Follow the handle: the top module sets the interface for uvm_test_top, the base test gets it and sets it again with a wildcard for all children, and the driver and monitor get it in their build phases. This chain is the standard way the real pins reach deep into the testbench. We cover it in detail in our guide on passing the virtual interface through uvm_config_db.

Running the simulation

With the files ready, you compile the package and the top module, then run. On most simulators the flow is: compile all source, then run the top module while choosing the test. The test name can also come from the command line, which lets one build run many tests.

# Example run (tool commands vary)
# 1. compile the design, package, and top
# 2. run, picking the test by name:
simv +UVM_TESTNAME=random_test

Using +UVM_TESTNAME on the command line is better than hard coding the test in run_test, because you can then switch tests without recompiling. This small habit pays off across a big regression.

What a good run looks like

After the run, check three things: the simulation ended cleanly with no fatal errors, the scoreboard reported zero mismatches, and coverage reached the bins you cared about. If all three hold, the pipe passed this test. If any fail, the report tells you where to look.

Signal of healthWhere to see itWhat it means
No UVM_ERROR or UVM_FATALend of logthe run completed and checks passed
Scoreboard mismatches = 0scoreboard reportevery output matched the reference model
Coverage bins hitcoverage reportthe test actually exercised the cases you planned

The whole series in one view

Across four parts you built a complete UVM testbench from nothing: the design and interface and transaction in part one, the driver and sequences in part two, the monitor and agent and scoreboard in part three, and the test and top and first run here in part four. The same layered shape works for any design; only the details of the transaction and the reference model change.

Keep learning

To go further, browse the UVM guides for the factory, phasing, and reporting, the SystemVerilog guides for covergroups and constraints, and the Verification Fundamentals for the wider flow. Preparing for an interview? See the interview questions.

Frequently asked questions

Where is the interface handle first placed?

In the top module, with a config DB set for uvm_test_top. The base test then gets it and sets it again with a wildcard so every component below can fetch it.

Why does the base test set the interface with a wildcard path?

So that every component under the test, such as the driver and monitor, can get the handle by the same name. The test provides once and the whole tree receives.

What does a coverage subscriber do?

It listens to the monitor stream and samples a covergroup for each packet. This measures whether the test actually exercised the interesting cases, which a pass alone does not prove.

Why use +UVM_TESTNAME instead of hard coding the test?

Because you can then switch tests from the command line without recompiling. One build can run an entire regression of different tests.

What does run_test do?

It starts the UVM phasing, builds the whole component tree from the chosen test downward, and runs the simulation through all the phases to the end.

How do I know the design passed?

Check that the run ended with no UVM errors or fatals, the scoreboard reported zero mismatches, and coverage hit the bins you planned. All three together mean the test passed.

Similar Posts

5 Comments

  1. Hi Hardik,

    Thanks for writing such an awesome blog!

    Can u please check whether you forgot to include pipe_sequence_lib.sv development.
    Thanks,
    Vivek B

    1. Hi Vivek,

      I think you didn’t notice that I already mentioned like You might have questions that we are not having test_lib.sv and pipe_sequence_lib.sv those are nothing but all the tests and sequences in a single file for better readability.

      Regards,
      Hardik

  2. Hi Hardik,

    I have another doubt

    input_packets_collected.get(input_packet);
    output_packets_collected.get(output_packet);

    These two will get the same data packet right, since there is only one tlm_analysis_port called items_collected_port in Monitor which transmits input and output transactions in a singe data packet.

    Thanks,
    Vivek

  3. Hi Hardik,
    in base_test’s build function, I think one should add “uvm_phase phase” as the function parameter, e.g. –
    “function void build_phase(uvm_phase phase);”
    Otherwise “phase” is undeclared.
    thanks,
    ze’ev

Comments are closed.