How to build UVM Environment Part – 1

In short: This is part one of a four part series where we build a full UVM environment, step by step, to verify a small design. In this part we study the design under test (a two channel pipe), write its SystemVerilog interface, and create the transaction (the data packet) that will carry stimulus and results. We also start the driver. By the end you will have the three lowest layers ready: the pins, the transaction, and the block that turns transactions into pin activity. Every code sample follows the IEEE 1800 and IEEE 1800.2 rules.

In an earlier post on UVM testbench architecture we looked at the parts of a testbench: the sequence, driver, monitor, agent, scoreboard, environment, and test. That post explained what each part is for. This series is different: here we actually build one, from the first line to a running test, for a real design. Reading the two together gives you both the map and the road trip.

We will take it slowly and in a fixed order, because the order is the whole point of UVM. You cannot connect a component before it exists, and you cannot check a result before you can drive stimulus. So we build from the bottom up.

The four parts, and where we are

Here is the plan for the whole series so you always know where a piece fits:

  • Part 1 (this post): understand the design, write the interface, build the transaction, and start the driver.
  • Part 2: finish the driver, add the sequencer and sequences, and wire the driver to the sequencer.
  • Part 3: add the monitor, the agent, and the scoreboard for checking.
  • Part 4: build the environment and the test, pass the interface in, and run the first simulation.

A simple way to picture it

Building a testbench is like setting up a factory quality line for a new gadget. First you learn what the gadget does and what its plugs look like: that is the design and its interface. Then you decide what a single test order looks like on paper: that is the transaction. Then you hire the worker who follows one order and operates the machine: that is the driver. Only after those exist do you add an inspector and a manager. We follow exactly that order.

Step 1: understand the design under test

The design we will verify is a small block called pipe. It has two 16 bit data inputs and two 16 bit data outputs, a two bit control field that selects what the block does, and an enable. Everything is clocked, with an active low reset. Before writing any testbench code, you must be able to say in one sentence what the block does, because that sentence becomes your checker later.

Here are its ports, laid out so you can see each one and its job:

SignalDirectionWidthPurpose
clkinput1Clock. Everything samples on its rising edge.
rst_ninput1Active low reset. Low means reset is applied.
i_cfinput2Control field. Selects which operation the pipe performs.
i_eninput1Enable. When high, the pipe acts on the inputs.
i_data1input16First data input.
i_data2input16Second data input.
o_data1output16First data output.
o_data2output16Second data output.

Here is the module header so the port list is concrete. You do not need the internal logic to verify it from the outside; you only need the ports and the specification of what each control value does.

module pipe (clk,
             rst_n,
             i_cf,
             i_en,
             i_data1,
             i_data2,
             o_data1,
             o_data2);

  input         clk;
  input         rst_n;
  input  [1:0]  i_cf;    // control field: picks the operation
  input         i_en;   // enable
  input  [15:0] i_data1;
  input  [15:0] i_data2;

  output [15:0] o_data1;
  output [15:0] o_data2;

  // ... internal logic lives here ...
endmodule

A good habit: write down, in plain words, what each value of i_cf means. For example, one value might pass the data straight through, another might swap the two channels, another might add them. That table of meanings is your reference model in miniature, and you will use it when you write the scoreboard in part three.

Step 2: write the interface

A SystemVerilog interface is a named bundle of the design signals. Instead of passing eight separate wires around your testbench, you pass one interface handle. This keeps connections tidy and is the standard way UVM reaches the design pins.

interface pipe_if (input logic clk, input logic rst_n);
  logic [1:0]  cf;
  logic [15:0] data_in1;
  logic [15:0] data_in2;
  logic [15:0] data_out1;
  logic [15:0] data_out2;
  logic        enable;
endinterface : pipe_if

Why add clocking blocks and modports

The plain interface above works, but two additions make it safer. A clocking block groups signals against the clock so the driver samples and drives at the right time, which removes a whole class of race conditions. A modport names a direction view, so the driver only drives outputs and the monitor only reads. Here is the same interface with both, which is closer to real project code.

interface pipe_if (input logic clk, input logic rst_n);
  logic [1:0]  cf;
  logic [15:0] data_in1;
  logic [15:0] data_in2;
  logic [15:0] data_out1;
  logic [15:0] data_out2;
  logic        enable;

  // Driver view: sample and drive on the clock
  clocking drv_cb @(posedge clk);
    default input #1step output #1ns;
    output cf, data_in1, data_in2, enable;
    input  data_out1, data_out2;
  endclocking

  // Monitor view: sample only, never drive
  clocking mon_cb @(posedge clk);
    default input #1step;
    input cf, data_in1, data_in2, enable, data_out1, data_out2;
  endclocking

  modport DRV (clocking drv_cb, input clk, input rst_n);
  modport MON (clocking mon_cb, input clk, input rst_n);
endinterface : pipe_if

If you are new to interfaces, start with the plain version and add the clocking blocks once the basics run. Both are valid; the second is what you grow into. For a deeper look at why the virtual interface handle matters, see our guide on why we need a virtual interface.

Step 3: build the transaction

The transaction, often called the sequence item, is one unit of work on paper. For the pipe it holds the control field, the enable, the two inputs, and, so the checker can use them later, the two outputs and a delay. Marking the fields rand lets UVM fill them with legal random values.

class data_packet extends uvm_sequence_item;
  rand bit [1:0]  cf;
  rand bit        enable;
  rand bit [15:0] data_in1;
  rand bit [15:0] data_in2;
       bit [15:0] data_out1;   // filled by the monitor, not randomised
       bit [15:0] data_out2;
  rand int        delay;

  // Keep the gap between packets small and legal
  constraint timing_c { delay inside {[0:5]}; }

  `uvm_object_utils_begin(data_packet)
    `uvm_field_int(cf,        UVM_DEFAULT)
    `uvm_field_int(enable,    UVM_DEFAULT)
    `uvm_field_int(data_in1,  UVM_DEFAULT)
    `uvm_field_int(data_in2,  UVM_DEFAULT)
    `uvm_field_int(data_out1, UVM_DEFAULT)
    `uvm_field_int(data_out2, UVM_DEFAULT)
    `uvm_field_int(delay,     UVM_DEFAULT)
  `uvm_object_utils_end

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

Why register the fields

The uvm_field_int lines register each field with UVM. This one step gives you three things for free: printing (a readable dump of the packet), copying (a deep copy without hand written code), and comparing (used by the scoreboard). Skipping the field macros means writing all of that by hand, so for a first testbench, register the fields.

A second example: adding a targeted constraint

Randomising every field is a fine start, but real tests aim at interesting cases. Suppose the block is most fragile when both inputs are at their maximum value at the same time. You can add a constraint that hits that corner more often, without hand picking values.

class stress_packet extends data_packet;
  `uvm_object_utils(stress_packet)

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

  // Push toward the all-ones corner case more often
  constraint corner_c {
    data_in1 dist { 16'hFFFF := 3, [0:16'hFFFE] := 1 };
    data_in2 dist { 16'hFFFF := 3, [0:16'hFFFE] := 1 };
  }
endclass

Notice this new class extends data_packet rather than copying it. That is inheritance doing its job, and it is exactly the pattern from our inheritance in SystemVerilog guide. Later, the UVM factory can swap the plain packet for this stress packet in one line, without touching the sequence.

Step 4: start the driver

The driver takes one transaction at a time and turns it into signal activity on the interface. It needs a handle to the pins, which it fetches from the configuration database in its build phase. In part one we set up the driver skeleton and fetch the interface; in part two we fill in the drive logic and connect it to the sequencer.

class pipe_driver extends uvm_driver #(data_packet);
  `uvm_component_utils(pipe_driver)

  virtual pipe_if vif;   // handle to the design pins

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

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // Get the interface handle that the test placed in the config DB
    if (!uvm_config_db #(virtual pipe_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "virtual interface not set for pipe_driver")
  endfunction

  virtual task run_phase(uvm_phase phase);
    // Drive logic comes in Part 2
  endtask
endclass

Why the fatal check matters

The uvm_fatal on a failed get is not optional decoration. If the interface handle is missing, every later drive would fail in a confusing way. Failing loudly and early, right where the handle should have arrived, saves hours of debugging. This pattern, get the handle and fatal if it is missing, belongs in every component that needs the interface. We cover how the handle gets there in our guide on passing the virtual interface through uvm_config_db.

Common mistakes at this stage

  • Randomising the outputs. The outputs come from the design, not from the test. Keep data_out1 and data_out2 out of the rand list, or your checker will compare against nonsense.
  • Forgetting the field macros. Without them, print, copy, and compare do nothing useful, and the scoreboard cannot compare packets automatically.
  • Skipping the fatal check on the interface get. A missing handle then shows up as a strange timeout far away instead of a clear message.
  • Driving from the monitor view. If you use modports, make sure the driver uses DRV and the monitor uses MON, so the monitor never accidentally drives a pin.

Where we are now

At the end of part one you have the design understood, the interface written, the transaction built (with a bonus stress variant), and the driver skeleton fetching its interface. These are the foundation layers. In part two we complete the driver, add the sequencer and sequences, and make the first stimulus flow.

Keep learning

Want the surrounding ideas while you build? Browse the UVM guides for phasing, the factory, and sequences, the SystemVerilog guides for classes, constraints, and interfaces, and the Verification Fundamentals for the wider flow. Preparing for an interview? See the interview questions.

Frequently asked questions

If you are still deciding whether to build in UVM at all, it helps to review the advantages of UVM over plain SystemVerilog first, then come back here to turn the testbench architecture into a working environment.

Why do we build the testbench from the bottom up?

Because a component cannot be connected before it exists, and a result cannot be checked before stimulus can be driven. Building the design, interface, transaction, and driver first gives every later part something solid to attach to.

What is the difference between the interface and the transaction?

The interface is the set of real design pins and their timing. The transaction is one unit of work on paper, holding the values you want to drive and the results you expect. The driver turns a transaction into interface activity.

Why should the output fields not be randomised?

The outputs are produced by the design, so the test does not choose them. The monitor fills them in from the pins, and the scoreboard compares them against the expected result. Randomising them would overwrite real data.

What do the uvm_field_int macros give me?

They register each field so UVM can print, copy, and compare the packet automatically. This saves writing those methods by hand and lets the scoreboard compare two packets in one call.

Do I need clocking blocks and modports to start?

No. A plain interface works for a first testbench. Add clocking blocks to remove timing races and modports to lock down directions once the basics run cleanly.

What does uvm_fatal do if the interface is missing?

It stops the simulation immediately with a clear message at the exact point the handle was expected. That is far easier to debug than a strange failure much later in the run.

Similar Posts

2 Comments

  1. good work Hardik, and The Art of Verification team.
    you have narrate UVM. very well and its sound like beginner can pick up easy.

Comments are closed.