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 fromuvm_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.

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.

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.