Every article in this chapter so far has worked with reg_if and reg_block only as names a virtual interface handle points to. This article finally instantiates them as real hardware, in the one place that ever does: the top-level module that starts everything else running.

9 min read | Intermediate Level

What You'll Learn

  • What a testbench top module is, and why it's the root of the entire simulation
  • How to instantiate reg_if and reg_block, and hand the interface to UVM
  • What run_test() actually does once it's called
  • How clock and reset generation usually start out simple and grow more structured over time

What Is Testbench Top?

All verification components, interfaces, and the DUT itself are instantiated inside one top-level module, usually named tb_top. It's a static container holding everything the simulation needs, and it becomes the root of the entire design hierarchy - the one piece of this tutorial that isn't a UVM class at all, just plain SystemVerilog.

Instantiating reg_if and reg_block

This is the moment reg_if and reg_block stop being names a virtual interface handle points to, and become actual hardware:


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

  bit clk;
  always #10 clk = ~clk;

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

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

endmodule

reg_if0 is instantiated once, with clk connected to its clocking port. dut0 is instantiated with reg_if0 passed directly as its single port - the same reg_if vif port reg_block declared back in the first article of this chapter. No wrapper module is needed in between, since reg_block was written from the start to take the whole interface as one port rather than a long list of individual signals.

The uvm_config_db::set() call is the other half of something UVM Driver already described from the receiving side: reg_driver's uvm_config_db::get(this, "", "vif", vif) call is waiting for something to actually set that value somewhere above it in the hierarchy. This is that something.

The path string "uvm_test_top.*" and the scope-matching rules behind how get() and set() find each other are covered in full in Passing Virtual Interface. For now, the important thing is simply that it happens here, in tb_top, before run_test() is called.

Quick Check

Q: Why is the uvm_config_db::set() call placed before run_test("reg_test"), rather than after?

Show Answer

reg_test's build_phase builds reg_env, which eventually reaches reg_driver's own build_phase, where it calls uvm_config_db::get() for the virtual interface. That get() can only succeed if a matching set() already happened earlier in simulation time - and run_test() is what kicks off every one of those phases. Calling set() after run_test() would be too late; build_phase would already have run and found nothing.

How run_test() Actually Works

run_test() is the global task that's been doing real work every time it's been called in this chapter's examples, without yet being explained. At its core, it's a short, fixed sequence:


task run_test(string test_name = "");
  uvm_root top;
  uvm_coreservice_t cs;
  cs  = uvm_coreservice_t::get();
  top = cs.get_root();
  top.run_test(test_name);
endtask

The global run_test() just forwards to uvm_root's own version of the same method, which does the actual work: check whether +UVM_TESTNAME was passed on the simulator's command line, and use that instead if it was; create an instance of whichever test class won out; run every phase in order, starting with build_phase; wait until every phase reports done; print a summary; and end the simulation.

"Run every phase in order" is doing a lot of work in that sentence - build_phase, connect_phase, run_phase, and several others all happen here, in a specific, fixed sequence. That full sequence, and what each phase is actually for, is covered in UVM Phases.

+UVM_TESTNAME is worth calling out specifically, since it's the more flexible way to choose a test in practice: passing run_test("reg_test") with a fixed string means changing tests requires editing and recompiling tb_top itself, while leaving the argument blank and instead passing +UVM_TESTNAME=reg_write_only_test on the simulator's command line means choosing a different test every run, with no recompilation needed at all.

Clock and Reset, Simple to Start

reg_if0's clock in the example above is about as simple as clock generation gets - one always block, one fixed period, toggling forever. That's genuinely enough for reg_block, which only ever needs one clock domain. A design with multiple clock domains, or a need to change frequency or duty cycle mid-test, usually outgrows a bare always block and moves the same responsibility into its own agent - configurable from a test the same way reg_agent is, just for timing instead of data.

Reset follows the same pattern. reg_block's reset is a single active-low signal with no real sequencing behind it, so there's nothing more sophisticated to build here. A design with hardware and software reset paths, or a specific sequence of events before reset is considered complete, would typically get its own reset agent for the same reason a more complex clock would - once a piece of testbench infrastructure has its own configuration knobs worth exposing to a test, it's usually worth giving it the same structure every other agent already has.

A testbench top module can also create generic interfaces purely to tap or force internal signals buried deep inside a design's hierarchy, useful when a verification component needs access to a net that isn't exposed on any port. reg_block is simple enough - one module, one flat memory array - that it never needs this; the technique earns its place on designs with real internal hierarchy to reach into.

Common Mistakes

Mistake #1: Calling uvm_config_db::set() after run_test()

Since run_test() doesn't return until the entire simulation finishes, any code written after it in the same initial block never runs in time to matter - and even in a separate initial block, there's no guarantee it executes before build_phase needs the value. The set() call always belongs before run_test() is called.

Mistake #2: Forgetting to import uvm_pkg or include the macro file

tb_top is the one module in this entire tutorial that isn't itself a UVM class, but it still uses UVM constructs directly - uvm_config_db, run_test(). Without import uvm_pkg::*; and `include "uvm_macros.svh", none of that is visible, and the module won't compile.

Practice Exercise

Challenge: Add Waveform Dumping

Add the standard VCD waveform dump calls to tb_top, so a waveform viewer can inspect the simulation after it runs.

Hint

This needs a second initial block, separate from the one that calls run_test(), using $dumpfile and $dumpvars.

Solution
initial begin
  $dumpfile("dump.vcd");
  $dumpvars;
end
  

This block runs independently of the one starting the test - it doesn't need to come before or after run_test() specifically, since dumping setup and test execution are unrelated to each other.

Summary

In this article, you learned:

  • tb_top is the static, top-level module holding the DUT, the interface, and the call that starts UVM running
  • reg_if and reg_block are instantiated here as real hardware for the first time in this tutorial
  • uvm_config_db::set() has to run before run_test(), since the value it sets is needed during build_phase
  • run_test() runs every phase in a fixed order, optionally overridden by +UVM_TESTNAME on the command line

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5