UVM Testbench1: D Flip Flop

UVM Testbench project1 is to verify a simple D flip-flop. Design and verification codes are mentioned and can be run in the EDA playground using simple instructions for better understanding.


D Flip Flop design and Truth Table

D Flip-Flow Design code in System Verilog

module dff(
    input clk,
    input rst,
    input [4:0] din,  
    output reg [4:0] qout  
    );
      
    always@(posedge clk)
    begin
      //If reset, output is 0
      if(rst)
        qout <= 4'b0;
        //Else output follows input   
      else
        qout <= din;
    end
endmodule


//Interface
interface dif;
   logic clk;
   logic rst;
   logic [3:0] d;
   logic [3:0] q;
endinterface

UVM Testbench block diagram

The diagram below represents how our testbench should look. NOTE that the connections are not shown; it’s a rough diagram of our Testbench.

8 Simple steps to create this UVM testbench

1. Create Transaction class
2. Create Sequencer, Driver, and Monitor classes
3. Create Agent class and connect Sequencer and Driver
4. Create Scoreboard
5. Create the Environment class and connect the scoreboard with the agent class
6. Create Testbench class
7. Create a Sequence and Testcase
8. Create Top module

STEP 1: Create a transaction class

The transaction class is a dynamic component of Testbench, which means it will not stay till the end of the simulation, so it comes under the category of uvm_object, and we will extend it from uvm_sequence_item. Since our D Flip Flop design has 4-bit input and output ports, the transaction class should follow the same because the motive of the transaction class is to make a packet of input-output data. Create 4-bit 2 variables called ‘d’ and ‘q’, where they represent the input and output of the D flip flop, respectively. Also, create a 1-bit reset variable “rst”. Since we want our inputs (‘d’ and ‘rst’) to be random, that’s why they are declared ‘rand’.

Now, as per the guidelines, for a uvm_object, we need to write a new function with a single argument of type string, which is the name of the class. Inside this new function, call super.new() with the same argument. The reason for calling super.new() is to call the new function of uvm_sequence_item. For example, you then create another class by extending the transaction class, you should call super.new() in the child class so the parent new() function is executed. Sometimes new() can also set some variables to the default value, so it’s important not to miss this.

Now, the most important part here is to write uvm_object utility macro to register our class with the factory. Here we have used begin and end with uvm_object_utils for field automation of all variables. If you don’t know the importance of uvm_object_utils or why factory is important, read the UVM articles. Inside begin and end, we’re using `uvm_field macros for variables. The importance of these lines is that they will enable automation on these variables, like print, copy, compare, etc. To know it in more detail, revisit the UVM basics.

At the end of the code, we’ve used the pre- and post randomize function calls. This can be skipped but it’s a good practice and this will make easier to understand how things are working when you look into log for these print statements.

class transaction extends uvm_sequence_item;
  
  rand bit [3:0] d;
  rand bit rst;
  bit [3:0] q;
 
  //New function
  function new(string name="transaction");
    super.new(name);
  endfunction
   
  //Object utility macro
  `uvm_object_utils_begin(transaction)
   `uvm_field_int(d,  UVM_ALL_ON)
   `uvm_field_int(rst, UVM_ALL_ON)
   `uvm_field_int(q, UVM_ALL_ON)
  `uvm_object_utils_end
    
  //Pre randomize function with a print statement
  function void pre_randomize();
    `uvm_info(get_type_name(),$sformatf("Pre-Randomize D %0d Rst %0d ",d,rst), UVM_LOW);
  endfunction
   
  //Post randomize function with a print statement 
  function void post_randomize();
    `uvm_info(get_type_name(),$sformatf("Post-Randomize D %0d Rst %0d ",d,rst), UVM_LOW);
  endfunction
  
endclass

STEP2A: Create a sequencer class

The role of Sequencer class is quite simple. It acts as an intermediator between ‘Sequence’ and ‘Driver’. A Sequence is one which defines what type of packets and what functionality is to be checked. Sequencer generates these data transactions and send it to driver.

Sequencer is a dynamic component of Testbench as it will stay till the end of the simulation, so it comes under category of uvm_component. Sequencer class is created by extending by uvm_sequencer. Here we have created a type parameterized sequencer class. NOTE: Only active components of a Testbench can be type parameterized, meaning only Sequencer and Driver class can be type parameterized here.

Here since we don’t have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of sequencer class.

Now, as per the guidelines, for a uvm_component, we need to write a new function with 2 arguments: first of type string which class name and second parent which is set to null. Inside this new function, call super.new() with same arguments. The reason for calling super.new() is the same which is mentioned above in transaction class explanation.

Here we’re just adding the build phase but this part can be skipped because we’re not creating anything in build phase.

class dd_sequencer extends uvm_sequencer #(transaction);
  `uvm_component_utils(dd_sequencer)
  
  function new(string name="dd_sequencer", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
  endfunction
   
endclass

STEP2B: Create driver class

The role of driver class is to get packets of transaction class from “Sequencer” and send it to DUT (Design Under Test; here D Flip Flop). So driver first requests packets from sequencer and then sends it to DUT.

Driver is a static component of Testbench, means it will stay there until end of simulation. So it comes under category of uvm_component and we create driver class by extending it from uvm driver dedicated class i.e uvm_driver class. Here you will notice we have type parameterized it with our transaction class. The advantage of this is it will give a handle ‘req’ of type ‘transaction’. This important feature is always missed by everyone.

Here since we don’t have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of driver class.

As the driver needs access to the interface signals of DUT, we create a handle to the virtual interface ‘vif’ of type ‘dif’, which is our D flip-flop interface. This will be used to drive signals to the DUT. If you’re not clear with interface and why we’re using virtual interface, I suggest searching these terms in the blog to learn in detail.

Now, as per the guidelines, for a uvm_component, we need to write a new function with 2 arguments: first of type string, which class name, and second parent which is set to null. Inside this new function, call super.new() with same arguments. The reason for calling super.new() is same which is mentioned above in transaction class explanation.

Now, the most important part here is ‘uvm phases’. Because the driver class is a component, it goes through multiple phases. If you’re not clear with uvm_phases, search this term in the blogs to know more. We will be using build phase and run phase in driver class. Build phase to get the handle of virtual interface and run phase to drive the packets to virtual interface.

Remember, build_phase works from top to bottom, meaning the top classes will be built first, for example testbench environment is built first, then the agent, and then the driver. In the build phase, we always call super.build_phase with phase as an argument. Now, in short, super.build_phase is used to call the build phase of the parent class, and also it will call ‘apply_config_settings’, which in turn helps in overriding. This topic is discussed in detail in other blogs. Here you see after calling super.build_phase, we have an if loop in which we’re trying to get the handle of virtual interface “vif” from the top level into our “vif” handle. If we don’t get this handle from the top level, we call uvm_error to report an error. In the build phase only, we create an object of the transaction class.

The syntax of run_phase is straightforward. It also requires only 1 argument, like build_phase, i.e, uvm_phase. Inside run_phase, we create a forever loop because we don’t know how many packets will be requested and sent to the DUT. As you already know, the driver and sequencer have inbuilt TLM ports, which will be connected in the Agent class. The driver requests a packet from the sequencer via “seq_item_port” and calls an inbuilt function “get_next_item()”. Once the packet is received, the driver now has to drive the signals of the virtual interface. Since we know the transaction class has only 2 inputs: ‘d’ and ‘rst’, we drive the values of this packet on the virtual interface. Last, after driving, we have to call the ‘item_done()’ function to represent that the transaction is complete. The last line of code inside this forever loop is the delay of 2 clock cycles. You can select any delay value, but remember that delay is important otherwise, everything will happen at 0 seconds, and your simulation will be stuck.

class dd_driver extends uvm_driver #(transaction);

  //transaction req; //this is created by type parameterization
  
  `uvm_component_utils(dd_driver)
  
  //Create handle to virtual interface
  virtual dif vif;
  
  //New function
  function new(string name="dd_driver", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  //Build phase
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!(uvm_config_db#(virtual dif)::get(this,"","vif",vif)))
      `uvm_error(get_type_name(), "No vif handle in driver");
    req = transaction::type_id::create("req",this);
  endfunction
  
  //Run phase
  virtual task run_phase(uvm_phase phase);
    forever begin
      //Reqesting packet from Sequencer
      seq_item_port.get_next_item(req);

      //Drive packet to dut
      vif.d <= req.d;
      vif.rst <= req.rst;
      
      //Send item done to Sequencer
      seq_item_port.item_done();
      
      repeat(2) @(posedge vif.clk);
    end
  endtask
  
endclass

STEP 2C: Create a monitor class

A monitor is required to observe the response from the DUT. Monitor is a static component of Testbench, means it will stay there until end of simulation. So it comes under category of uvm_component and we create monitor class by extending it from uvm_monitor.

Here since we don’t have any variables for which we need automation, we only write uvm_component_utils to register our class with factory. The argument to this is name of monitor class.

Because monitor monitors the response from DUT, it needs to send this to other components of Testbench like scoreboard for comparison. So we declare a uvm_analysis port ‘send’. Later this port will be connected to Scoreboard at top level ‘env’ class. We also declare a handle to virtual interface because we need to monitor the response.

The new function is same and written here. Now the most important part here is ‘uvm_phases’. We’ve declared build_phase. First we need to get the handle of virtual interface from top level. If we don’t get this handle, error is raised. Now we create the object of ‘req’ and port ‘send’.

The run_phase is made virtual. The reason behind this is that if in the future you need to create a child class of this monitor class, the child class might need to override this definition of run_phase. In the run phase, the most important first we’re waiting for delay of 2 clock cycles. NOTE: This delay should be the same as mentioned in the driver class. Now we read the input and output signals of D flip-flop design via virtual interface and then write it to the ‘req’ object. After that, we send this ‘req’ packet via the port ‘send’.

class dd_monitor extends uvm_monitor;
  `uvm_component_utils(dd_monitor)
  
  //Analysis port for scoreboard
  uvm_analysis_port#(transaction) send;
  
  transaction req;
  
  //Virtual interface
  virtual dif vif;
  
  function new(string name="dd_monitor", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!(uvm_config_db#(virtual dif)::get(this,"","vif",vif)))
      `uvm_error(get_type_name(), "No vif handle in driver");
    req = transaction::type_id::create("req",this);
    send = new("send",this);
  endfunction
  
  virtual task run_phase(uvm_phase phase);
    forever begin
      repeat(2 )@(posedge vif.clk);
      `uvm_info(get_type_name,$sformatf("Monitor sampling packages"), UVM_LOW)
      req.d = vif.d;
      req.q = vif.q;
      req.rst = vif.rst;
      send.write(req);
    end
  endtask
  
endclass

STEP 3: Create an agent class

Now the role of agent class is to encapsulate Monitor, Driver and Sequencer. It also comes under the category of uvm_component and we create agent class by extending it from uvm_agent.

Now since we’ve extended our class from uvm_agent, we get an inbuilt switch called ‘is_active’ which will tell if the Agent is active or Passive. We’ll add this switch in automation macro ‘`uvm_field_enum’ and register our class with factory.

Next steps are creating a handle of driver, sequencer, and monitor, and writing a new() function for uvm_component. Now in build_phase, we first check if ‘is_active’ is active or passive. This is set from the top level. NOTE we don’t need explicit ‘get’ here to receive the value of ‘is_active’. If this is an active agent, only then do we need to create objects of Driver and Sequencer. Monitor is always created no matter what.

In connect_phase, same condition is checked. If the agent is active, only then connect the TLM ports of driver and sequencer using the below syntax. Note we have also called super.connect() which can be skipped here.

class dd_agent extends uvm_agent;

  //uvm_active_passive_enum is_active; //created by uvm_agent
  
  `uvm_component_utils_begin(dd_agent)
  `uvm_field_enum(uvm_active_passive_enumj, is_active, UVM_ALL_ON)
  `uvm_component_utils_end  
 
  //Create handles
  dd_driver d;
  dd_monitor m;
  dd_sequencer s;
  
  function new(string name="dd_agent", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(is_active == UVM_ACTIVE)
    begin
      d = dd_driver::type_id::create("d",this);
      s = dd_sequencer::type_id::create("s",this);
    end
      m = dd_monitor::type_id::create("m",this);
  endfunction
    
  //Connect Driver and Sequencer
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if(is_active == UVM_ACTIVE)
      d.seq_item_port.connect(s.seq_item_export);
  endfunction
  
endclass

STEP 4: Create scoreboard class

The main purpose of scoreboard for this design is to compare input and output values as per D flip flop table.

First, we register our scoreboard class with the factory and then declare a uvm_import port ‘recv’ to receive packets from the Monitor. This ‘recv’ is connected with the Monitor port in the top env class.
Then, the new function is written for the UVM component syntax.

In the build phase, we’ve to create the object of the ‘recv’ port. Then the most important part here is the ‘write’ function. The importance of this is because in Monitor, we send the packet via port using this syntax “port.write”. So this ‘write’ function should be present in the class that wants to receive this packet from the Monitor. Inside this write function, we’ve coded the comparison logic. If reset is 0, then the output ‘q’ should be zero. Else if reset is 1, output ‘q’ should be the same as input ‘d’.

class dd_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(dd_scoreboard)
  
  //Analysis import to get packets from Monitor
  uvm_analysis_imp#(transaction, dd_scoreboard) recv;
  
  function new(string name="dd_scoreboard", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    recv = new("recv",this);
  endfunction
  
  //Write function of import
  virtual function void write(transaction req);
    if(req.rst == 1)
    begin
      if(req.q == 0)
        `uvm_info(get_type_name,$sformatf("scoreboard rst0 Pass D %0d Q %0d Rst %0d",req.d,req.q,req.rst), UVM_LOW)
      else
        `uvm_error(get_type_name,$sformatf("scoreboard rst0 Fail D %0d Q %0d Rst %0d",req.d,req.q,req.rst))
    end
    else
    begin
      if(req.q == req.d)
        `uvm_info(get_type_name,$sformatf("scoreboard  Pass D %0d Q %0d Rst %0d",req.d,req.q,req.rst), UVM_LOW)
      else
        `uvm_error(get_type_name,$sformatf("scoreboard Fail D %0d Q %0d Rst %0d",req.d,req.q,req.rst))
    end  
  endfunction
  
endclass

STEP 5: Create an environment class

Now we’ll be creating an env class to encapsulate Agent and Scoreboard, and connect the scoreboard with the monitor.

We follow the syntax of uvm_component here and extend the class from uvm_env. Register this class with the factory and have a new function for the component.

In this build phase, create object of Agent and Scoreboard. In the connect phase, using the below syntax, connect the monitor TLM port ‘send’ with Scoreboard’s receiving port ‘recv’.

class dd_env extends uvm_env;
  `uvm_component_utils(dd_env)
  
  dd_agent a;
  dd_scoreboard sc;
  
  function new(string name="dd_env", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    a = dd_agent::type_id::create("a",this);
    sc = dd_scoreboard::type_id::create("sc",this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    a.m.send.connect(sc.recv);
  endfunction
    
endclass

STEP 6: Create testbench class

This class is optional here. You can skip this and directly invoke env class in your testcase. The purpose of this class comes when you have a complex design, then more than 1 env classes are there and you’ve to encapsulate all those classes under one class which is this Testbench class. The syntax of class is simple, we just create an object of env class here.

class dd_tb extends uvm_env;
  `uvm_component_utils(dd_tb)

  dd_env env;
  
  function new(string name="dd_tb", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = dd_env::type_id::create("env",this);
  endfunction
    
endclass

STEP 7A: Create a sequence

Now sequence is something which will actually verify the DUT. Sequence tells what type of packets you want to create which will be sent to DUT later on. Now this sequence below creates 10 packets of type ‘transaction’ and reset is 0 in all packets.

Now remember, sequence is a uvm_object, so we create this class by extending it from uvm_sequence and following the uvm_object guidelines when registering to the factory and for new function. To understand the code better, you need to understand the topic of “Raising objections in UVM”.

class seq1 extends uvm_sequence #(transaction);
  
  `uvm_object_utils(seq1)
  
  function new(string name="seq1");
    super.new(name);
  endfunction
   
  virtual task pre_body();
    begin
    uvm_phase ph = get_starting_phase();
    if(ph != null)
      begin
      starting_phase.raise_objection(this, get_type_name());
        $display("Pre body raised");
      end
      $display("Entering pre body");
    end
  endtask
  
  virtual task body();
    req = transaction::type_id::create("req");
    repeat(10)
      begin
       `uvm_info(get_type_name,$sformatf("Start Sequence"), UVM_LOW);

       assert(req.randomize());
       //Reset is 0
       req.rst = 0;
       `uvm_send(req);
            
       `uvm_info(get_type_name,$sformatf("Sent D %0d RST %0d",req.d,req.rst), UVM_LOW);
      end
   `uvm_info(get_type_name,$sformatf("Seq1 Ends-------------------------------------"), UVM_LOW);
  endtask
  
  virtual task post_body();
    if(starting_phase != null)
      starting_phase.drop_objection(this, get_type_name());
  endtask
  
endclass

STEP7 B: Create a testcase

A sequence cannot run on its own. A testcase commands which sequence will run on which sequencer. For us, ‘seq1’ will be made to run on the sequencer. Now test is a uvm_component, so we will follow the uvm_component syntax in registering with the factory and the new function.

Here, in build_phase, we first need to set ‘is_active’ as UVM_ACTIVE before calling super.build_phase(). Now, to explain it better, when the test is executed, the testbench and Agent are still not made, that’s why in the syntax of ‘uvm_config_int::set’, ‘is_active’ is written in double quotes. At this point in time, there’s no switch like ‘is_active’. After this, when we call super.build(), this then starts building the hierarchy downwards, like from testcases -> testbench -> env -> agent, and so on. So super.build() in Agent class will actually call an inbuilt function to set ‘is_active’. Then we create an object of testbench and seq1.

Now in run task, we first raise objections (Read about Objections in the blogs) and we run the seq1 ‘ss1’ on sequencer via syntax (sequence.start(sequencer)). And after a delay of 10ns, we drop the objections.

class test1 extends uvm_component;
  `uvm_component_utils(test1)

  dd_tb tb;
  seq1 ss1;
   
  function new(string name="test1", uvm_component parent = null);
    super.new(name,parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    //Set is_active
    uvm_config_int::set(this,"*","is_active", UVM_ACTIVE);
    super.build_phase(phase);
    tb = dd_tb::type_id::create("tb",this);
    ss1 = seq1::type_id::create("ss1",this);   
  endfunction
   
  virtual task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    
    `uvm_info(get_type_name,$sformatf("SEQ1 starts"), UVM_LOW)
    ss1.start(tb.env.a.s);
    #10
       
    phase.drop_objection(this);
  endtask
    
endclass

STEP 8: Create the top module

The top module actually binds design with virtual interface, sets the clock, sets the virtual interface handle to below hierarchies, and tells which testcase to run.

Here first we create a handle to dif (vif) which is our design interface. And then we connect design ‘dff’ with virtual interface signals ‘vif’.

After, we initially set the clock of ‘vif’ to 0 and then in an ‘always’ procedural statement, we toggle the clock every 10ns.

Inside another ‘initial’ procedural statement, we first set the virtual interface handle. Now in the below syntax ‘uv,_config_db’, we set ‘vif’ at ‘*’ meaning any component in below hierarchy can get the vif via adding’ get’ statement. To understand this better, go to the blogs. After that, in ‘run_test’ we tell which testcase to execute.

The last procedural statement is added to dump the VCD files in the EDA playground.

module tb_top;
 
  bit clk;
  
  //Interface
  dif vif();
  //Design instance
  dff design1(.clk(vif.clk), .din(vif.d), .rst(vif.rst), .qout(vif.q));
  
  //Initialize clock to zero 
  initial begin
    clk = 0;
  end
  
  //Generate 100Mhz clock 
  always begin
    #10ns clk = ~clk;
  end
  
  //Connect clocks
  assign vif.clk = clk;
  
  //Run testcase
  initial begin
    uvm_config_db#(virtual dif)::set(null,"*","vif",vif);
    run_test("test1");
  end
  
  //Dump waveforms in EDA playground
  initial begin
    $dumpfile("d.vcd");
    $dumpvars;
  end
  
endmodule

HOW TO RUN THIS PROJECT IN EDA PLAYGROUND?

To run this project in the EDA playground, you can visit the link below and try running it.

EDA playground project link: https://www.edaplayground.com/x/H8ai

Thank you for reading till the end, and I hope this project is clear to all my readers. Feel free to ask doubts via the contact form, or I would recommend asking via comments.


Please do leave your feedback and subscribe for more such posts.



One response

  1. Solving the actual problem. No website posts UVM testbench examples with these many details. It will literally make life easy for Verification learners.

Leave a Reply

Your email address will not be published. Required fields are marked *