What are registers ?
Registers are essential components in hardware designs, used to store configuration settings, control information, and status data. In modern designs, these registers are often accessed by software or firmware using bus protocols like AXI/APB and can be programmed with certain values. For example, there could be a 32-bit register with several individual fields within it. Each field represents a particular feature that can be configured by software when required.

A SystemVerilog based testbench was explored before to verify a simple register/memory element design that stores write data and gives back read data from requested addresses. Let us build a similar testbench using UVM components so that you can compare it with a traditional SystemVerilog testbench.

Sequence Item
A data class called reg_item is defined to hold random input address and write data and will be randomized inside the sequence before it is sent to the driver. The class also has utility macros to help with common requirements like print, copy and clone.
class reg_item extends uvm_sequence_item;
rand bit [`ADDR_WIDTH-1:0] addr;
rand bit [`DATA_WIDTH-1:0] wdata;
rand bit wr;
bit [`DATA_WIDTH-1:0] rdata;
// Use utility macros to implement standard functions
// like print, copy, clone, etc
`uvm_object_utils_begin(reg_item)
`uvm_field_int (addr, UVM_DEFAULT)
`uvm_field_int (wdata, UVM_DEFAULT)
`uvm_field_int (rdata, UVM_DEFAULT)
`uvm_field_int (wr, UVM_DEFAULT)
`uvm_object_utils_end
virtual function string convert2str();
return $sformatf("addr=0x%0h wr=0x%0h wdata=0x%0h rdata=0x%0h", addr, wr, wdata, rdata);
endfunction
function new(string name = "reg_item");
super.new(name);
endfunction
endclass
UVM Driver
The UVM driver extends from uvm_driver
and is parameterized to accept an object of type reg_item . It also recieves handle to a virtual interface that is used to toggle pins of the DUT. The driver uses standard method calls get_next_item
and item_done
to communicate with its sequencer.
class driver extends uvm_driver #(reg_item);
`uvm_component_utils(driver)
function new(string name = "driver", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual reg_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual reg_if)::get(this, "", "reg_vif", vif))
`uvm_fatal("DRV", "Could not get vif")
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
reg_item m_item;
`uvm_info("DRV", $sformatf("Wait for item from sequencer"), UVM_LOW)
seq_item_port.get_next_item(m_item);
drive_item(m_item);
seq_item_port.item_done();
end
endtask
virtual task drive_item(reg_item m_item);
vif.sel <= 1;
vif.addr <= m_item.addr;
vif.wr <= m_item.wr;
vif.wdata <= m_item.wdata;
@ (posedge vif.clk);
while (!vif.ready) begin
`uvm_info("DRV", "Wait until ready is high", UVM_LOW)
@(posedge vif.clk);
end
vif.sel <= 0;
endtask
endclass
UVM Monitor
The UVM monitor is derived from uvm_monitor
and has a virtual interface handle to listen to activity on the given interface. It tries to decode pin level activity into a protocol packet which it can send out to other testbench components. In this case, it tries to see if address and data pairs are available on the DUT interface, capture them into a class object of type reg_item and send out via an analysis port.
Since it has to decode and capture any activity happening on the DUT interface, it has to run for as long as the simulation is active and hence it is placed inside a forever
loop.
class monitor extends uvm_monitor;
`uvm_component_utils(monitor)
function new(string name="monitor", uvm_component parent=null);
super.new(name, parent);
endfunction
uvm_analysis_port #(reg_item) mon_analysis_port;
virtual reg_if vif;
semaphore sema4;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual reg_if)::get(this, "", "reg_vif", vif))
`uvm_fatal("MON", "Could not get vif")
sema4 = new(1);
mon_analysis_port = new ("mon_analysis_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
// This task monitors the interface for a complete
// transaction and writes into analysis port when complete
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
`uvm_info(get_type_name(), $sformatf("Monitor found packet %s", item.convert2str()), UVM_LOW)
mon_analysis_port.write(item);
end
end
endtask
endclass
UVM Agent
The role of a UVM agent is to encapsulate the sequencer, driver and monitor into a single container. The sequencer is responsible for acting on a given sequence and send the driver with data objects. It is parameterized to accept objects of type reg_item and its seq_item_port
is connected to the driver's seq_item_export
.
class agent extends uvm_agent;
`uvm_component_utils(agent)
function new(string name="agent", uvm_component parent=null);
super.new(name, parent);
endfunction
driver d0; // Driver handle
monitor m0; // Monitor handle
uvm_sequencer #(reg_item) s0; // Sequencer Handle
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
s0 = uvm_sequencer#(reg_item)::type_id::create("s0", this);
d0 = driver::type_id::create("d0", this);
m0 = monitor::type_id::create("m0", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
d0.seq_item_port.connect(s0.seq_item_export);
endfunction
endclass
UVM Scoreboard
The scoreboard receives a data object through its uvm_analysis_imp
port from the monitor. The scoreboard receives data packets from the monitor for both write and read operations and hence it can predict what the read data should be for a given address. As soon as the scoreboard receives an item, its write
method will be executed which in turn runs the checker and predicts the result.
This scoreboard has an internal array which is used to store data for different addresses and hence mimics design behavior. This is also called as a reference model. Reference models may be as simple as this or very complicated based on design functionality.
class scoreboard extends uvm_scoreboard;
`uvm_component_utils(scoreboard)
function new(string name="scoreboard", uvm_component parent=null);
super.new(name, parent);
endfunction
reg_item refq[`DEPTH];
uvm_analysis_imp #(reg_item, scoreboard) m_analysis_imp;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_analysis_imp = new("m_analysis_imp", this);
endfunction
virtual function write(reg_item item);
if (item.wr) begin
if (refq[item.addr] == null)
refq[item.addr] = new;
refq[item.addr] = item;
`uvm_info(get_type_name(), $sformatf("Store addr=0x%0h wr=0x%0h data=0x%0h", item.addr, item.wr, item.wdata), UVM_LOW)
end
if (!item.wr) begin
if (refq[item.addr] == null)
if (item.rdata != 'h1234)
`uvm_error (get_type_name(), $sformatf("First time read, addr=0x%0h exp=1234 act=0x%0h",
item.addr, item.rdata))
else
`uvm_info(get_type_name(), $sformatf("PASS! First time read, addr=0x%0h exp=1234 act=0x%0h",
item.addr, item.rdata), UVM_LOW)
else
if (item.rdata != refq[item.addr].wdata)
`uvm_error (get_type_name(), $sformatf("addr=0x%0h exp=0x%0h act=0x%0h",
item.addr, refq[item.addr].wdata, item.rdata))
else
`uvm_info(get_type_name(), $sformatf("PASS! addr=0x%0h exp=0x%0h act=0x%0h",
item.addr, refq[item.addr].wdata, item.rdata), UVM_LOW)
end
endfunction
endclass
UVM Environment
The UVM environment is responsible for instantiating and connecting all testbench components. Here, analysis port from the agent's monitor is connected to the analysis implementation port in scoreboard. This connection allows the scoreboard to receive packets whenever the monitor pushes new data into the analysis port.
class env extends uvm_env;
`uvm_component_utils(env)
function new(string name="env", uvm_component parent=null);
super.new(name, parent);
endfunction
agent a0; // Agent handle
scoreboard sb0; // Scoreboard handle
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
a0 = agent::type_id::create("a0", this);
sb0 = scoreboard::type_id::create("sb0", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
a0.m0.mon_analysis_port.connect(sb0.m_analysis_imp);
endfunction
endclass
UVM Test
The test instantiates an environment, sets up virtual interface handles to sub components and starts a top level sequence. In this simple example, we have chosen to also reset the DUT using a reset task, after which a sequence of type gen_item_seq is started on the agent's sequencer.
Since the sequence consumes simulation time and all other testbench components have to run as long as the sequence is active, it is important to raise and drop objections appropriately. Note that the sequence has a random variable to represent the number of packets sent to the driver and can be constrained appropriately to test more combinations of address and data.
class test extends uvm_test;
`uvm_component_utils(test)
function new(string name = "test", uvm_component parent=null);
super.new(name, parent);
endfunction
env e0;
virtual reg_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
e0 = env::type_id::create("e0", this);
if (!uvm_config_db#(virtual reg_if)::get(this, "", "reg_vif", vif))
`uvm_fatal("TEST", "Did not get vif")
uvm_config_db#(virtual reg_if)::set(this, "e0.a0.*", "reg_vif", vif);
endfunction
virtual task run_phase(uvm_phase phase);
gen_item_seq seq = gen_item_seq::type_id::create("seq");
phase.raise_objection(this);
apply_reset();
seq.randomize() with {num inside {[20:30]}; };
seq.start(e0.a0.s0);
#200;
phase.drop_objection(this);
endtask
virtual task apply_reset();
vif.rstn <= 0;
repeat(5) @ (posedge vif.clk);
vif.rstn <= 1;
repeat(10) @ (posedge vif.clk);
endtask
endclass
UVM Sequence
Last but not the least, we have the main sequence that forms the stimulus and randomizer aspect of the testbench. The sequence when started on a sequencer gets the body
method executed which ultimately randomizes address/data and send them to the driver.
class gen_item_seq extends uvm_sequence;
`uvm_object_utils(gen_item_seq)
function new(string name="gen_item_seq");
super.new(name);
endfunction
rand int num; // Config total number of items to be sent
constraint c1 { soft num inside {[2:5]}; }
virtual task body();
for (int i = 0; i < num; i ++) begin
reg_item m_item = reg_item::type_id::create("m_item");
start_item(m_item);
m_item.randomize();
`uvm_info("SEQ", $sformatf("Generate new item: "), UVM_LOW)
m_item.print();
finish_item(m_item);
end
`uvm_info("SEQ", $sformatf("Done generation of %0d items", num), UVM_LOW)
endtask
endclass

UVM_INFO @ 0: reporter [RNTST] Running test test... UVM_INFO testbench.sv(80) @ 0: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 290: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xa4 wr=0x0 wdata=0x9783 rdata=0x0 UVM_INFO testbench.sv(80) @ 310: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 310: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x1b wr=0x1 wdata=0x3025 rdata=0x0 UVM_INFO testbench.sv(141) @ 330: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xa4 wr=0x0 wdata=0x9783 rdata=0x1234 UVM_INFO testbench.sv(182) @ 330: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0xa4 exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 330: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 350: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x1b wr=0x1 wdata=0x3025 rdata=0x0 UVM_INFO testbench.sv(172) @ 350: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x1b wr=0x1 data=0x3025 UVM_INFO testbench.sv(80) @ 350: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 350: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x9 wr=0x0 wdata=0x6169 rdata=0x0 UVM_INFO testbench.sv(80) @ 370: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 370: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x6e wr=0x0 wdata=0x2b4f rdata=0x0 UVM_INFO testbench.sv(141) @ 390: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x9 wr=0x0 wdata=0x6169 rdata=0x1234 UVM_INFO testbench.sv(182) @ 390: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x9 exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 390: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(80) @ 410: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 410: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x4a wr=0x1 wdata=0x8dd7 rdata=0x0 UVM_INFO testbench.sv(141) @ 430: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x6e wr=0x0 wdata=0x2b4f rdata=0x1234 UVM_INFO testbench.sv(182) @ 430: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x6e exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 430: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 450: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x4a wr=0x1 wdata=0x8dd7 rdata=0x0 UVM_INFO testbench.sv(172) @ 450: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x4a wr=0x1 data=0x8dd7 UVM_INFO testbench.sv(80) @ 450: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 450: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x9d wr=0x0 wdata=0x8901 rdata=0x0 UVM_INFO testbench.sv(80) @ 470: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 470: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x66 wr=0x1 wdata=0x1cce rdata=0x0 UVM_INFO testbench.sv(141) @ 490: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x9d wr=0x0 wdata=0x8901 rdata=0x1234 UVM_INFO testbench.sv(182) @ 490: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x9d exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 490: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 510: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x66 wr=0x1 wdata=0x1cce rdata=0x0 UVM_INFO testbench.sv(172) @ 510: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x66 wr=0x1 data=0x1cce UVM_INFO testbench.sv(80) @ 510: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 510: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xa7 wr=0x1 wdata=0x493c rdata=0x0 UVM_INFO testbench.sv(141) @ 530: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xa7 wr=0x1 wdata=0x493c rdata=0x0 UVM_INFO testbench.sv(172) @ 530: uvm_test_top.e0.sb0 [scoreboard] Store addr=0xa7 wr=0x1 data=0x493c UVM_INFO testbench.sv(80) @ 530: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 530: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x5e wr=0x1 wdata=0xe4c rdata=0x0 UVM_INFO testbench.sv(141) @ 550: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x5e wr=0x1 wdata=0xe4c rdata=0x0 UVM_INFO testbench.sv(172) @ 550: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x5e wr=0x1 data=0xe4c UVM_INFO testbench.sv(80) @ 550: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 550: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x8d wr=0x1 wdata=0x6bfe rdata=0x0 UVM_INFO testbench.sv(141) @ 570: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x8d wr=0x1 wdata=0x6bfe rdata=0x0 UVM_INFO testbench.sv(172) @ 570: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x8d wr=0x1 data=0x6bfe UVM_INFO testbench.sv(80) @ 570: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 570: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x32 wr=0x0 wdata=0x6252 rdata=0x0 UVM_INFO testbench.sv(80) @ 590: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 590: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x4f wr=0x0 wdata=0xf148 rdata=0x0 UVM_INFO testbench.sv(141) @ 610: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x32 wr=0x0 wdata=0x6252 rdata=0x1234 UVM_INFO testbench.sv(182) @ 610: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x32 exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 610: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(80) @ 630: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 630: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xe2 wr=0x0 wdata=0x18e0 rdata=0x0 UVM_INFO testbench.sv(141) @ 650: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x4f wr=0x0 wdata=0xf148 rdata=0x1234 UVM_INFO testbench.sv(182) @ 650: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x4f exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 650: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(80) @ 670: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 670: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xec wr=0x0 wdata=0xd91a rdata=0x0 UVM_INFO testbench.sv(141) @ 690: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xe2 wr=0x0 wdata=0x18e0 rdata=0x1234 UVM_INFO testbench.sv(182) @ 690: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0xe2 exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 690: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(80) @ 710: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 710: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x6e wr=0x1 wdata=0x31f6 rdata=0x0 UVM_INFO testbench.sv(141) @ 730: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xec wr=0x0 wdata=0xd91a rdata=0x1234 UVM_INFO testbench.sv(182) @ 730: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0xec exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 730: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 750: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x6e wr=0x1 wdata=0x31f6 rdata=0x0 UVM_INFO testbench.sv(172) @ 750: uvm_test_top.e0.sb0 [scoreboard] Store addr=0x6e wr=0x1 data=0x31f6 UVM_INFO testbench.sv(80) @ 750: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 750: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x66 wr=0x0 wdata=0x2374 rdata=0x0 UVM_INFO testbench.sv(80) @ 770: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 770: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xd5 wr=0x1 wdata=0xad94 rdata=0x0 UVM_INFO testbench.sv(141) @ 790: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x66 wr=0x0 wdata=0x2374 rdata=0x1cce UVM_INFO testbench.sv(189) @ 790: uvm_test_top.e0.sb0 [scoreboard] PASS! addr=0x66 exp=0x1cce act=0x1cce UVM_INFO testbench.sv(94) @ 790: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 810: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xd5 wr=0x1 wdata=0xad94 rdata=0x0 UVM_INFO testbench.sv(172) @ 810: uvm_test_top.e0.sb0 [scoreboard] Store addr=0xd5 wr=0x1 data=0xad94 UVM_INFO testbench.sv(80) @ 810: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 810: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xbb wr=0x1 wdata=0xd056 rdata=0x0 UVM_INFO testbench.sv(141) @ 830: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xbb wr=0x1 wdata=0xd056 rdata=0x0 UVM_INFO testbench.sv(172) @ 830: uvm_test_top.e0.sb0 [scoreboard] Store addr=0xbb wr=0x1 data=0xd056 UVM_INFO testbench.sv(80) @ 830: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 830: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0x18 wr=0x0 wdata=0x8bba rdata=0x0 UVM_INFO testbench.sv(80) @ 850: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(52) @ 850: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: addr=0xec wr=0x1 wdata=0xdfc1 rdata=0x0 UVM_INFO testbench.sv(141) @ 870: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0x18 wr=0x0 wdata=0x8bba rdata=0x1234 UVM_INFO testbench.sv(182) @ 870: uvm_test_top.e0.sb0 [scoreboard] PASS! First time read, addr=0x18 exp=1234 act=0x1234 UVM_INFO testbench.sv(94) @ 870: uvm_test_top.e0.a0.d0 [DRV] Wait until ready is high UVM_INFO testbench.sv(141) @ 890: uvm_test_top.e0.a0.m0 [monitor] Monitor found packet addr=0xec wr=0x1 wdata=0xdfc1 rdata=0x0 UVM_INFO testbench.sv(172) @ 890: uvm_test_top.e0.sb0 [scoreboard] Store addr=0xec wr=0x1 data=0xdfc1 UVM_INFO testbench.sv(80) @ 890: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(55) @ 890: uvm_test_top.e0.a0.s0@@seq [SEQ] Done generation of 20 items UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 1090: 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) @ 1090: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---
The scope resolution operator ::
is used to refer an identifier within the scope of a class.
Left hand side of the scope resolution operator :: should be a class type name, package name, covergroup type name, coverpoint or cross name, typedef
name. The right hand side of the operator should be an identifier like a variable or method name.
Why is scope resolution operator required ?
Classes and other scopes can have same identifier names and may create a namespace collision if referred to without specifying the scope. The scope resolution operator ::
uniquely identifies a member or parameter of a given class.
They are also used to access static variables and methods, parameters and local parameters of a class from outside the class. It also allows access to public and protected members of a base class from within the child class.
Examples
1. Defining extern function
class ABC;
int data;
extern virtual function void display();
endclass
// Definition of an external function using scope
// resolution operator
function void ABC::display();
$display("data = 0x%0h", data);
endfunction
module tb;
initial begin
ABC abc = new();
abc.data = 32'hface_cafe;
abc.display();
end
endmodule
ncsim> run data = 0xfacecafe ncsim: *W,RNQUIE: Simulation is complete. ncsim> exit
2. Accessing static methods and functions
class ABC;
static int data;
static function void display();
$display("data = 0x%0h", data);
endfunction
endclass
module tb;
initial begin
ABC a1, a2;
// Assign to static variable before creating
// class objects, and display using class_type and
// scope resolution operator
ABC::data = 32'hface_cafe;
ABC::display();
a1 = new();
a2 = new();
$display ("a1.data=0x%0h a2.data=0x%0h", a1.data, a2.data);
end
endmodule
ncsim> run data = 0xfacecafe a1.data=0xfacecafe a2.data=0xfacecafe ncsim: *W,RNQUIE: Simulation is complete. ncsim> exit
3. Using package
package my_pkg;
typedef enum bit {FALSE, TRUE} e_bool;
endpackage
module tb;
bit val;
initial begin
// Refer to types that have been declared
// in a package. Note that package has to
// be included in compilation but not
// necessarily "imported"
val = my_pkg::TRUE;
$display("val = 0x%0h", val);
end
endmodule
ncsim> run val = 0x1 ncsim: *W,RNQUIE: Simulation is complete. ncsim> exit
4. Avoid namespace collision
package my_pkg;
typedef enum bit {FALSE, TRUE} e_bool;
endpackage
import my_pkg::*;
module tb;
typedef enum bit {TRUE, FALSE} e_bool;
initial begin
e_bool val;
// Be explicit and say that TRUE from my_pkg
// should be assigned to val
val = my_pkg::TRUE;
$display("val = 0x%0h", val);
// TRUE from current scope will be assigned to
// val
val = TRUE;
$display("val = 0x%0h", val);
end
endmodule
ncsim> run val = 0x1 val = 0x0 ncsim: *W,RNQUIE: Simulation is complete. ncsim> exit
What are UVM phases ?
All testbench components are derived from uvm_component
and are aware of the phase concept. Each component goes through a pre-defined set of phases, and it cannot proceed to the next phase until all components finish their execution in the current phase. So UVM phases act as a synchronizing mechanism in the life cycle of a simulation.
Because phases are defined as callbacks, classes derived from uvm_component
can perform useful work in the callback phase method. Methods that do not consume simulation time are function
s and methods that consume simulation time are task
s. All phases can be grouped into three categories:
- Build time phases
- Run time phases
- Clean-Up phases

Note that run_phase
is launched in parallel with other run-time phases.
Main UVM Phases
Phase Category | UVM Phase Name | Method Type | Description |
---|---|---|---|
Build | build_phase | function | Used to build testbench components and create their instances |
connect_phase | function | Used to connect between different testbench components via TLM ports | |
end_of_elaboration_phase | function | Used to display UVM topology and other functions required to be done after connection | |
start_of_simulation_phase | function | Used to set initial run-time configuration or display topology | |
Run | run_phase | task | Actual simulation that consumes time happens in this UVM phase and runs parallel to other UVM run-time phases. |
Clean | extract_phase | function | Used to extract and compute expected data from scoreboard, an |
check_phase | function | Used to perform scoreboard tasks that check for errors between expected and actual values from design | |
report_phase | function | Used to display result from checkers, or summary of other test objectives | |
final_phase | function | Typically used to do last minute operations before exiting the simulation |
Logically, the first thing to be done is to create testbench component objects so that they can be connected together. This is the reason for the build_phase
. It is better to not start connecting them while other testbench components are still building their sub-components. So we have connect_phase
which will connect all the components that were built in the previous phase. Although the next two phases are rarely used or are typically used to display UVM hierhachy information. Test stimulus is driven to the design during the run_phase
which is launched in parallel with other run-time phases that are described shown below.
Why is build done top to bottom ?
Verification environment usually looks like a nested structure as shown in the image below. In order to build the agent, UVM environment is required to be defined and similarly in order to build the environment, the test class has to be defined. Hence, we can only go from top to bottom.
A SystemVerilog based testbench was explored before to verify a simple design that sends incoming packets to two output ports based on address range. Let us build a similar testbench using UVM components so that you can compare it with a traditional SystemVerilog testbench.

Sequence Item
A class called switch_item is defined to hold random input address and data. It also has variables to hold output address and data so that they can be compared easily in a scoreboard. The pros/cons of this method will be discussed later. Here, addr_a and data_a represent the output packet that comes from port A of the design while addr_b and data_b represent output packet that comes from port B of the design.
Since only one packet comes out of either port A or port B, use of the same class object is okay. The class also has utility macros to help with common requirements like print, copy and clone.
// This is the base transaction object that will be used
// in the environment to initiate new transactions and
// capture transactions at DUT interface
class switch_item extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit [15:0] data;
bit [7:0] addr_a;
bit [15:0] data_a;
bit [7:0] addr_b;
bit [15:0] data_b;
// Use utility macros to implement standard functions
// like print, copy, clone, etc
`uvm_object_utils_begin(switch_item)
`uvm_field_int (addr, UVM_DEFAULT)
`uvm_field_int (data, UVM_DEFAULT)
`uvm_field_int (addr_a, UVM_DEFAULT)
`uvm_field_int (data_a, UVM_DEFAULT)
`uvm_field_int (addr_b, UVM_DEFAULT)
`uvm_field_int (data_b, UVM_DEFAULT)
`uvm_object_utils_end
function new(string name = "switch_item");
super.new(name);
endfunction
endclass
UVM Driver
The UVM driver extends from uvm_driver
and is parameterized to accept an object of type switch_item . It also recieves handle to a virtual interface that is used to toggle pins of the DUT. The driver uses standard method calls get_next_item
and item_done
to communicate with its sequencer.
class driver extends uvm_driver #(switch_item);
`uvm_component_utils(driver)
function new(string name = "driver", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual switch_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual switch_if)::get(this, "", "switch_vif", vif))
`uvm_fatal("DRV", "Could not get vif")
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
switch_item m_item;
`uvm_info("DRV", $sformatf("Wait for item from sequencer"), UVM_LOW)
seq_item_port.get_next_item(m_item);
drive_item(m_item);
seq_item_port.item_done();
end
endtask
virtual task drive_item(switch_item m_item);
vif.vld <= 1;
vif.addr <= m_item.addr;
vif.data <= m_item.data;
@ (posedge vif.clk);
vif.vld <= 0;
endtask
endclass
UVM Monitor
The UVM monitor is derived from uvm_monitor
and has a virtual interface handle to listen to activity on the given interface. It tries to decode pin level activity into a protocol packet which it can send out to other testbench components. In this case, it tries to see if address and data pairs are available on the DUT interface, capture them into a class object of type switch_item and send out via an analysis port.
Since it has to decode and capture any activity happening on the DUT interface, it has to run for as long as the simulation is active and hence it is placed inside a forever
loop.
class monitor extends uvm_monitor;
`uvm_component_utils(monitor)
function new(string name="monitor", uvm_component parent=null);
super.new(name, parent);
endfunction
uvm_analysis_port #(switch_item) mon_analysis_port;
virtual switch_if vif;
semaphore sema4;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual switch_if)::get(this, "", "switch_vif", vif))
`uvm_fatal("MON", "Could not get vif")
sema4 = new(1);
mon_analysis_port = new ("mon_analysis_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
fork
sample_port("Thread0");
sample_port("Thread1");
join
endtask
virtual task sample_port(string tag="");
// This task monitors the interface for a complete
// transaction and pushes into the mailbox when the
// transaction is complete
forever begin
@(posedge vif.clk);
if (vif.rstn & vif.vld) begin
switch_item item = new;
sema4.get();
item.addr = vif.addr;
item.data = vif.data;
`uvm_info("MON", $sformatf("T=%0t [Monitor] %s First part over",
$time, tag), UVM_LOW)
@(posedge vif.clk);
sema4.put();
item.addr_a = vif.addr_a;
item.data_a = vif.data_a;
item.addr_b = vif.addr_b;
item.data_b = vif.data_b;
mon_analysis_port.write(item);
`uvm_info("MON", $sformatf("T=%0t [Monitor] %s Second part over, item:",
$time, tag), UVM_LOW)
item.print();
end
end
endtask
endclass
Note that every packet that is sent on its input port should be found on one of the output ports. The monitor will capture input port on every clock, and capture both output ports on every clock and send a complete packet to scoreboard. This makes a monitor slightly complicated. We will see an alternative strategy later that shifts the complexity to scoreboard instead.
UVM Agent
The role of a UVM agent is to encapsulate the sequencer, driver and monitor into a single container. The sequencer is responsible for acting on a given sequence and send the driver with data objects. It is parameterized to accept objects of type switch_item and its seq_item_port
is connected to the driver's seq_item_export
.
class agent extends uvm_agent;
`uvm_component_utils(agent)
function new(string name="agent", uvm_component parent=null);
super.new(name, parent);
endfunction
driver d0; // Driver handle
monitor m0; // Monitor handle
uvm_sequencer #(switch_item) s0; // Sequencer Handle
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
s0 = uvm_sequencer#(switch_item)::type_id::create("s0", this);
d0 = driver::type_id::create("d0", this);
m0 = monitor::type_id::create("m0", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
d0.seq_item_port.connect(s0.seq_item_export);
endfunction
endclass
UVM Scoreboard
The scoreboard receives a data object through its uvm_analysis_imp
port from the monitor. Since our monitor captures an entire transfer with both input and output data, implementation of the scoreboard becomes really simple.
Since we know from design specifications that addresses that is less than a specified value should go to output port A and those that are above it should go to port B, the checker logic can be implemented on that basis. As soon as the scoreboard receives an item, its write
method will be executed which in turn runs the checker and predicts the result.
class scoreboard extends uvm_scoreboard;
`uvm_component_utils(scoreboard)
function new(string name="scoreboard", uvm_component parent=null);
super.new(name, parent);
endfunction
uvm_analysis_imp #(switch_item, scoreboard) m_analysis_imp;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_analysis_imp = new("m_analysis_imp", this);
endfunction
virtual function write(switch_item item);
if (item.addr inside {[0:'h3f]}) begin
if (item.addr_a != item.addr | item.data_a != item.data)
`uvm_error("SCBD", $sformatf("ERROR! Mismatch addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", item.addr, item.data, item.addr_a, item.data_a))
else
`uvm_info("SCBD", $sformatf("PASS! Match addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", item.addr, item.data, item.addr_a, item.data_a), UVM_LOW)
end else begin
if (item.addr_b != item.addr | item.data_b != item.data)
`uvm_error("SCBD", $sformatf("ERROR! Mismatch addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", item.addr, item.data, item.addr_b, item.data_b))
else
`uvm_info("SCBD", $sformatf("PASS! Match addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", item.addr, item.data, item.addr_b, item.data_b), UVM_LOW)
end
endfunction
endclass
UVM Environment
The UVM environment is responsible for instantiating and connecting all testbench components. Here, analysis port from the agent's monitor is connected to the analysis implementation port in scoreboard. This connection allows the scoreboard to receive packets whenever the monitor pushes new data into the analysis port.
class env extends uvm_env;
`uvm_component_utils(env)
function new(string name="env", uvm_component parent=null);
super.new(name, parent);
endfunction
agent a0; // Agent handle
scoreboard sb0; // Scoreboard handle
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
a0 = agent::type_id::create("a0", this);
sb0 = scoreboard::type_id::create("sb0", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
a0.m0.mon_analysis_port.connect(sb0.m_analysis_imp);
endfunction
endclass
UVM Test
The test instantiates an environment, sets up virtual interface handles to sub components and starts a top level sequence. In this simple example, we have chosen to also reset the DUT using a reset task, after which a sequence of type gen_item_seq is started on the agent's sequencer.
Since the sequence consumes simulation time and all other testbench components have to run as long as the sequence is active, it is important to raise and drop objections appropriately.
class test extends uvm_test;
`uvm_component_utils(test)
function new(string name = "test", uvm_component parent=null);
super.new(name, parent);
endfunction
env e0;
virtual switch_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
e0 = env::type_id::create("e0", this);
if (!uvm_config_db#(virtual switch_if)::get(this, "", "switch_vif", vif))
`uvm_fatal("TEST", "Did not get vif")
uvm_config_db#(virtual switch_if)::set(this, "e0.a0.*", "switch_vif", vif);
endfunction
virtual task run_phase(uvm_phase phase);
gen_item_seq seq = gen_item_seq::type_id::create("seq");
phase.raise_objection(this);
apply_reset();
seq.randomize();
seq.start(e0.a0.s0);
phase.drop_objection(this);
endtask
virtual task apply_reset();
vif.rstn <= 0;
repeat(5) @ (posedge vif.clk);
vif.rstn <= 1;
repeat(10) @ (posedge vif.clk);
endtask
endclass
UVM Sequence
Last but not the least, we have the main sequence that forms the stimulus and randomizer aspect of the testbench. The sequence when started on a sequencer gets the body
method executed which ultimately randomizes address/data and send them to the driver.
class gen_item_seq extends uvm_sequence;
`uvm_object_utils(gen_item_seq)
function new(string name="gen_item_seq");
super.new(name);
endfunction
rand int num; // Config total number of items to be sent
constraint c1 { num inside {[2:5]}; }
virtual task body();
for (int i = 0; i < num; i ++) begin
switch_item m_item = switch_item::type_id::create("m_item");
start_item(m_item);
m_item.randomize();
`uvm_info("SEQ", $sformatf("Generate new item: "), UVM_LOW)
m_item.print();
finish_item(m_item);
end
`uvm_info("SEQ", $sformatf("Done generation of %0d items", num), UVM_LOW)
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test test... UVM_INFO testbench.sv(78) @ 0: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(49) @ 290: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: --------------------------------------------------------------------------- Name Type Size Value --------------------------------------------------------------------------- m_item switch_item - @2847 addr integral 8 'he6 data integral 16 'h9248 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'h0 data_b integral 16 'h0 begin_time time 64 290 depth int 32 'd2 parent sequence (name) string 3 seq parent sequence (full name) string 25 uvm_test_top.e0.a0.s0.seq sequencer string 21 uvm_test_top.e0.a0.s0 --------------------------------------------------------------------------- UVM_INFO testbench.sv(139) @ 310: uvm_test_top.e0.a0.m0 [MON] T=310 [Monitor] Thread0 First part over UVM_INFO testbench.sv(78) @ 310: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(49) @ 310: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: --------------------------------------------------------------------------- Name Type Size Value --------------------------------------------------------------------------- m_item switch_item - @2890 addr integral 8 'h88 data integral 16 'h8675 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'h0 data_b integral 16 'h0 begin_time time 64 310 depth int 32 'd2 parent sequence (name) string 3 seq parent sequence (full name) string 25 uvm_test_top.e0.a0.s0.seq sequencer string 21 uvm_test_top.e0.a0.s0 --------------------------------------------------------------------------- UVM_INFO testbench.sv(183) @ 330: uvm_test_top.e0.sb0 [SCBD] PASS! Match addr=0xe6 data=0x9248 addr_b=0xe6 data_b=0x9248 UVM_INFO testbench.sv(148) @ 330: uvm_test_top.e0.a0.m0 [MON] T=330 [Monitor] Thread0 Second part over, item: -------------------------------------- Name Type Size Value -------------------------------------- switch_item switch_item - @2911 addr integral 8 'he6 data integral 16 'h9248 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'he6 data_b integral 16 'h9248 -------------------------------------- UVM_INFO testbench.sv(78) @ 330: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(139) @ 330: uvm_test_top.e0.a0.m0 [MON] T=330 [Monitor] Thread1 First part over UVM_INFO testbench.sv(49) @ 330: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: --------------------------------------------------------------------------- Name Type Size Value --------------------------------------------------------------------------- m_item switch_item - @2928 addr integral 8 'hcc data integral 16 'h6ed0 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'h0 data_b integral 16 'h0 begin_time time 64 330 depth int 32 'd2 parent sequence (name) string 3 seq parent sequence (full name) string 25 uvm_test_top.e0.a0.s0.seq sequencer string 21 uvm_test_top.e0.a0.s0 --------------------------------------------------------------------------- UVM_INFO testbench.sv(183) @ 350: uvm_test_top.e0.sb0 [SCBD] PASS! Match addr=0x88 data=0x8675 addr_b=0x88 data_b=0x8675 UVM_INFO testbench.sv(148) @ 350: uvm_test_top.e0.a0.m0 [MON] T=350 [Monitor] Thread1 Second part over, item: -------------------------------------- Name Type Size Value -------------------------------------- switch_item switch_item - @2893 addr integral 8 'h88 data integral 16 'h8675 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'h88 data_b integral 16 'h8675 -------------------------------------- UVM_INFO testbench.sv(78) @ 350: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(139) @ 350: uvm_test_top.e0.a0.m0 [MON] T=350 [Monitor] Thread0 First part over UVM_INFO testbench.sv(49) @ 350: uvm_test_top.e0.a0.s0@@seq [SEQ] Generate new item: --------------------------------------------------------------------------- Name Type Size Value --------------------------------------------------------------------------- m_item switch_item - @2950 addr integral 8 'hb2 data integral 16 'h4b58 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'h0 data_b integral 16 'h0 begin_time time 64 350 depth int 32 'd2 parent sequence (name) string 3 seq parent sequence (full name) string 25 uvm_test_top.e0.a0.s0.seq sequencer string 21 uvm_test_top.e0.a0.s0 --------------------------------------------------------------------------- UVM_INFO testbench.sv(183) @ 370: uvm_test_top.e0.sb0 [SCBD] PASS! Match addr=0xcc data=0x6ed0 addr_b=0xcc data_b=0x6ed0 UVM_INFO testbench.sv(148) @ 370: uvm_test_top.e0.a0.m0 [MON] T=370 [Monitor] Thread0 Second part over, item: -------------------------------------- Name Type Size Value -------------------------------------- switch_item switch_item - @2872 addr integral 8 'hcc data integral 16 'h6ed0 addr_a integral 8 'h0 data_a integral 16 'h0 addr_b integral 8 'hcc data_b integral 16 'h6ed0 -------------------------------------- UVM_INFO testbench.sv(78) @ 370: uvm_test_top.e0.a0.d0 [DRV] Wait for item from sequencer UVM_INFO testbench.sv(139) @ 370: uvm_test_top.e0.a0.m0 [MON] T=370 [Monitor] Thread1 First part over UVM_INFO testbench.sv(53) @ 370: uvm_test_top.e0.a0.s0@@seq [SEQ] Done generation of 4 items UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 370: 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) @ 370: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---