This UVM testbench example verifies a small packet switch that routes each incoming packet to one of two output ports based on its address. The same design is verified with a plain class-based testbench in SystemVerilog Testbench Example 2, so the two can be compared side by side.
The interesting part of this example is the monitor. The design needs a clock to move a packet from input to output, so the monitor uses two threads that take turns to capture a complete input-plus-output transfer while new packets keep arriving every clock.
8 min read | Intermediate Level
What You'll Learn
- Build a complete UVM testbench for a two-port packet switch
- Write a monitor that pairs each input with its output one clock later
- Check routing in a scoreboard using the design's address split
- Keep the run phase alive long enough to check the last packet
The Design
The design, switch, accepts an 8-bit address and 16-bit data whenever vld is high. On the next clock edge it copies the packet to port A if the address is 0x3F or less, and to port B otherwise, clearing the other port to zero. When vld is low the outputs simply hold their last values.
A new packet can arrive on every clock, so at any moment one packet may be at the input while the previous one is at an output. The testbench has to keep track of both.
Sequence Item
switch_item holds the random input address and data, plus fields for what came out of port A and port B. Because each packet leaves through exactly one port, a single object can carry the whole transfer from input to output, which keeps the scoreboard simple.
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
Driver
The protocol has no handshake, so the driver is short. It puts the packet on the bus with vld high, waits one clock edge, and drops vld. If the next item is already waiting, the driver puts it on the bus in the same time step, so vld stays high and packets go out back to back.
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
Monitor
Capturing one transfer takes two clock edges: the input is sampled on the first, and the output on the second. With a new packet arriving every clock, a single loop would miss every other packet. The monitor therefore runs two copies of sample_port() in parallel and uses a semaphore with one key to make them take turns.
The thread holding the key samples the input. On the next edge it hands the key back and samples the outputs, while the other thread takes the key and samples the next input. The check on vld after get() stops a thread that has been waiting from capturing an idle bus as a packet.
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="");
forever begin
@(posedge vif.clk);
if (vif.rstn & vif.vld) begin
switch_item item = new;
sema4.get();
if (!vif.vld) begin // bus went idle while this thread waited
sema4.put();
continue;
end
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
This puts the timing complexity in the monitor so that the scoreboard receives complete transfers. A common alternative is to keep the monitor simple, sending input packets and output packets separately, possibly through two analysis ports, and let the scoreboard match them up. That scales better when a design can delay or reorder packets.
Agent
The agent groups the sequencer, driver and monitor, and connects the driver's seq_item_port to the sequencer'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;
monitor m0;
uvm_sequencer #(switch_item) s0;
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
Scoreboard
Because each item already contains both the input and the outputs, the scoreboard only has to apply the routing rule from the specification. An address of 0x3F or less must appear on port A, anything above it on port B, with the same data.
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 void 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
This check could be stricter: it does not confirm that the other port was cleared to zero. Adding that comparison would catch a design that drives a packet out of both ports.
Environment
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;
scoreboard sb0;
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
Test
The test builds the environment, passes the virtual interface to the agent, resets the design and runs one sequence between raise_objection() and drop_objection(). This is the version that produced the log below. It has a flaw that the log makes visible, discussed after it.
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();
if (!seq.randomize()) `uvm_fatal("TEST", "Sequence randomization failed")
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
Sequence
The sequence sends between 2 and 5 random packets and prints each one as it is generated.
class gen_item_seq extends uvm_sequence #(switch_item);
`uvm_object_utils(gen_item_seq)
function new(string name="gen_item_seq");
super.new(name);
endfunction
rand int num; // Total number of items to send
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);
if (!m_item.randomize()) `uvm_error("SEQ", "Randomization failed")
`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
Interface and Top Module
interface switch_if (input bit clk);
logic rstn;
logic vld;
logic [7:0] addr;
logic [15:0] data;
logic [7:0] addr_a;
logic [15:0] data_a;
logic [7:0] addr_b;
logic [15:0] data_b;
endinterface
module tb;
reg clk;
always #10 clk =~ clk;
switch_if _if (clk);
switch u0 ( .clk (clk),
.rstn (_if.rstn),
.addr (_if.addr),
.data (_if.data),
.vld (_if.vld),
.addr_a (_if.addr_a),
.data_a (_if.data_a),
.addr_b (_if.addr_b),
.data_b (_if.data_b));
initial begin
clk <= 0;
uvm_config_db#(virtual switch_if)::set(null, "uvm_test_top", "switch_vif", _if);
run_test("test");
end
endmodule
Simulation Results
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 ---
The sequence generated four packets, all with addresses above 0x3F, so all of them belong on port B. The monitor messages alternate between Thread0 and Thread1 exactly as in the diagram, and the first three packets (e6, 88, cc) each produce a PASS one clock after their input was sampled.
The fourth packet, b2, is different. Thread1 samples its input at 370, but in that same time step the sequence finishes, the test drops its objection, and the run phase ends. The output edge at 390 never happens, so b2 is never checked. A bug that only affected the last packet of a test would go unnoticed.
Fixing the end of the test
The run phase must stay open until everything in flight has been checked. The simplest fix is a short delay before dropping the objection. A cleaner one is a drain time on the phase's objection, which delays the end of the phase after the last objection is dropped:
virtual task run_phase(uvm_phase phase);
gen_item_seq seq = gen_item_seq::type_id::create("seq");
phase.raise_objection(this);
phase.get_objection().set_drain_time(this, 40); // two clocks of slack
apply_reset();
if (!seq.randomize()) `uvm_fatal("TEST", "Sequence randomization failed")
seq.start(e0.a0.s0);
phase.drop_objection(this);
endtask
With this change the output edge for b2 is reached and its result is reported before the phase ends.
Quick Check
Q: Without the vld check after sema4.get(), what would a waiting thread capture if the driver sent a single packet followed by an idle bus?
Show Answer
Both threads see vld on the packet's edge, but only one gets the key. The other wakes up a clock later, after the bus has gone idle, and captures the stale address as a new packet. Because the outputs hold their last values, that phantom packet would even pass the scoreboard check.
Common Mistakes
Mistake #1: Dropping the objection the moment the sequence ends
The sequence finishes when the last item has been driven, not when it has been checked. Add a drain time, or wait for the scoreboard to see every expected packet, before letting the run phase end.
Mistake #2: Monitoring a pipelined design with a single loop
If a transfer takes two edges to observe and packets arrive on every edge, one loop that waits for the output will miss every other input. Use parallel threads as shown, or send inputs and outputs separately and match them in the scoreboard.
Mistake #3: Checking only the port that should be active
The scoreboard here never looks at the port that should be idle. A routing bug that copies a packet to both ports would pass. Check both outputs on every transfer.
Summary
In this article, you learned:
- A complete UVM testbench for a two-port packet switch, from sequence item to top module
- How two monitor threads and a semaphore capture overlapping input/output transfers
- How a scoreboard checks routing with the design's address split
- Why the run phase needs a drain time so the last packet is checked
- Where a simple scoreboard can be made stricter