Virtual Interfaces and Monitors

Let’s extend the code from previous blog post and introduce virtual interfaces and monitors.

Virtual Interfaces

In the simplest of words Virtual Interface is a pointer to an actual physical interface in the design. That’s why the namesake. That begs the question, why do I need a virtual interface ? The answer is, you really don’t. As you saw in my previous example, you can stimulate a design’s inputs using a UVM testbench without needing a virtual interface. However, having a virtual interface will benefit your testbench and it’s architecture.

Consider virtual interfaces to be the bridges that connect the physical design’s inputs and outputs to the abstract world of classes and software. “Classes” in systemverilog are objects which are created when the constructor is called. It would just be weird for classes to have access to the real physical interfaces; that sort of explains why it’s illegal for classes to have physical interfaces. A virtual(or abstract) pointer/handle to the actual interface makes more sense for software. With virtual interfaces, you could do fancy stuff like having different instances of the same class point to different physical interfaces. Of course, this adds to re-usability of your classes (think agents/monitors).

Monitors

I am clubbing monitors with virtual interfaces mostly because the most obvious use of virtual interfaces is in Monitors (One can argue the same with Drivers too, I will visit Drivers sometime later). Monitors are probably the simplest of UVM components in most testbenches. The primary idea of having a monitor is to look at the signals on the design’s physical interface. Monitors can then add functionality based on the observed signals. In most testbenches, monitors send data to the Scoreboard via analysis ports. Why do we need monitors ? The answer is related to re-use of these monitors across standard interfaces in the design. Can you build a testbench without monitors ? Of course you can, but the testbench wouldn’t be too useful outside of a very specific use case.

In this simple proof of concept example, the monitor uses a virtual interface, and is simply going to print out signals it sees on the interface.

Design and it’s interface

I am reusing the same simple DUT, and adding an interface.

interface sq_if();
  logic [7:0] a;
  logic [15:0] sq_out;
  
  modport to_DUT (input a, output sq_out);
endinterface

module square(sq_if i1);

  always_comb begin
    i1.sq_out = i1.a*i1.a;
    $display("Inside RTL:: time = %0d, a=%0d, square=%0d", $time, i1.a, i1.sq_out);
  end
   
endmodule

Set the virtual interface

The “top” has code that associates the physical interface with it’s virtual counterpart. The physical interface is instantiated just like a module. The path to this physical interface is then passed to the virtual interface in the uvm_config_db. (If you aren’t aware of what the config_db is, think of it as a database where you can store values and retrieve values from anywhere in your testbench). This virtual interface can be retrieved from the config_db in other testbench components as required.

import uvm_pkg::*;

`include "sq_if.sv"
`include "sq_mon.sv"
`include "sq_test.sv"

module top;
  bit [7:0] a;
  bit [15:0] square_out;
  
  sq_if i_sq_if();

  square sq_i(i_sq_if.to_DUT);
  
  initial begin
    uvm_config_db #(virtual sq_if)::set(null,"uvm_test_top","sq_if", i_sq_if);
    run_test("sq_test");
  end
    
endmodule

A simple Monitor

The monitor retrieves the virtual interface from the config_db. It proceeds to print the value of the signals every time the input changes.

class sq_mon extends uvm_monitor;
  `uvm_component_utils(sq_mon)
  
  function new(string name, uvm_component parent=null);
    super.new(name,parent);   
  endfunction
  
  virtual task run_phase(uvm_phase phase);
    
    virtual sq_if vif0;  
    uvm_config_db #(virtual sq_if)::get(null,"uvm_test_top","sq_if",vif0);
    
    forever begin
      @ vif0.a ;
      $display("Inside Monitor:: time = %0d, vif0.a=%0d, vif0.square=%0d", $time, vif0.a, vif0.sq_out);
    end
    
  endtask
  
endclass

The Test

The test too retrieves the virtual interface from the config_db. This is only required because the test uses this to drive the DUT’s inputs.

The test also includes a handle and a constructor to the monitor. The test use UVM’s “build_phase” to construct the monitor. UVM phase mechanism divides the testbench into sequential parts and helps organize and architect the testbench better. Think of the build_phase as a function where all components of the testbench are created before jumping into the actual simulation (which happens in the run_phase).

class sq_test extends uvm_test;
  `uvm_component_utils(sq_test)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual sq_if vif0;
  
  virtual function void build_phase(uvm_phase phase);
    sq_mon mon1;
    
    uvm_config_db #(virtual sq_if)::get(null,"uvm_test_top","sq_if",vif0);
    mon1 = sq_mon::type_id::create("mon1", this);
  endfunction
    
  task run_phase(uvm_phase phase);

    $display("Test's Run Phase.");
    phase.raise_objection(this);
    #1; 
    vif0.a = 9;
    #1;
    vif0.a = 15;
    phase.drop_objection(this);
        
  endtask
  
endclass

Output

UVM_INFO @ 0: reporter [RNTST] Running test sq_test...
Inside RTL:: time = 0, a=x, square=x
Test's Run Phase.
Inside RTL:: time = 1, a=9, square=81
Inside Monitor:: time = 1, vif0.a=9, vif0.square=81
Inside RTL:: time = 2, a=15, square=225
Inside Monitor:: time = 2, vif0.a=15, vif0.square=225

Conclusion

There we have it. We created a virtual interface and used it to correctly capture the stimulus and response in the monitor. We used the uvm_config_db, and also made use of the build_phase.

Comparing this testbench to it’s predecessor in the previous post, we have added more abstraction, structure and re-usability. We are yet to add Drivers, Agents and Scoreboards. We will get there eventually.

References

https://verificationacademy.com/forums/systemverilog/why-do-we-need-virtual-interfaces-system-verilog

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.

UVM Sequences and Sequencers

In my previous post, I put together the least lines of code to write a “Hello, World!” program in SystemVerilog using UVM. That’s cute, but not really helpful. I want to work towards a skeleton code that turns into something useful, while still staying close to the basics.

This post is a pretty basic introduction to Sequences and Sequencers. I am going to print “Hello, World” from the sequencer(s) and the sequence(s). I am continuing to stay in the “programming world” so as to speak; there is no interaction with a DUT/RTL of any kind, no signals or clocks just yet.

Sequences

Let’s talk about Sequences first. “Sequences” are exactly what they say they are. A sequence is a series of steps that is to be executed. UVM sequences have a “body” method that gets executed when the sequence is called.

Sequences are started using sequence.start() task. The start() task (as defined in uvm_sequence_base.sv) allows me to provide a sequencer for this sequence to run on. For now think of the sequencers as an arbiter for the sequences. I can run multiple sequences on a sequencer; the start() task also accepts a priority number for the sequences. If you don’t provide a priority, the default priority is assumed.

A Basic Sequence

class hw_seq extends uvm_sequence;
  task body();
    $display("Hello, World! from sequence");
  endtask
endclass

Rest of the “Hello, World!” code

class my_uvm_hw_test extends uvm_test;
  `uvm_component_utils(my_uvm_hw_test)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  task run_phase(uvm_phase phase);
    hw_seq hw_seq_h;  
    hw_seq_h = new();

    $display("Starting HW sequence");
    hw_seq_h.start(null); //Null if no sequencer.
    
  endtask
endclass

module my_uvm_hw;
  initial
    run_test("my_uvm_hw_test");
endmodule

Expected Output

>>Starting HW sequence
>>Hello, World! from sequence

Please note that I have no time consuming events in the above example. If the sequence did involve time consuming events, UVM excepts to raise and drop exceptions.

Sequencers

The simplest way to think of a sequencer is as an arbiter for the sequences. I can start a bunch of sequences on a sequencer, and the sequencer will arbitrate between them. I can have the sequencer do a lot of fancy stuff and I intend to demonstrate this with an example; and will do this in a later post once I have discussed “sequence_items.”

A Basic Sequencer

class hw_seqr extends uvm_sequencer;
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
//The run_phase isn't required.
//It's just to display the Hello World.
  task run_phase(uvm_phase phase);
    $display("Hello, World! from the sequencer");
  endtask
  
endclass

Running sequences on a Sequencer

...
...
hw_seq_h.start(hw_seqr_h); //Passing sequencer instead of null.
...
...

Do note that if I am running just one sequence, I don’t really have to use a sequencer. Using a sequencer provides other benefits; being a “uvm_component” it makes it easier to communicate with other components (the most common usage is for the sequencer to communicate with the driver). A sequencer is usually associated with a parameterized sequence_item.

That’s it for sequences and sequencers. I now have a UVM program of sorts that uses a test, a uvm_sequence and a uvm_sequencer. It’s still a cute piece of code that doesn’t do much with respect to driving actual signals to a DUT. We need a couple more pieces for that to happen and I plan to discuss those in the next posts.

References and useful links

A simple “Hello, World!” in UVM

Hello, World!

Thanks for joining me!

This is the first post in this blog and let’s start with a classic, the “Hello, World!“. Traditionally “Hello, World!” is the first step in learning a new language. It’s simple, prints output text and gives you a quick feel of what the language offers.

Let’s look at a few “Hello, World!” programs in different programming languages and make some comments.

#include <stdio.h>
void main() {
  printf("Hello World");
}

Easy enough, I had say.

Java

class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

A Classification, I object.

Python

print("Hello, world!")

Really, this is it ?

Prologue

Universal Verification Methodology (UVM) is based on System-Verilog. I am not going to elaborate on what a methodology is, or how it came into being. That is a story for another day.

I have struggled to get started with UVM. There is no easy learning guide. Everything available online, barring a couple of web-pages, starts off in a complicated manner and doesn’t break down things for a beginner to understand. A lot of “Why’s” and “How’s” remain as questions long forgotten and conveniently ignored.

With this post, I intend to make things easy for beginners and novices. I will try to break down stuff that were hard for me to understand and digest. I will try to answer questions that I had. This post is also meant to serve me better understand these topics.

The first question that I have asked myself is :

How do I write a simple “Hello, World!” in UVM ? 

Let me add more context. All programming languages that I have learnt, I have indeed started with the quintessential “Hello, World!” It’s my first step, and a massive physiological barrier. If I can’t write a simple “Hello, World!”, is there any point in trying to do more?

The truth is, I have been doing more with UVM. Architecting and building massive UVM Test-benches using templates and available code. That’s pretty easy. However, every now and then, there comes a problem to solve for which I had to learn something I didn’t know. I soon realized that my foundations were shaky and I had to do something about it. 

After all, UVM is a methodology based on System-Verilog. I wanted to start with a “Hello, World!” program in System-Verilog and replace it part-by-part to make it a UVM “Hello, World!” The question I really wanted to answer was :

What’s the bare minimum code for UVM “Hello-World!” ?

System-Verilog

module my_sv_hw;
  initial
    $display("Hello, World!");
endmodule

I know this, alright!

UVM “Hello, World!”

One can argue, given that UVM is based on System-Verilog, the same program above is also the bare minimum “Hello, World!” in UVM. I tried the same argument in an interview, and I did not get the job for obvious reasons.

Let’s get started. I don’t really care about a Device-Under-Test (DUT). I am going to approach this as a programming language that can print a string. If System-Verilog can just print a string, UVM should also be able to achieve the same.

Step 1 : Use a ‘test’

UVM runs a “test“. I am going to replace the $display() with a “run_test()” in my UVM-Test-Bench. My code now looks like this:

module my_uvm_hw;
  initial
    run_test("my_uvm_hw_test");
endmodule

That’s it. UVM understands “run_test“, so I don’t really have to dig into what it does right now. “run_test“, true to what it sounds, is trying to run a test called “my_uvm_hw_test

Step 2a : Start Writing the test!

Using what little of object oriented programming I know, I am going to write my test. UVM says all tests need to extend from a base-class called “uvm_test”. Don’t ask why! Actually, it’s okay to ask “why”. I will address the “why” in another blog post. For now, I am going to assume that uvm_test base class is a basic class template that really does nothing by itself. This leads to the below skeleton.

class my_uvm_hw_test extends uvm_test;
endclass

Step 2b : The constructor

My class needs a constructor. It doesn’t really have to do anything. Nevertheless, you need a constructor to be present, and the constructor to reference the parent class’s constructor. This isn’t UVM, it’s just object-oriented-programming thing. The base class has a constructor, so extended class needs one too. This gives me the below code.

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

Step 2c : Run it already!

UVM works in phases. Think of it as stages. Think of it as Lego. When you are putting all pieces together, it’s the “build” phase. When you playing with the completed Lego, it’s the “run” phase. I have just one block here. I don’t need a build-phase (because there is nothing else to put together). I just need a run-phase. And that’s where I am going to sneak in my little string.

task run_phase(uvm_phase phase);
  $display("Hello, World!");
endtask

Step 3 : Some UVM Stuff

UVM requires me to register classes with “factory“. I think of this as a passport or Social Security Number. It helps UVM factory know who I am, anytime. This is done by the below code.

`uvm_component_utils(my_uvm_hw_test)

For the compiler/simulator to understand that you are talking UVM, you need to import things-you-need from the uvm-package. For now, I will import everything (using *). For example, uvm_package includes the code for “uvm_test” base class.

Step 4 : Put everything together

import uvm_pkg::*; 
class my_uvm_hw_test extends uvm_test;
  `uvm_component_utils(my_uvm_hw_test)

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

  task run_phase(uvm_phase phase);
    $display("Hello, World!");
  endtask
endclass

module my_uvm_hw;
  initial
    run_test("my_uvm_hw_test");
endmodule

Not too shabby, eh?

Final Thoughts

And I am done. I have a piece of code that I can understand. A piece of code that runs and displays “Hello, World!”. It’s simple and neat. I like it.

Of course, I can’t do much with just this. I had be surprised if this is useful in the real world at all. But that’s okay. Most “Hello, World!” programs aren’t immensely useful either. It’s a start. I can build on this!

There are still a few questions that remain unanswered. Can I skip the factory registration for my class ? Can I not have a constructor ? Do I really need a “run” phase ? I will try answering these questions in my upcoming posts.

Hopefully, this was helpful. If not, at least it was a decent read. Let me know your thoughts.