Class definitions can become very long with a lot of lines between class and endclass. This makes it difficult to understand what all functions and variables exist within the class because each function and task occupy quite a lot of lines.

Using extern qualifier in method declaration indicates that the implementation is done outside the body of this class.

Example

  
  
class ABC;
  
  // Let this function be declared here and defined later
  // by "extern" qualifier
  extern function void display();
    
endclass
    
// Outside the class body, we have the implementation of the
// function declared as "extern"
function void ABC::display();
  
   $display ("Hello world");
  
endfunction

module tb;
  
  // Lets simply create a class object and call the display method
  initial begin
    ABC abc = new();
    abc.display();
  end
endmodule

  
Simulation Log
ncsim> run
Hello world
ncsim: *W,RNQUIE: Simulation is complete.

A UVM transaction class typically defines all the input and output control signals that can be randomized and driven to the DUT.

Steps to create a UVM transaction object

1. Create custom class inherited from uvm_sequence_item, register with factory and call new
  
  
// my_object is user-given name for this class that has been derived from "uvm_sequence_item"
class my_object extends uvm_sequence_item;
	
	// This is standard code for all components
	function new (string name = "my_object", uvm_component parent = null);
		super.new (name, parent);
	endfunction
	
	// Code for rest of the steps come here
endclass

  

2. Declare variables related to this transaction class
  
  
	// Declare variables and make the ones that has to be randomized as "rand"
	rand bit [15:0] addr;
	rand bit [31:0] data;
	rand bit [2:0]  burst;

  

3. Register the class with factory and apply UVM field macros if required
  
  
	`uvm_object_utils_begin (my_object)
		// Apply the given object macros to the corresponding variables
		`uvm_field_int (addr,  UVM_ALL_ON | UVM_NO_COMPARE)
		`uvm_field_int (data,  UVM_ALL_ON)
		`uvm_field_int (burst, UVM_ALL_ON)
	`uvm_object_utils_end

  

4. Add constraints as required
  
  
	constraint c_addr { addr <= 16'hBFFF; }
	constraint c_burst { burst inside {[0:4]}; }

  
Define convert2string function
  
  
	function string convert2string();
		return $sformatf("addr=0x%0h data=0x%0h burst=0x%0h", addr, data, burst);
	endfunction

  

uvm_driver is a child of uvm_component that has a TLM port to communicate with the sequencer. The driver is a parameterized class with the type of request and response sequence items. This allows the driver to send back a different sequence_item type back to the sequencer as the response. However, most drivers use a response object of the same type as the request sequence item.

The uvm_driver gets request sequence items (REQ) from the sequencer FIFO using a handshake mechanism and optionally returns a response sequence item (RSP) back to the sequencer response FIFO. There are primarily two ways for the driver to get a sequence item from the sequencer. In this article, we'll look at an example that uses the first style.

driver-sequencer-get-next-item-flow

Using get_next_item method in a driver

In this case, the driver requests for a sequence item from the sequencer using the get_next_item method through the seq_item_port TLM handle. Since the implementation of this port is defined in the sequencer, the function call makes the sequencer to pop an item from its internal FIFO and provide it to the driver via the argument provided in get_next_item method.

  
  
class my_driver extends uvm_driver #(my_data);
	`uvm_component_utils (my_driver)
	
   virtual task run_phase(uvm_phase phase);
      super.run_phase(phase);
      
      // 1. This task will get an item from the sequencer using get_next_item()
      `uvm_info ("DRIVER", $sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
      seq_item_port.get_next_item(req);
      
      // 2. For simplicity, lets just assume the driver drives the received packet
      // during this time and consumes 20ns to complete driving the transaction
      `uvm_info ("DRIVER", $sformatf ("Start driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
      #20;
      
      // 3. After driver has finished the transaction, it has to let the sequencer know
      // by calling item_done()
      `uvm_info ("DRIVER", $sformatf ("Finish driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
      seq_item_port.item_done();
   endtask

  

Once driver gets the next item, it can drive the data in the received sequence item to the DUT via a virtual interface handle. After the driver has finished driving the item, it has to let the sequencer know that the process has finished using item_done method.

How does the sequencer get these sequence items ?

A uvm_sequence is started on a sequencer which pushes the sequence item onto the sequencer's FIFO.

  
  
class my_sequence extends uvm_sequence;
	`uvm_object_utils (my_sequence)
	
	
  virtual task body();
  	// 1. Create an item the connected sequencer can accept
    my_data tx = my_data::type_id::create("tx");
    `uvm_info ("SEQ", $sformatf("About to call start_item"), UVM_MEDIUM)
    
    // 2. Call the start_item() task which will send this object to the driver
    start_item(tx);
    `uvm_info ("SEQ", $sformatf("start_item() fn call done"), UVM_MEDIUM)
    
    // 3. Because the class handle passed to the driver points to the same object, we 
    // can do late randomization
    tx.randomize();
    `uvm_info ("SEQ", $sformatf("tx randomized with addr=0x%0h data=0x%0h", tx.addr, tx.data), UVM_MEDIUM)
    
    // 4. Call finish_item method so that the sequence waits until the driver lets the 
    // sequencer know that this item has finished
    finish_item(tx);
    `uvm_info ("SEQ", $sformatf("finish_item() fn call done"), UVM_MEDIUM)
  endtask
endclass

  

Example

To illustrate how the get and put method calls between driver and sequencer work, let us build a simple testbench structure like the one shown below.

uvm-driver-sequencer-get-put-testbench

Define a sequence item

To keep things simple, let us define a transaction object class that will become the sequence item used for sequencer-driver communication. Assume that this sequence item contains two variables called addr and data.

  
  
// Note that this class is dervide from "uvm_sequence_item"
class my_data extends uvm_sequence_item;
  rand bit [7:0]   data;
  rand bit [7:0]   addr;

	// Rest of the class contents come here ...
endclass

  

Define the driver

The uvm_driver is parameterized to accept a class object of the type my_data and the driver is expected to unpack this class object and drive the signals appropriately to the DUT via the interface.

  
  
class my_driver extends uvm_driver #(my_data);
   `uvm_component_utils (my_driver)
   // Other parts of the driver code if they exist

   virtual task run_phase(uvm_phase phase);
      super.run_phase(phase);
      
      // 1. Get the next available item from the sequencer. If none exists, then wait until
      // next item is available -> this is blocking in nature
      `uvm_info ("DRIVER", $sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
      seq_item_port.get_next_item(req);
      
      // 2. Let us assume that the driver actively does the pin wiggling of the DUT during this time and 
      // consider it takes 20ns
      `uvm_info ("DRIVER", $sformatf ("Start driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
      #20;
      
      // 3. After the driver has driven all data to the DUT, it should let the sequencer know that it finished 
      // driving the transaction by calling "item_done". Optionally the response packet can be passed along with
      // the item_done method call and it will be placed in the sequencer's response FIFO
      `uvm_info ("DRIVER", $sformatf ("Finish driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
      seq_item_port.item_done();
   endtask
endclass

  

Define the sequence

A sequence item is always started using the start_item and finish_item methods.

  
  
class my_sequence extends uvm_sequence #(my_data);
  	// Rest of the sequence code

  	virtual task body();
  		// 1. Create a sequence item of the given type
    	my_data tx = my_data::type_id::create("tx");
    	`uvm_info ("SEQ", $sformatf("About to call start_item"), UVM_MEDIUM)
    	
    	// 2. Start the item on the sequencer which will send this to the driver
    	start_item(tx);
    	`uvm_info ("SEQ", $sformatf("start_item() fn call done"), UVM_MEDIUM)
    	
    	// 3. Do some late randomization to create a different content in this transaction object
    	tx.randomize();
    	`uvm_info ("SEQ", $sformatf("tx randomized with addr=0x%0h data=0x%0h", tx.addr, tx.data), UVM_MEDIUM)
    	
    	// 4. Call finish_item to let driver continue driving the transaction object or sequence item
    	finish_item(tx);
    	`uvm_info ("SEQ", $sformatf("finish_item() fn call done"), UVM_MEDIUM)
  	endtask
endclass

  

Define the test class

To keep things simple, let us directly create instances of the driver and sequencer inside the test class.

Driver and Sequencer should be instantiated inside an agent. An agent is instantiated in an environment and the the environment in turn should be created in the test.

  
  
class base_test extends uvm_test;
	// Rest of the test code is here

	// The sequencer is parameterized to accept objects of type "my_data" only
  	my_driver                	m_drv0;
  	uvm_sequencer #(my_data) 	m_seqr0;
  	my_sequence   				m_seq;

	// Build the sequencer and driver components
  	virtual function void build_phase(uvm_phase phase);
     	super.build_phase(phase);
     	m_drv0 = my_driver::type_id::create ("m_drv0", this);
     	m_seqr0 = uvm_sequencer#(my_data)::type_id::create ("m_seqr0", this);
  	endfunction
  
   	// Connect the sequencer "export" to the driver's "port"
   	virtual function void connect_phase (uvm_phase phase);
     	super.connect_phase (phase);
     	m_drv0.seq_item_port.connect (m_seqr0.seq_item_export);
   	endfunction

	// Start the sequence on the given sequencer
  	virtual task run_phase(uvm_phase phase);
    	m_seq = my_sequence::type_id::create("m_seq");
    	phase.raise_objection(this);
    	m_seq.start(m_seqr0);
    	phase.drop_objection(this);
  	endtask
endclass

  
Simulation Log
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO testbench.sv(33) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Waiting for data from sequencer
UVM_INFO testbench.sv(71) @ 0: [email protected]@m_seq [SEQ] About to call start_item
UVM_INFO testbench.sv(73) @ 0: [email protected]@m_seq [SEQ] start_item() fn call done
UVM_INFO testbench.sv(75) @ 0: [email protected]@m_seq [SEQ] tx randomized with addr=0x8f data=0x1d
UVM_INFO testbench.sv(35) @ 0: uvm_test_top.m_top_env.m_drv0 [DRIVER] Start driving tx addr=0x8f data=0x1d
UVM_INFO testbench.sv(37) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] Finish driving tx addr=0x8f data=0x1d
UVM_INFO testbench.sv(77) @ 20: [email protected]@m_seq [SEQ] finish_item() fn call done
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 20: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 20: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

Some of the main built-in primitives were discussed in the previous article and it would be good to see some practical examples of using simple and, nor and not gates.

Note that in order to write the Verilog code using gates, it is necessary for you to know how to connect the elements. This is very different from a behavioral description in which case the selection and connection of elements is left upto the synthesis tools.

Example #1: 2x1 Multiplexer

Output of module has to be of type wire in order to connect with the output port of a primitive.

  
  
module mux_2x1 ( input a, b, sel,
				 output out);
	wire sel_n;
	wire out_0;
	
	not (sel_n, sel);
	
	and (out_0, a, sel);
	and (out_1, b, sel_n);
	
	or (out, out_0, out_1);
endmodule

  
  
  
module tb;
  reg a, b, sel;
  wire out;
  integer i;
  
  mux_2x1 u0 ( 	.a(a), .b(b), .sel(sel), .out(out));
  
  initial begin
    {a, b, sel} <= 0;
    
    $monitor ("T=%0t a=%0b b=%0b sel=%0b out=%0b", $time, a, b, sel, out);
    
	for (int i = 0; i < 10; i = i+1) begin
    	#1 	a <= $random;
      		b <= $random;
			sel <= $random;
    end
  end
endmodule

  
Simulation Log
ncsim> run
T=0 a=0 b=0 sel=0 out=0
T=1 a=0 b=1 sel=1 out=0
T=2 a=1 b=1 sel=1 out=1
T=3 a=1 b=0 sel=1 out=1
T=6 a=0 b=1 sel=0 out=1
T=7 a=1 b=1 sel=0 out=1
T=8 a=1 b=0 sel=0 out=0
T=9 a=0 b=1 sel=0 out=1
T=10 a=1 b=1 sel=1 out=1
ncsim: *W,RNQUIE: Simulation is complete.

Full Adder

  
  
module fa (	input a, b, cin,
			output sum, cout);
	
	wire s1, net1, net2;
	
	xor (s1, a, b);
	and (net1, a, b);
	
	xor (sum, s1, cin);
	and (net2, s1, cin);
	
	xor (cout, net1, net2);
	
endmodule

  
  
  
module tb;
  reg a, b, cin;
  wire sum, cout;
  integer i;
  
  fa u0 ( .a(a), .b(b), .cin(cin),
         .sum(sum), .cout(cout));
  
  initial begin
    {a, b, cin} <= 0;
    
    $monitor ("T=%0t a=%0b b=%0b cin=%0b cout=%0b sum=%0b",
              $time, a, b, cin, cout, sum);
    
    for (i = 0; i < 10; i = i+1) begin
      #1 	a <= $random;
      		b <= $random;
      		cin <= $random;
    end
  end
endmodule

  
Simulation Log
ncsim> run
T=0 a=0 b=0 cin=0 cout=0 sum=0
T=1 a=0 b=1 cin=1 cout=1 sum=0
T=2 a=1 b=1 cin=1 cout=1 sum=1
T=3 a=1 b=0 cin=1 cout=1 sum=0
T=6 a=0 b=1 cin=0 cout=0 sum=1
T=7 a=1 b=1 cin=0 cout=1 sum=0
T=8 a=1 b=0 cin=0 cout=0 sum=1
T=9 a=0 b=1 cin=0 cout=0 sum=1
T=10 a=1 b=1 cin=1 cout=1 sum=1
ncsim: *W,RNQUIE: Simulation is complete.

2x4 Decoder

  
  
module tb;
  reg x, y, en;
  wire a, b, c, d;
  integer i;
  
  dec_2x4 u0 ( .x(x), .y(y), .en(en),
           		.a(a), .b(b), .c(c), .d(d));
  
  initial begin
    {x, y, en} <= 0;
    $monitor ("T=%0t x=%0b y=%0b en=%0b a=%0b b=%0b c=%0b d=%0b",
              $time, x, y, en, a, b, c, d);
    
	en <= 1;
    for (i = 0; i < 10; i = i+1) begin
      #1 	x <= $random;
      		y <= $random;
    end
  end
endmodule

  
Simulation Log
ncsim> run
T=0 x=0 y=0 en=1 a=0 b=0 c=0 d=1
T=1 x=0 y=1 en=1 a=0 b=0 c=1 d=0
T=2 x=1 y=1 en=1 a=1 b=0 c=0 d=0
T=4 x=1 y=0 en=1 a=0 b=1 c=0 d=0
T=5 x=1 y=1 en=1 a=1 b=0 c=0 d=0
T=6 x=0 y=1 en=1 a=0 b=0 c=1 d=0
T=7 x=1 y=0 en=1 a=0 b=1 c=0 d=0
T=10 x=1 y=1 en=1 a=1 b=0 c=0 d=0
ncsim: *W,RNQUIE: Simulation is complete.

4x2 Encoder

  
  
module enc_4x2 ( input a, b, c, d,
                output x, y);
  
  or (x, b, d);
  or (y, c, d);
  
endmodule

  
  
  
module tb;
  reg a, b, c, d;
  wire x, y;
  integer i;
  
  enc_4x2 u0 ( .a(a), .b(b), .c(c), .d(d), .x(x), .y(y));
  
  initial begin
    {a, b, c, d} <= 0;
    
    $monitor("T=%0t a=%0b b=%0b c=%0b d=%0b x=%0b y=%0b", 
             $time, a, b, c, d, x, y);
    
    for (i = 0; i <= 16; i = i+1) begin
      #1 {a, b, c, d} <= i;
    end
  end
  
endmodule

  
Simulation Log
ncsim> run
T=0 a=0 b=0 c=0 d=0 x=0 y=0
T=2 a=0 b=0 c=0 d=1 x=1 y=1
T=3 a=0 b=0 c=1 d=0 x=0 y=1
T=4 a=0 b=0 c=1 d=1 x=1 y=1
T=5 a=0 b=1 c=0 d=0 x=1 y=0
T=6 a=0 b=1 c=0 d=1 x=1 y=1
T=7 a=0 b=1 c=1 d=0 x=1 y=1
T=8 a=0 b=1 c=1 d=1 x=1 y=1
T=9 a=1 b=0 c=0 d=0 x=0 y=0
T=10 a=1 b=0 c=0 d=1 x=1 y=1
T=11 a=1 b=0 c=1 d=0 x=0 y=1
T=12 a=1 b=0 c=1 d=1 x=1 y=1
T=13 a=1 b=1 c=0 d=0 x=1 y=0
T=14 a=1 b=1 c=0 d=1 x=1 y=1
T=15 a=1 b=1 c=1 d=0 x=1 y=1
T=16 a=1 b=1 c=1 d=1 x=1 y=1
T=17 a=0 b=0 c=0 d=0 x=0 y=0
ncsim: *W,RNQUIE: Simulation is complete.

There are two types of arrays in SystemVerilog - packed and unpacked arrays.

A packed array is used to refer to dimensions declared before the variable name.

  
  
	bit [3:0] 	data; 			// Packed array or vector
	logic 		queue [9:0]; 	// Unpacked array

  

A packed array is guaranteed to be represented as a contiguous set of bits. They can be made of only the single bit data types like bit, logic, and other recursively packed arrays.

Single Dimensional Packed Arrays

A one-dimensional packed array is also called as a vector.

  
  
module tb;
	bit [7:0] 	m_data; 	// A vector or 1D packed array
	
	initial begin
		// 1. Assign a value to the vector
		m_data = 8'hA2; 
		
		// 2. Iterate through each bit of the vector and print value
		for (int i = 0; i < $size(m_data); i++) begin
			$display ("m_data[%0d] = %b", i, m_data[i]);
		end
	end
endmodule

  
Simulation Log
ncsim> run
m_data[0] = 0
m_data[1] = 1
m_data[2] = 0
m_data[3] = 0
m_data[4] = 0
m_data[5] = 1
m_data[6] = 0
m_data[7] = 1
ncsim: *W,RNQUIE: Simulation is complete.

Multidimensional Packed Arrays

A multidimensional packed array is still a set of contiguous bits but are also segmented into smaller groups.

Example #1

The code shown below declares a 2D packed array that occupies 32-bits or 4 bytes and iterates through the segments and prints its value.

  
  
module tb;
  bit [3:0][7:0] 	m_data; 	// A MDA, 4 bytes
	
	initial begin
		// 1. Assign a value to the MDA
		m_data = 32'hface_cafe;
      
      $display ("m_data = 0x%0h", m_data);
		
		// 2. Iterate through each segment of the MDA and print value
      for (int i = 0; i < $size(m_data); i++) begin
        $display ("m_data[%0d] = %b (0x%0h)", i, m_data[i], m_data[i]);
		end
	end
endmodule

  
Simulation Log
ncsim> run
m_data = 0xfacecafe
m_data[0] = 11111110 (0xfe)
m_data[1] = 11001010 (0xca)
m_data[2] = 11001110 (0xce)
m_data[3] = 11111010 (0xfa)
ncsim: *W,RNQUIE: Simulation is complete.

Example #2

Let us see a 3D packed array now.

  
  
module tb;
  bit [2:0][3:0][7:0] 	m_data; 	// An MDA, 12 bytes
	
	initial begin
		// 1. Assign a value to the MDA
      m_data[0] = 32'hface_cafe;
      m_data[1] = 32'h1234_5678;
      m_data[2] = 32'hc0de_fade;
      
      // m_data gets a packed value
      $display ("m_data = 0x%0h", m_data);
		
		// 2. Iterate through each segment of the MDA and print value
      foreach (m_data[i]) begin
        $display ("m_data[%0d] = 0x%0h", i, m_data[i]);
        foreach (m_data[i][j]) begin
          $display ("m_data[%0d][%0d] = 0x%0h", i, j, m_data[i][j]);
        end
      end
	end
endmodule

  
Simulation Log
ncsim> run
m_data = 0xc0defade12345678facecafe
m_data[2] = 0xc0defade
m_data[2][3] = 0xc0
m_data[2][2] = 0xde
m_data[2][1] = 0xfa
m_data[2][0] = 0xde
m_data[1] = 0x12345678
m_data[1][3] = 0x12
m_data[1][2] = 0x34
m_data[1][1] = 0x56
m_data[1][0] = 0x78
m_data[0] = 0xfacecafe
m_data[0][3] = 0xfa
m_data[0][2] = 0xce
m_data[0][1] = 0xca
m_data[0][0] = 0xfe
ncsim: *W,RNQUIE: Simulation is complete.