Driving a DUT with a UVM TestBench – Basics

It’s been a while since my last post. Reviving this blog, I want to continue on the path of simplifying UVM for beginners. The previous articles discussed a basic “Hello World” in UVM, and some basics on UVM Sequences. This article will show how to drive signals on a sample DUT from a UVM testbench.

Most online examples and tutorials add a layer of complexity to basic UVM testbenches. You will hear virtual interfaces, drivers, monitors, scoreboards, sequences, sequencers, factory, config_db, sequence items, phases yada yada yada. It’s usually a puzzle that doesn’t make sense initially and takes a lot of time figuring out the whys and hows.

In this example, I will ignore all that’s un-necessary or at least try to. I will keep it real simple. While it doesn’t make much practical sense, I hope to answer the most of whys and hows for later on. This should later help answer questions like “Why do need a virtual interface?”, “Do I need a Driver, Monitor, Agent and Scoreboard every time?”, “Why do I need separate sequences?”, “Why do I need a sequencer?” and so on.

A Simple Design Under Test (DUT)

The DUT doesn’t really need an explanation. No reset or clocks, just combinational logic that squares a number. I am adding a display statement inside the DUT to confirm that my stimulus indeed was exercised.

module square(input bit  [7:0]  a, output bit [15:0] square);
  
  always_comb begin
    square = a*a;
    $display("Inside RTL:: time = %0d, a=%0d, square=%0d", $time, a, square);
  end
  
endmodule

UVM Test to stimulate the DUT

The plan is to drive the DUT’s input. That’s always the plan with any testbench!

I am using 2 files here. A top/toplevel and a test. The top instantiates the DUT, and calls the “run_test”. You will see that this is very similar to the “Hello world” program from this article.

Top / Toplevel

import uvm_pkg::*;

`include "sq_test.sv"

module top;
  bit [7:0] a;
  bit [15:0] square_out;

  square sq_i(a,square_out);
  
  initial begin
    run_test("sq_test");
  end
    
endmodule

The Test

Again, you will see that this is similar to the “Hello World” program from here. During the run phase, an “objection” is raised to keep the test alive. A UVM test concludes once all “objections” are dropped (or concludes immediately if there are no “objections” raised). The “#1” delay is just to show that the test can indeed progress simulation time. The test drives a hard-coded value to the DUT. No randomizations, no virtual interfaces, nothing fancy, just proof of concept.

class sq_test extends uvm_test;
  `uvm_component_utils(sq_test)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
    
  task run_phase(uvm_phase phase);
       $display("Test's Run Phase.");
    
    phase.raise_objection(this);
    #1;  // Just 
    top.a = 9;
    phase.drop_objection(this);
        
  endtask
  
endclass

Output

This was compiled and simulated with edaplaground’s Cadence Xcelium 20.09. The output is pretty much what you want and expect.

xcelium> run
Inside RTL:: time = 0, a=0, square=0
Inside RTL:: time = 0, a=0, square=0
Test's Run Phase.
Inside RTL:: time = 1, a=9, square=81

Conclusions

As mentioned before, there are limited practical applications to a Testbench like the one above. It’s too simple, doesn’t really make use of the powerful OOP concepts in SystemVerilog. There are hard-codings and makes it impossible for re-use. There are a bunch of other shortcomings.

The plan is to start with this code and work our way up to making this a more practical and powerful testbench. In the next article, let us try to answer the questions from the introduction. Let us introduce virtual sequencers, sequences, drivers and so on and how that helps make this a better testbench.

One thought on “Driving a DUT with a UVM TestBench – Basics

Leave a comment