UVM TLM ports and exports are also used to send transaction objects cross different levels of testbench hierarchy.

Ports shall be used to initiate and forward packets to the top layer of the hierarchy. Exports shall be used to accept and forward packets from the top layer to destination. Implementation ports shall be used to define the put method at the target. Shown below are a few examples that use ports, exports and implementation for components at different hierarchy levels.

Port to Port to Export to Imp

subCompA is a subcomponent within componentA that is trying to send transactions to another subcomponent called subCompB in componentB. To maintain flexibility and portability of code, it is recommended to allow subCompA to send data to componentA which should then forward them to the top layer of the hierarchy. componentB shall accept the transaction and forward it to subCompB.

uvm-tlm-put-port-port-export-imp

A class called Packet is defined below to act as the data item that will be transferred from one component to another. This class object will have two random variables that can be randomized before sending.

  
  
// Create a class data object that can be sent from one 
// component to another
class Packet extends uvm_object;
  rand bit[7:0] addr;
  rand bit[7:0] data;
  
  `uvm_object_utils_begin(Packet)
  	`uvm_field_int(addr, UVM_ALL_ON)
  	`uvm_field_int(data, UVM_ALL_ON)
  `uvm_object_utils_end
  
  function new(string name = "Packet");
    super.new(name);
  endfunction
endclass

  

Subcomponent A

  
  
class subCompA extends uvm_component;
  `uvm_component_utils (subCompA)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  uvm_blocking_put_port #(Packet) m_put_port;
  int m_num_tx=2;
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_put_port = new ("m_put_port", this);
  endfunction
  
  // Create a packet, randomize it and send it through the port
  // Note that put() is a method defined by the receiving component
  // Repeat these steps N times to send N packets
   virtual task run_phase (uvm_phase phase);
     phase.raise_objection(this);
     repeat (m_num_tx) begin
         Packet pkt = Packet::type_id::create ("pkt");
         assert(pkt.randomize ()); 
       
       	 // Print the packet to be displayed in log
       `uvm_info ("SUBCOMPA", "Packet sent to subCompB", UVM_LOW)
         pkt.print (uvm_default_line_printer);
       
         // Call the TLM put() method of put_port class and pass packet as argument
         m_put_port.put (pkt);
      end
      phase.drop_objection(this);
   endtask
endclass

  

Component A

  
  
class componentA extends uvm_component;
   `uvm_component_utils (componentA)
   function new (string name = "componentA", uvm_component parent= null);
      super.new (name, parent);
   endfunction

    subCompA m_subcomp_A;
    uvm_blocking_put_port #(Packet) m_put_port;
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_subcomp_A = subCompA::type_id::create("m_subcomp_A", this);
    m_put_port = new ("m_put_port", this);
  endfunction
  
  // Connection with subCompA
  virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    m_subcomp_A.m_put_port.connect(this.m_put_port);
  endfunction
endclass

  

Component B

  
  
class componentB extends uvm_component;
   `uvm_component_utils (componentB)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
	subCompB m_subcomp_B;
   uvm_blocking_put_export#(Packet) m_put_export;
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_subcomp_B = subCompB::type_id::create("m_subcomp_B", this);
    m_put_export = new("m_put_export", this);
  endfunction
  
  // Connection with subCompB
  virtual function void connect_phase(uvm_phase phase);
    m_put_export.connect(m_subcomp_B.m_put_imp);
  endfunction
endclass

  

Subcomponent B

  
  
class subCompB extends uvm_component;
  `uvm_component_utils (subCompB)
  function new (string name = "subCompB", uvm_component parent = null);
      super.new (name, parent);
   endfunction

   // Mention type of transaction, and type of class that implements the put ()
  uvm_blocking_put_imp #(Packet, subCompB) m_put_imp;
 
   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
     m_put_imp = new ("m_put_imp", this);
   endfunction
 
    // Implementation of the 'put()' method in this case simply prints it.
  	virtual task put (Packet pkt);            
      `uvm_info ("SUBCOMPB", "Packet received from subCompA", UVM_LOW)
      pkt.print(uvm_default_line_printer);
   endtask
endclass

  
  
  
class my_test extends uvm_test;
  `uvm_component_utils (my_test)
 
   componentA compA;
   componentB compB;
 
  function new (string name = "my_test", uvm_component parent = null);
      super.new (name, parent);
   endfunction
 
   // Create objects of both components, set number of transfers
   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      compA = componentA::type_id::create ("compA", this);
      compB = componentB::type_id::create ("compB", this);
   endfunction
 
   // Connection between componentA and componentB is done here
   virtual function void connect_phase (uvm_phase phase);
     compA.m_put_port.connect (compB.m_put_export);  
   endfunction
  
   virtual function void end_of_elaboration_phase(uvm_phase phase);
    super.end_of_elaboration_phase(phase);
    uvm_top.print_topology();
  endfunction
endclass

  
Simulation Log

UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------
Name              Type                     Size  Value
------------------------------------------------------
uvm_test_top      my_test                  -     @1842
  compA           componentA               -     @1911
    m_put_port    uvm_blocking_put_port    -     @2008
    m_subcomp_A   subCompA                 -     @1974
      m_put_port  uvm_blocking_put_port    -     @2047
  compB           componentB               -     @1942
    m_put_export  uvm_blocking_put_export  -     @2115
    m_subcomp_B   subCompB                 -     @2081
      m_put_imp   uvm_blocking_put_imp     -     @2152
------------------------------------------------------

UVM_INFO testbench.sv(71) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(151) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(71) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO testbench.sv(151) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: 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) @ 0: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

Port to Port to Imp

In this example componentA forwards the packet from subCompA to the destination componentB. The only difference is that componentB is the target and hence should define the implementation port instead of an export.

uvm-tlm-put-port-port-imp
  
  
// componentB shall implement "put" and there's no subCompB
class componentB extends uvm_component;
   `uvm_component_utils (componentB)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  uvm_blocking_put_imp#(Packet, componentB) m_put_imp;
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_put_imp = new("m_put_imp", this);
  endfunction
  
      // Implementation of the 'put()' method in this case simply prints it.
    virtual task put (Packet pkt);            
      `uvm_info ("COMPB", "Packet received from subCompA", UVM_LOW)
      pkt.print(uvm_default_line_printer);
   endtask
endclass

  
Simulation Log

UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
----------------------------------------------------
Name              Type                   Size  Value
----------------------------------------------------
uvm_test_top      my_test                -     @1839
  compA           componentA             -     @1908
    m_put_port    uvm_blocking_put_port  -     @2005
    m_subcomp_A   subCompA               -     @1971
      m_put_port  uvm_blocking_put_port  -     @2044
  compB           componentB             -     @1939
    m_put_imp     uvm_blocking_put_imp   -     @2081
----------------------------------------------------

UVM_INFO testbench.sv(70) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to CompB
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(124) @ 0: uvm_test_top.compB [COMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(70) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to CompB
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO testbench.sv(124) @ 0: uvm_test_top.compB [COMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: 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) @ 0: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

Port to Export to Imp

In this example componentA is the initiator and sends a packet from its port to the destination subCompB which implements the put method. Since componentB is the container for the target, it should have an export to forward the packets received from the connected port at the top level.

uvm-tlm-put-port-export-imp
  
  
// componentA will start transactions and send to its port
class componentA extends uvm_component;
   `uvm_component_utils (componentA)
   function new (string name = "componentA", uvm_component parent= null);
      super.new (name, parent);
   endfunction

    uvm_blocking_put_port #(Packet) m_put_port;
    int m_num_tx=2;
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    m_put_port = new ("m_put_port", this);
  endfunction
  
  
  // Create a packet, randomize it and send it through the port
  // Note that put() is a method defined by the receiving component
  // Repeat these steps N times to send N packets
   virtual task run_phase (uvm_phase phase);
     phase.raise_objection(this);
     repeat (m_num_tx) begin
         Packet pkt = Packet::type_id::create ("pkt");
         assert(pkt.randomize ()); 
       
       	 // Print the packet to be displayed in log
       `uvm_info ("SUBCOMPA", "Packet sent to subCompB", UVM_LOW)
         pkt.print (uvm_default_line_printer);
       
         // Call the TLM put() method of put_port class and pass packet as argument
         m_put_port.put (pkt);
      end
      phase.drop_objection(this);
   endtask
endclass

  
Simulation Log

UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------
Name              Type                     Size  Value
------------------------------------------------------
uvm_test_top      my_test                  -     @1839
  compA           componentA               -     @1908
    m_put_port    uvm_blocking_put_port    -     @1974
  compB           componentB               -     @1939
    m_put_export  uvm_blocking_put_export  -     @2044
    m_subcomp_B   subCompB                 -     @2010
      m_put_imp   uvm_blocking_put_imp     -     @2081
------------------------------------------------------

UVM_INFO testbench.sv(73) @ 0: uvm_test_top.compA [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'ha1  data: 'h64  } 
UVM_INFO testbench.sv(127) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'ha1  data: 'h64  } 
UVM_INFO testbench.sv(73) @ 0: uvm_test_top.compA [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'hc1  data: 'hb9  } 
UVM_INFO testbench.sv(127) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: ([email protected]) { addr: 'hc1  data: 'hb9  } 
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: 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) @ 0: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

Port to Port to Export to Export to Imp

In this example there is an additional layer between componentB and the target subCompB2 called subCompB1. This layer has to simply forward the packet to the destination and hence shall have an export.

uvm-tlm-put-port-port-export-export-imp Simulation Log

UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
--------------------------------------------------------
Name                Type                     Size  Value
--------------------------------------------------------
uvm_test_top        my_test                  -     @1845
  compA             componentA               -     @1914
    m_put_port      uvm_blocking_put_port    -     @2011
    m_subcomp_A     subCompA                 -     @1977
      m_put_port    uvm_blocking_put_port    -     @2050
  compB             componentB               -     @1945
    m_put_export    uvm_blocking_put_export  -     @2118
    m_subcomp_B1    subCompB1                -     @2084
      m_put_export  uvm_blocking_put_export  -     @2186
      m_subcomp_B2  subCompB2                -     @2152
        m_put_imp   uvm_blocking_put_imp     -     @2223
--------------------------------------------------------

UVM_INFO testbench.sv(75) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(179) @ 0: uvm_test_top.compB.m_subcomp_B1.m_subcomp_B2 [SUBCOMPB2] Packet received from subCompA
pkt: ([email protected]) { addr: 'h24  data: 'h31  } 
UVM_INFO testbench.sv(75) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO testbench.sv(179) @ 0: uvm_test_top.compB.m_subcomp_B1.m_subcomp_B2 [SUBCOMPB2] Packet received from subCompA
pkt: ([email protected]) { addr: 'h9a  data: 'hfb  } 
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: 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) @ 0: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---

This is another example of a SystemVerilog testbench using OOP concepts like inheritance, polymorphism to build a functional testbench for a simple design.

Design

  
  
module switch 
  # (parameter ADDR_WIDTH = 8,
     parameter DATA_WIDTH = 16,
     parameter ADDR_DIV = 8'h3F
    )
  
  ( input clk,
   	input rstn,
    input vld,
    
    input [ADDR_WIDTH-1:0] 	addr,
   input [DATA_WIDTH-1:0] 	data,
   
   output reg [ADDR_WIDTH-1:0] 	addr_a,
   output reg [DATA_WIDTH-1:0] 	data_a,

   output reg [ADDR_WIDTH-1:0] 	addr_b,
   output reg [DATA_WIDTH-1:0] 	data_b
  );
  
  always @ (posedge clk) begin
    if (!rstn) begin
      	addr_a <= 0;
    	data_a <= 0;
    	addr_b <= 0;
    	data_b <= 0; end else begin if (vld) begin if (addr >= 0 & addr <= ADDR_DIV) begin
        	addr_a <= addr;
      		data_a <= data;
          	addr_b <= 0;
          	data_b <= 0;
      	end else begin
          	addr_a <= 0;
          	data_a <= 0;
        	addr_b <= addr;
        	data_b <= data;
      	end
      end
    end
  end
endmodule

  

In a previous article, concepts and components of a simple testbench was discussed. Let us look at a practical SystemVerilog testbench example with all those verification components and how concepts in SystemVerilog has been used to create a reusable environment.

Design

  
  
// Note that in this protocol, write data is provided
// in a single clock along with the address while read
// data is received on the next clock, and no transactions
// can be started during that time indicated by "ready"
// signal.

module reg_ctrl 
  # (
     parameter ADDR_WIDTH 	= 8,
     parameter DATA_WIDTH 	= 16,
     parameter DEPTH 		= 256,
     parameter RESET_VAL  	= 16'h1234
  )
  ( input 					clk,
  	input 					rstn,
    input [ADDR_WIDTH-1:0] 	addr,
    input 					sel,
  	input 					wr,
    input [DATA_WIDTH-1:0] 	wdata,
    output reg [DATA_WIDTH-1:0] 	rdata,
    output reg				ready);
  
  	// Some memory element to store data for each addr
    reg [DATA_WIDTH-1:0] ctrl [DEPTH];
  
  reg  ready_dly;
  wire ready_pe;
  
  // If reset is asserted, clear the memory element
  // Else store data to addr for valid writes
  // For reads, provide read data back
  always @ (posedge clk) begin
    if (!rstn) begin
      for (int i = 0; i < DEPTH; i += 1) begin
        ctrl[i] <= RESET_VAL;
      end
    end else begin
    	if (sel & ready & wr) begin
      		ctrl[addr] <= wdata;
    	end
 
    	if (sel & ready & !wr) begin
          rdata <= ctrl[addr];
  		end else begin
          rdata <= 0;
        end
    end
  end
  
  // Ready is driven using this always block
  // During reset, drive ready as 1
  // Else drive ready low for a clock low
  // for a read until the data is given back
  always @ (posedge clk) begin
    if (!rstn) begin
      ready <= 1;
    end else begin
      if (sel & ready_pe) begin
      	ready <= 1;
      end
	 if (sel & ready & !wr) begin
       ready <= 0;
     end
    end
  end
  
  // Drive internal signal accordingly
  always @ (posedge clk) begin
    if (!rstn) ready_dly <= 1;
   		else ready_dly <= ready;
  end
  
   assign ready_pe = ~ready & ready_dly;
endmodule

  

Transaction Object

  
  
class reg_item;
  // This is the base transaction object that will be used
  // in the environment to initiate new transactions and 
  // capture transactions at DUT interface
  rand 	bit [7:0] 	addr;
  rand 	bit [15:0] 	wdata;
  		bit [15:0] 	rdata;
  rand 	bit 		wr;
  
  // This function allows us to print contents of the data packet
  // so that it is easier to track in a logfile
  function void print(string tag="");
    $display ("T=%0t [%s] addr=0x%0h wr=%0d wdata=0x%0h rdata=0x%0h", 
              			$time, tag, addr, wr, wdata, rdata);
  endfunction
endclass

  

Driver

  
  
// The driver is responsible for driving transactions to the DUT 
// All it does is to get a transaction from the mailbox if it is 
// available and drive it out into the DUT interface.
class driver;
  virtual reg_if vif;
  event drv_done;
  mailbox drv_mbx;
  
  task run();
    $display ("T=%0t [Driver] starting ...", $time);
    @ (posedge vif.clk);
    
    // Try to get a new transaction every time and then assign 
    // packet contents to the interface. But do this only if the 
    // design is ready to accept new transactions
    forever begin
      reg_item item;
      
      $display ("T=%0t [Driver] waiting for item ...", $time);
      drv_mbx.get(item);      
	  item.print("Driver");
      vif.sel <= 1;
      vif.addr 	<= item.addr;
      vif.wr 	<= item.wr;
      vif.wdata <= item.wdata;
      @ (posedge vif.clk);
      while (!vif.ready)  begin
        $display ("T=%0t [Driver] wait until ready is high", $time);
        @(posedge vif.clk);
      end
      
      // When transfer is over, raise the done event
      vif.sel <= 0; ->drv_done;
    end   
  endtask
endclass

  

Monitor

  
  
// The monitor has a virtual interface handle with which it can monitor
// the events happening on the interface. It sees new transactions and then
// captures information into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
  virtual reg_if vif;
  mailbox scb_mbx; 		// Mailbox connected to scoreboard
  
  task run();
    $display ("T=%0t [Monitor] starting ...", $time);
    
    // Check forever at every clock edge to see if there is a 
    // valid transaction and if yes, capture info into a class
    // object and send it to the scoreboard when the transaction 
    // is over.
    forever begin
      @ (posedge vif.clk);
      if (vif.sel) begin
        reg_item item = new;
        item.addr = vif.addr;
        item.wr = vif.wr;
        item.wdata = vif.wdata;

        if (!vif.wr) begin
          @(posedge vif.clk);
        	item.rdata = vif.rdata;
        end
        item.print("Monitor");
        scb_mbx.put(item);
      end
    end
  endtask
endclass

  

Scoreboard

  
  
// The scoreboard is responsible to check data integrity. Since the design
// stores data it receives for each address, scoreboard helps to check if the
// same data is received when the same address is read at any later point
// in time. So the scoreboard has a "memory" element which updates it
// internally for every write operation.
class scoreboard;
  mailbox scb_mbx;
  
  reg_item refq[256];
  
  task run();
    forever begin
      reg_item item;
      scb_mbx.get(item);
      item.print("Scoreboard");
      
      if (item.wr) begin
        if (refq[item.addr] == null)
          refq[item.addr] = new;
        
        refq[item.addr] = item;
        $display ("T=%0t [Scoreboard] Store addr=0x%0h wr=0x%0h data=0x%0h", $time, item.addr, item.wr, item.wdata);
      end
      
        if (!item.wr) begin
          if (refq[item.addr] == null)
            if (item.rdata != 'h1234)
              	$display ("T=%0t [Scoreboard] ERROR! First time read, addr=0x%0h exp=1234 act=0x%0h",
                        											$time, item.addr, item.rdata);
          	else
          		$display ("T=%0t [Scoreboard] PASS! First time read, addr=0x%0h exp=1234 act=0x%0h",
                    												$time, item.addr, item.rdata);
          else
            if (item.rdata != refq[item.addr].wdata)
              $display ("T=%0t [Scoreboard] ERROR! addr=0x%0h exp=0x%0h act=0x%0h",
                        $time, item.addr, refq[item.addr].wdata, item.rdata);
           else
             $display ("T=%0t [Scoreboard] PASS! addr=0x%0h exp=0x%0h act=0x%0h", 
                       $time, item.addr, refq[item.addr].wdata, item.rdata);
        end
    end
  endtask
endclass

  

Environment

  
  
// The environment is a container object simply to hold all verification 
// components together. This environment can then be reused later and all
// components in it would be automatically connected and available for use
// This is an environment without a generator.
class env;
  driver 			d0; 		// Driver to design
  monitor 			m0; 		// Monitor from design
  scoreboard 		s0; 		// Scoreboard connected to monitor
  mailbox 			scb_mbx; 	// Top level mailbox for SCB <-> MON 
  virtual reg_if 	vif; 		// Virtual interface handle
  
  // Instantiate all testbench components
  function new();
    d0 = new;
    m0 = new;
    s0 = new;
    scb_mbx = new();
  endfunction
  
  // Assign handles and start all components so that 
  // they all become active and wait for transactions to be
  // available
  virtual task run();
    d0.vif = vif;
    m0.vif = vif;
    m0.scb_mbx = scb_mbx;
    s0.scb_mbx = scb_mbx;
    
    fork
    	s0.run();
		d0.run();
    	m0.run();
    join_any
  endtask
endclass

  

Test

  
  
// an environment without the generator and hence the stimulus should be 
// written in the test. 
class test;
  env e0;
  mailbox drv_mbx;
  
  function new();
    drv_mbx = new();
    e0 = new();
  endfunction
  
  virtual task run();
    e0.d0.drv_mbx = drv_mbx;
    
    fork
    	e0.run();
    join_none
    
    apply_stim();
  endtask
  
  virtual task apply_stim();
    reg_item item;
    
    $display ("T=%0t [Test] Starting stimulus ...", $time);
    item = new;
    item.randomize() with { addr == 8'haa; wr == 1; };
    drv_mbx.put(item);
    
    item = new;
    item.randomize() with { addr == 8'haa; wr == 0; };
    drv_mbx.put(item);
  endtask
endclass

  

Interface

  
  
// The interface allows verification components to access DUT signals
// using a virtual interface handle
interface reg_if (input bit clk);
  logic rstn;
  logic [7:0] addr;
  logic [15:0] wdata;
  logic [15:0] rdata;
  logic 		wr;
  logic 		sel;
  logic 		ready;
endinterface

  

Testbench Top

  
  
// Top level testbench contains the interface, DUT and test handles which 
// can be used to start test components once the DUT comes out of reset. Or
// the reset can also be a part of the test class in which case all you need
// to do is start the test's run method.
module tb;
  reg clk;
  
  always #10 clk = ~clk;
  reg_if _if (clk);
  
  reg_ctrl u0 ( .clk (clk),
            	.addr (_if.addr),
               	.rstn(_if.rstn),
            	.sel  (_if.sel),
               	.wr (_if.wr),
            	.wdata (_if.wdata),
            	.rdata (_if.rdata),
            	.ready (_if.ready));
  
  initial begin
    new_test t0;
    
    clk <= 0;
    _if.rstn <= 0;
    _if.sel <= 0;
    #20 _if.rstn <= 1;
    
    t0 = new;
    t0.e0.vif = _if;
    t0.run();
    
    // Once the main stimulus is over, wait for some time
    // until all transactions are finished and then end 
    // simulation. Note that $finish is required because
    // there are components that are running forever in 
    // the background like clk, monitor, driver, etc
    #200 $finish;
  end
  
  // Simulator dependent system tasks that can be used to 
  // dump simulation waves.
  initial begin
    $dumpvars;
    $dumpfile("dump.vcd");
  end
endmodule

  

There are two types of timing controls in Verilog - delay and event expressions. The delay control is just a way of adding a delay between the time the simulator encounters the statement and when it actually executes it. The event expression allows the statement to be delayed until the occurrence of some simulation event which can be a change of value on a net or variable (implicit event) or an explicitly named event that is triggered in another procedure.

Simulation time can be advanced by one of the following methods.

Gates and nets that have been modeled to have internal delays also advance simulation time.

Delay Control

If the delay expression evaluates to an unknown or high-impedance value it will be interpreted as zero delay. If it evaluates to a negative value, it will be interpreted as a 2's complement unsigned integer of the same size as a time variable.

  
  
`timescale 1ns/1ps

module tb;
  reg [3:0] a, b;
  
  initial begin
    {a, b} <= 0;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);
    
    #10;
    a <= $random;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);
    
    #10 b <= $random;
    $display ("T=%0t a=%0d b=%0d", $realtime, a, b);
    
    #(a) $display ("T=%0t After a delay of a=%0d units", $realtime, a);
    #(a+b) $display ("T=%0t After a delay of a=%0d + b=%0d = %0d units", $realtime, a, b, a+b);
    #((a+b)*10ps) $display ("T=%0t After a delay of %0d * 10ps", $realtime, a+b);
    
    #(b-a) $display ("T=%0t Expr evaluates to a negative delay", $realtime);
    #('h10) $display ("T=%0t Delay in hex", $realtime);
    
    a = 'hX;
    #(a) $display ("T=%0t Delay is unknown, taken as zero a=%h", $realtime, a);
    
    a = 'hZ;
    #(a) $display ("T=%0t Delay is in high impedance, taken as zero a=%h", $realtime, a);
    
    #1ps $display ("T=%0t Delay of 10ps", $realtime);
  end
  
endmodule

  

Note that the precision of timescale is in 1ps and hence $realtime is required to display the precision value for the statement with a delay expression (a+b)*10ps.

Simulation Log

xcelium> run
T=0 a=x b=x
T=10000 a=0 b=0
T=20000 a=4 b=0
T=24000 After a delay of a=4 units
T=29000 After a delay of a=4 + b=1 = 5 units
T=29050 After a delay of 5 * 10ps
T=42050 Expr evaluates to a negative delay
T=58050 Delay in hex
T=58050 Delay is unknown, taken as zero a=x
T=58050 Delay is in high impedance, taken as zero a=z
T=58051 Delay of 10ps
xmsim: *W,RNQUIE: Simulation is complete.

Event Control

Value changes on nets and variables can be used as a synchronization event to trigger execution other procedural statements and is an implicit event. The event can also be based on the direction of change like towards 0 which makes it a negedge and a change towards 1 makes it a posedge.

  • A negedge is when there is a transition from 1 to X, Z or 0 and from X or Z to 0
  • A posedge is when there is a transition from 0 to X, Z or 1 and from X or Z to 1

A transition from the same state to the same state is not considered as an edge. An edge event like posedge or negedge can be detected only on the LSB of a vector signal or variable. If an expression evaluates to the same result it cannot be considered as an event.

  
  
module tb;
  reg a, b;

  initial begin
    a <= 0;
    
    #10 a <= 1;
    #10 b <= 1;

    #10 a <= 0;
    #15 a <= 1; 
  end

  // Start another procedural block that waits for an update to
  // signals made in the above procedural block
  
  initial begin 
    @(posedge a); 
    $display ("T=%0t Posedge of a detected for 0->1", $time); 
    @(posedge b); 
    $display ("T=%0t Posedge of b detected for X->1", $time);
  end 
  
  initial begin
    @(posedge (a + b)) $display ("T=%0t Posedge of a+b", $time);

    @(a) $display ("T=%0t Change in a found", $time);
  end
endmodule

  
Simulation Log

ncsim> run
T=10 Posedge of a detected for 0->1
T=20 Posedge of b detected for X->1
T=30 Posedge of a+b
T=45 Change in a found
ncsim: *W,RNQUIE: Simulation is complete.

Named Events

The keyword event can be used to declare a named event which can be triggered explicitly. An event cannot hold any data, has no time duration and can be made to occur at any particular time. A named event is triggered by the -> operator by prefixing it before the named event handle. A named event can be waited upon by using the @ operator described above.

  
  
module tb;
  event a_event;
  event b_event[5];
  
  initial begin
    #20 -> a_event;
    
    #30;
    ->a_event;
    
    #50 ->a_event;
    #10 ->b_event[3];
  end
  
  always @ (a_event) $display ("T=%0t [always] a_event is triggered", $time);
  
  initial begin
    #25;
    @(a_event) $display ("T=%0t [initial] a_event is triggered", $time);
    
    #10 @(b_event[3]) $display ("T=%0t [initial] b_event is triggered", $time);
  end
endmodule

  

Named events can be used to synchronize two or more concurrently running processes. For example, the always block and the second initial block are synchronized by a_event. Events can be declared as arrays like in the case of b_event which is an array of size 5 and the index 3 is used for trigger and wait purpose.

Simulation Log

ncsim> run
T=20 [always] a_event is triggered
T=50 [always] a_event is triggered
T=50 [initial] a_event is triggered
T=100 [always] a_event is triggered
T=110 [initial] b_event is triggered
ncsim: *W,RNQUIE: Simulation is complete.

Event or operator

The or operator can be used to wait on until any one of the listed events is triggered in an expression. The comma , can also be used instead of the or operator.

  
  
module tb;
  reg a, b;
  
  initial begin
    $monitor ("T=%0t a=%0d b=%0d", $time, a, b);
    {a, b} <= 0;
    
    #10 a <= 1;
    #5  b <= 1;
	#5  b <= 0;
  end
  
  // Use "or" between events
  always @ (posedge a or posedge b) 
    $display ("T=%0t posedge of a or b found", $time);
  
  // Use a comma between
  always @ (posedge a, negedge b)
    $display ("T=%0t posedge of a or negedge of b found", $time);
  
  always @ (a, b) 
    $display ("T=%0t Any change on a or b", $time);
endmodule

  
Simulation Log

ncsim> run
T=0 posedge of a or negedge of b found
T=0 Any change on a or b
T=0 a=0 b=0
T=10 posedge of a or b found
T=10 posedge of a or negedge of b found
T=10 Any change on a or b
T=10 a=1 b=0
T=15 posedge of a or b found
T=15 Any change on a or b
T=15 a=1 b=1
T=20 posedge of a or negedge of b found
T=20 Any change on a or b
T=20 a=1 b=0
ncsim: *W,RNQUIE: Simulation is complete.

Implicit Event Expression List

The sensitivity list or the event expression list is often a common cause for a lot of functional errors in the RTL. This is because the user may forget to update the sensitivity list after introducing a new signal in the procedural block.

  
  
module tb;
	reg a, b, c, d;
	reg x, y;
	
	// Event expr/sensitivity list is formed by all the
	// signals inside () after @ operator and in this case
	// it is a, b, c or d
	always @ (a, b, c, d) begin
		x = a | b;
		y = c ^ d;
	end
	
	initial begin
		$monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b x=%0b y=%0b", $time, a, b, c, d, x, y);
		{a, b, c, d} <= 0;
	
		#10 {a, b, c, d} <= $random;
		#10 {a, b, c, d} <= $random;
		#10 {a, b, c, d} <= $random;
	end
endmodule

  
Simulation Log

ncsim> run
T=0 a=0 b=0 c=0 d=0 x=0 y=0
T=10 a=0 b=1 c=0 d=0 x=1 y=0
T=20 a=0 b=0 c=0 d=1 x=0 y=1
T=30 a=1 b=0 c=0 d=1 x=1 y=1
ncsim: *W,RNQUIE: Simulation is complete.

If the user decides to add new signal e and capture the inverse into z, special care must be taken to add e also into the sensitivity list.

  
  
module tb;
	reg a, b, c, d, e;
	reg x, y, z;
	
  // Add "e" also into sensitivity list
  always @ (a, b, c, d, e) begin
		x = a | b;
		y = c ^ d;
    	z = ~e;
	end
	
	initial begin
      $monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b e=%0b x=%0b y=%0b z=%0b", 
                				$time, a, b, c, d, e, x, y, z);
      {a, b, c, d, e} <= 0;
	
      #10 {a, b, c, d, e} <= $random;
      #10 {a, b, c, d, e} <= $random;
      #10 {a, b, c, d, e} <= $random;
	end
endmodule

  
Simulation Log

ncsim> run
T=0 a=0 b=0 c=0 d=0 e=0 x=0 y=0 z=1
T=10 a=0 b=0 c=1 d=0 e=0 x=0 y=1 z=1
T=20 a=0 b=0 c=0 d=0 e=1 x=0 y=0 z=0
T=30 a=0 b=1 c=0 d=0 e=1 x=1 y=0 z=0
ncsim: *W,RNQUIE: Simulation is complete.

Verilog now allows the sensitivity list to be replaced by * which is a convenient shorthand that eliminates these problems by adding all nets and variables that are read by the statemnt like shown below.

  
  
module tb;
	reg a, b, c, d, e;
	reg x, y, z;
	
  // Use @* or @(*)
  always @ * begin
		x = a | b;
		y = c ^ d;
    	z = ~e;
	end
	
	initial begin
      $monitor ("T=%0t a=%0b b=%0b c=%0b d=%0b e=%0b x=%0b y=%0b z=%0b", 
                				$time, a, b, c, d, e, x, y, z);
      {a, b, c, d, e} <= 0;
	
      #10 {a, b, c, d, e} <= $random;
      #10 {a, b, c, d, e} <= $random;
      #10 {a, b, c, d, e} <= $random;
	end
endmodule

  
Simulation Log

ncsim> run
T=0 a=0 b=0 c=0 d=0 e=0 x=0 y=0 z=1
T=10 a=0 b=0 c=1 d=0 e=0 x=0 y=1 z=1
T=20 a=0 b=0 c=0 d=0 e=1 x=0 y=0 z=0
T=30 a=0 b=1 c=0 d=0 e=1 x=1 y=0 z=0
ncsim: *W,RNQUIE: Simulation is complete.

Level Sensitive Event Control

Execution of a procedural statement can also be delayed until a condition becomes true and can be accomplished with the wait keyword and is a level-sensitive control.

The wait statement shall evaluate a condition and if it is false, the procedural statements following it shall remain blocked until the condition becomes true.

  
  
module tb;
  reg [3:0] ctr;
  reg clk;
  
  initial begin
    {ctr, clk} <= 0;
    
    wait (ctr);
    $display ("T=%0t Counter reached non-zero value 0x%0h", $time, ctr);
    
    wait (ctr == 4) $display ("T=%0t Counter reached 0x%0h", $time, ctr);
    
    $finish;
  end
  
  always #10 clk = ~clk;
  
  always @ (posedge clk) 
    ctr <= ctr + 1;
  
endmodule

  
Simulation Log

ncsim> run
T=10 Counter reached non-zero value 0x1
T=70 Counter reached 0x4
T=90 Counter reached 0x5
T=170 Counter reached 0x9
Simulation complete via $finish(1) at time 170 NS + 1

Verilog also provides support for transistor level modeling although it is rarely used by designers these days as the complexity of circuits have required them to move to higher levels of abstractions rather than use switch level modeling.

NMOS/PMOS

  
  
module des (input d, ctrl,
			output outn, outp);
			
  nmos (outn, d, ctrl);
  pmos (outp, d, ctrl);
endmodule		

  
  
  
module tb;
  reg d, ctrl;
  wire outn, outp;
  
  des u0 (.d(d), .ctrl(ctrl), .outn(outn), .outp(outp));
  
  initial begin
    {d, ctrl} <= 0;
    
    $monitor ("T=%0t d=%0b ctrl=%0b outn=%0b outp=%0b", $time, d, ctrl, outn, outp);
    
    #10 d <= 1;
    #10 ctrl <= 1;
    #10 ctrl <= 0;
    #10 d <= 0;
  end
endmodule

  
Simulation Log

ncsim> run
T=0 d=0 ctrl=0 outn=z outp=0
T=10 d=1 ctrl=0 outn=z outp=1
T=20 d=1 ctrl=1 outn=1 outp=z
T=30 d=1 ctrl=0 outn=z outp=1
T=40 d=0 ctrl=0 outn=z outp=0
ncsim: *W,RNQUIE: Simulation is complete.

CMOS Switches

  
  
module des (input d, nctrl, pctrl,
			output out);
			
  cmos (out, d, nctrl, pctrl);
endmodule

  
  
  
module tb;
  reg d, nctrl, pctrl;
  wire out;
  
  des u0 (.d(d), .nctrl(nctrl), .pctrl(pctrl), .out(out));
  
  initial begin
    {d, nctrl, pctrl} <= 0;
    
    $monitor ("T=%0t d=%0b nctrl=%0b pctrl=%0b out=%0b", $time, d, nctrl, pctrl, out);
    
    #10 d <= 1;
    #10 nctrl <= 1;
    #10 pctrl <= 1;
    #10 nctrl <= 0;
    #10 pctrl <= 0;
    #10 d <= 0;
    #10;
  end
endmodule

  
Simulation Log

ncsim> run
T=0 d=0 nctrl=0 pctrl=0 out=0
T=10 d=1 nctrl=0 pctrl=0 out=1
T=20 d=1 nctrl=1 pctrl=0 out=1
T=30 d=1 nctrl=1 pctrl=1 out=1
T=40 d=1 nctrl=0 pctrl=1 out=z
T=50 d=1 nctrl=0 pctrl=0 out=1
T=60 d=0 nctrl=0 pctrl=0 out=0
ncsim: *W,RNQUIE: Simulation is complete.

Bidirectional Switches

tran

  
  
module des (input io1, ctrl,
            output io2);

  tran (io1, io2);	
endmodule

  
  
  
module tb;
  reg io1, ctrl;
  wire io2;
  
  des u0 (.io1(io1), .ctrl(ctrl), .io2(io2));
  
  initial begin
    {io1, ctrl} <= 0;
    
    $monitor ("T=%0t io1=%0b ctrl=%0b io2=%0b", $time, io1, ctrl, io2);
    
    #10 io1  <= 1;
    #10 ctrl <= 1;
    #10 ctrl <= 0;
    #10 io1  <= 0;
        
  end
endmodule

  
Simulation Log

ncsim> run
T=0 io1=0 ctrl=0 io2=0
T=10 io1=1 ctrl=0 io2=1
T=20 io1=1 ctrl=1 io2=1
T=30 io1=1 ctrl=0 io2=1
T=40 io1=0 ctrl=0 io2=0
ncsim: *W,RNQUIE: Simulation is complete.

tranif0

  
  
module des (input io1, ctrl,
            output io2);
            
  tranif0 (io1, io2, ctrl);
endmodule

  
Simulation Log

ncsim> run
T=0 io1=0 ctrl=0 io2=0
T=10 io1=1 ctrl=0 io2=1
T=20 io1=1 ctrl=1 io2=z
T=30 io1=1 ctrl=0 io2=1
T=40 io1=0 ctrl=0 io2=0
ncsim: *W,RNQUIE: Simulation is complete.

tranif1

  
  
module des (input io1, ctrl,
            output io2);
            
  tranif1 (io1, io2, ctrl);
endmodule

  
Simulation Log

ncsim> run
T=0 io1=0 ctrl=0 io2=z
T=10 io1=1 ctrl=0 io2=z
T=20 io1=1 ctrl=1 io2=1
T=30 io1=1 ctrl=0 io2=z
T=40 io1=0 ctrl=0 io2=z
ncsim: *W,RNQUIE: Simulation is complete.

Power and Ground

  
  
module des (output vdd, 
			output gnd);
			
	supply1 _vdd;
	supply0 _gnd;
	
	assign vdd = _vdd;
	assign gnd = _gnd;
endmodule						

  
  
  
module tb;
  wire vdd, gnd;
  
  des u0 (.vdd(vdd), .gnd(gnd));
  
  initial begin
    #10;
    $display ("T=%0t vdd=%0d gnd=%0d", $time, vdd, gnd);
  end
endmodule

  
Simulation Log

ncsim> run
T=10 vdd=1 gnd=0
ncsim: *W,RNQUIE: Simulation is complete.