This UVM verification testbench example verifies a Verilog pattern detector from start to finish: the design specification, a short test plan, every testbench component, and the debug loop between the designer and the verification engineer. The detector watches a serial input and raises out for one clock each time the last four inputs were 1011.
9 min read | Intermediate Level
What You'll Learn
- Build a complete UVM testbench for a small sequential design
- Write a scoreboard that predicts the expected output from the input stream
- Make a testbench reusable by putting the pattern in the configuration database
- Read scoreboard errors to find the exact state machine bug
Design Specification
The design, det_1011, gets one new bit on its in port every clock. When the most recent four bits form the pattern 1011, its out port goes high for one clock. Patterns may overlap, so the stream 1011011 contains two matches that share the middle 1.
The designer implements this as a five-state machine: IDLE, S1, S10, S101 and S1011, where each name records how much of the pattern has been seen so far. out is 1 only in S1011.
The diagram shows the correct, final machine. The orange transitions are the ones the first release got wrong. Each was found by the testbench and fixed in a later version, as the rest of this article shows.
Test Plan
The input stream is random, with ones more likely than zeros, so that long runs of ones and many overlapping matches appear without writing directed tests. The stimulus must cover cases such as these, where the highlighted part contains one or more matches:
- 011011011010 - two overlapping matches
- 101011100 - a match followed by zeros
- 111011011 - a match preceded by extra ones
The testbench has the usual structure: a sequence generates the bits, a driver applies them, a monitor observes both pins, and a scoreboard checks the output.
Testbench
Sequence item
One item is one clock's worth of data. in is random with an 80% chance of being 1. out is filled in only by the monitor.
class Item extends uvm_sequence_item;
`uvm_object_utils(Item)
rand bit in;
bit out;
virtual function string convert2str();
return $sformatf("in=%0d, out=%0d", in, out);
endfunction
function new(string name = "Item");
super.new(name);
endfunction
constraint c1 { in dist {0:/20, 1:/80}; }
endclass
Sequence
The sequence sends num items. The soft constraint gives a short default run of 10 to 50 items that a test can override.
class gen_item_seq extends uvm_sequence #(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 { soft num inside {[10:50]}; }
virtual task body();
for (int i = 0; i < num; i ++) begin
Item m_item = 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: %s", m_item.convert2str()), UVM_HIGH)
finish_item(m_item);
end
`uvm_info("SEQ", $sformatf("Done generation of %0d items", num), UVM_LOW)
endtask
endclass
Driver
The driver gets each item from the sequencer and drives it onto the interface through the clocking block, one item per clock.
class driver extends uvm_driver #(Item);
`uvm_component_utils(driver)
function new(string name = "driver", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual des_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual des_if)::get(this, "", "des_vif", vif))
`uvm_fatal("DRV", "Could not get vif")
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
Item m_item;
`uvm_info("DRV", $sformatf("Wait for item from sequencer"), UVM_HIGH)
seq_item_port.get_next_item(m_item);
drive_item(m_item);
seq_item_port.item_done();
end
endtask
virtual task drive_item(Item m_item);
@(vif.cb);
vif.cb.in <= m_item.in;
endtask
endclass
Monitor
On every clock after reset, the monitor builds a new item with the current input and the output sampled by the clocking block, and writes it to its analysis port.
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 #(Item) mon_analysis_port;
virtual des_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual des_if)::get(this, "", "des_vif", vif))
`uvm_fatal("MON", "Could not get vif")
mon_analysis_port = new ("mon_analysis_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
@ (vif.cb);
if (vif.rstn) begin
Item item = Item::type_id::create("item");
item.in = vif.in;
item.out = vif.cb.out;
mon_analysis_port.write(item);
`uvm_info("MON", $sformatf("Saw item %s", item.convert2str()), UVM_HIGH)
end
end
endtask
endclass
Scoreboard
The scoreboard is a reference model. It shifts each input bit into act_pattern and compares it with ref_pattern. Because the design's output rises one clock after the fourth bit, the scoreboard first checks the output against the expectation from the previous clock, and then computes a new expectation for the next clock.
class scoreboard extends uvm_scoreboard;
`uvm_component_utils(scoreboard)
function new(string name="scoreboard", uvm_component parent=null);
super.new(name, parent);
endfunction
bit [`LENGTH-1:0] ref_pattern;
bit [`LENGTH-1:0] act_pattern;
bit exp_out;
uvm_analysis_imp #(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);
if (!uvm_config_db#(bit[`LENGTH-1:0])::get(this, "", "ref_pattern", ref_pattern))
`uvm_fatal("SCBD", "Did not get ref_pattern !")
endfunction
virtual function void write(Item item);
act_pattern = act_pattern << 1 | item.in;
`uvm_info("SCBD", $sformatf("in=%0d out=%0d ref=0b%0b act=0b%0b",
item.in, item.out, ref_pattern, act_pattern), UVM_LOW)
// Check this clock's output against last clock's expectation
if (item.out != exp_out) begin
`uvm_error("SCBD", $sformatf("ERROR ! out=%0d exp=%0d", item.out, exp_out))
end else begin
`uvm_info("SCBD", $sformatf("PASS ! out=%0d exp=%0d", item.out, exp_out), UVM_HIGH)
end
// A full match now means out must be 1 on the next clock
if (!(ref_pattern ^ act_pattern)) begin
`uvm_info("SCBD", $sformatf("Pattern found to match, next out should be 1"), UVM_LOW)
exp_out = 1;
end else begin
exp_out = 0;
end
endfunction
endclass
Agent and environment
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 #(Item) s0;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
s0 = uvm_sequencer#(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
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
Tests
To reuse the environment for other patterns, the pattern is an N-bit value whose width comes from the LENGTH macro, and the test passes it to the scoreboard through uvm_config_db. The base test also forwards the virtual interface, applies reset and starts the sequence. A derived test only has to set a pattern and the number of items.
`define LENGTH 4
class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name = "base_test", uvm_component parent=null);
super.new(name, parent);
endfunction
env e0;
bit [`LENGTH-1:0] pattern = 4'b1011;
gen_item_seq seq;
virtual des_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
e0 = env::type_id::create("e0", this);
// Get the interface from tb and pass it to the agent's components
if (!uvm_config_db#(virtual des_if)::get(this, "", "des_vif", vif))
`uvm_fatal("TEST", "Did not get vif")
uvm_config_db#(virtual des_if)::set(this, "e0.a0.*", "des_vif", vif);
// Make the pattern visible to everything below the test
uvm_config_db#(bit[`LENGTH-1:0])::set(this, "*", "ref_pattern", pattern);
seq = gen_item_seq::type_id::create("seq");
if (!seq.randomize()) `uvm_fatal("TEST", "Sequence randomization failed")
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
apply_reset();
seq.start(e0.a0.s0);
#200;
phase.drop_objection(this);
endtask
virtual task apply_reset();
vif.rstn <= 0;
vif.in <= 0;
repeat(5) @ (posedge vif.clk);
vif.rstn <= 1;
repeat(10) @ (posedge vif.clk);
endtask
endclass
class test_1011 extends base_test;
`uvm_component_utils(test_1011)
function new(string name="test_1011", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
pattern = 4'b1011;
super.build_phase(phase);
if (!seq.randomize() with { num inside {[300:500]}; })
`uvm_fatal("TEST", "Sequence randomization failed")
endfunction
endclass
test_1011 sets pattern before calling super.build_phase(), because the base class puts it into the configuration database there. Its inline constraint on num wins over the soft default in the sequence.
Interface and top module
interface des_if (input bit clk);
logic rstn;
logic in;
logic out;
clocking cb @(posedge clk);
default input #1step output #3ns;
input out;
output in;
endclocking
endinterface
module tb;
reg clk;
always #10 clk =~ clk;
des_if _if (clk);
det_1011 u0 ( .clk(clk),
.rstn(_if.rstn),
.in(_if.in),
.out(_if.out));
initial begin
clk <= 0;
uvm_config_db#(virtual des_if)::set(null, "uvm_test_top", "des_vif", _if);
run_test("test_1011");
end
endmodule
Design v1.0
The first release of the design:
module det_1011 ( input clk,
input rstn,
input in,
output out );
parameter IDLE = 0,
S1 = 1,
S10 = 2,
S101 = 3,
S1011 = 4;
reg [2:0] cur_state, next_state;
assign out = cur_state == S1011 ? 1 : 0;
always @ (posedge clk) begin
if (!rstn)
cur_state <= IDLE;
else
cur_state <= next_state;
end
always @ (cur_state or in) begin
case (cur_state)
IDLE : begin
if (in) next_state = S1;
else next_state = IDLE;
end
S1: begin
if (in) next_state = IDLE;
else next_state = S10;
end
S10 : begin
if (in) next_state = S101;
else next_state = IDLE;
end
S101 : begin
if (in) next_state = S1011;
else next_state = IDLE;
end
S1011: begin
next_state = IDLE;
end
endcase
end
endmodule
Running test_1011 on it produces 19 errors. Here is one of them:
...
UVM_INFO testbench.sv(149) @ 350: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b111
UVM_INFO testbench.sv(149) @ 370: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1111
UVM_INFO testbench.sv(149) @ 390: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
UVM_INFO testbench.sv(149) @ 410: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 430: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 430: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 450: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b111
UVM_ERROR testbench.sv(156) @ 450: uvm_test_top.e0.sb0 [SCBD] ERROR ! out=0 exp=1
UVM_INFO testbench.sv(149) @ 470: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
...
** Report counts by severity
UVM_INFO : 527
UVM_WARNING : 0
UVM_ERROR : 19
UVM_FATAL : 0
** Report counts by id
[RNTST] 1
[SCBD] 542
[SEQ] 1
[TEST_DONE] 1
[UVM/RELNOTES] 1
The act column is the scoreboard's view of the last four inputs. Before the error, the input was a run of ones followed by 0 1 1. In v1.0, a second 1 in state S1 sends the machine back to IDLE, so the run of ones keeps resetting it and the final 1011 is never recognised. The fix is to stay in S1, because the newest 1 is still a valid start of the pattern.
Design v1.1
The designer changes the S1 branch:
S1: begin
if (in) next_state = S1; // Fix for bug found in v1.0
else next_state = S10;
end
The same test now reports 16 errors:
...
UVM_INFO testbench.sv(149) @ 950: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1111
UVM_INFO testbench.sv(149) @ 970: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
UVM_INFO testbench.sv(149) @ 990: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 1010: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1010
UVM_INFO testbench.sv(149) @ 1030: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b101
UVM_INFO testbench.sv(149) @ 1050: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 1050: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 1070: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b111
UVM_ERROR testbench.sv(156) @ 1070: uvm_test_top.e0.sb0 [SCBD] ERROR ! out=0 exp=1
UVM_INFO testbench.sv(149) @ 1090: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
...
** Report counts by severity
UVM_INFO : 527
UVM_WARNING : 0
UVM_ERROR : 16
UVM_FATAL : 0
** Report counts by id
[RNTST] 1
[SCBD] 539
[SEQ] 1
[TEST_DONE] 1
[UVM/RELNOTES] 1
This time the input was 1 0 1 0 1 1. The machine reaches S101, then sees a 0 and drops to IDLE. But the last two bits, 1 0, are themselves the start of the pattern, so the machine should go to S10 instead.
Design v1.2
S101 : begin
if (in) next_state = S1011;
else next_state = S10; // Fix for bug found in v1.1
end
Six errors remain:
...
UVM_INFO testbench.sv(149) @ 4130: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 4150: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 4150: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 4170: uvm_test_top.e0.sb0 [SCBD] in=1 out=1 ref=0b1011 act=0b111
UVM_INFO testbench.sv(149) @ 4190: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
UVM_INFO testbench.sv(149) @ 4210: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 4230: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 4230: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 4250: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b110
UVM_ERROR testbench.sv(156) @ 4250: uvm_test_top.e0.sb0 [SCBD] ERROR ! out=0 exp=1
UVM_INFO testbench.sv(149) @ 4270: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
...
** Report counts by severity
UVM_INFO : 527
UVM_WARNING : 0
UVM_ERROR : 6
UVM_FATAL : 0
** Report counts by id
[RNTST] 1
[SCBD] 529
[SEQ] 1
[TEST_DONE] 1
[UVM/RELNOTES] 1
The first match at 4150 is caught correctly: out is 1 at 4170. The second match, which overlaps the first through the 1 at 4170, is missed. In S1011 the machine always returns to IDLE, throwing that 1 away. With in = 1 it should go to S1.
Design v1.3
S1011: begin
if (in) next_state = S1; // Fix for bug found in v1.2
else next_state = IDLE;
end
Three errors are left:
...
UVM_INFO testbench.sv(149) @ 4190: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b1110
UVM_INFO testbench.sv(149) @ 4210: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 4230: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 4230: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 4250: uvm_test_top.e0.sb0 [SCBD] in=0 out=1 ref=0b1011 act=0b110
UVM_INFO testbench.sv(149) @ 4270: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
UVM_INFO testbench.sv(149) @ 4290: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1011
UVM_INFO testbench.sv(166) @ 4290: uvm_test_top.e0.sb0 [SCBD] Pattern found to match, next out should be 1
UVM_INFO testbench.sv(149) @ 4310: uvm_test_top.e0.sb0 [SCBD] in=0 out=0 ref=0b1011 act=0b110
UVM_ERROR testbench.sv(156) @ 4310: uvm_test_top.e0.sb0 [SCBD] ERROR ! out=0 exp=1
UVM_INFO testbench.sv(149) @ 4330: uvm_test_top.e0.sb0 [SCBD] in=1 out=0 ref=0b1011 act=0b1101
...
** Report counts by severity
UVM_INFO : 527
UVM_WARNING : 0
UVM_ERROR : 3
UVM_FATAL : 0
** Report counts by id
[RNTST] 1
[SCBD] 526
[SEQ] 1
[TEST_DONE] 1
[UVM/RELNOTES] 1
Now the stream is 1011011 with the overlap on a 0: after the match at 4230, the next bit is 0. The last two bits are then 1 0, a partial match, but the machine goes to IDLE. The correct next state is S10.
Design v1.4
The final design, with all four fixes:
module det_1011 ( input clk,
input rstn,
input in,
output out );
parameter IDLE = 0,
S1 = 1,
S10 = 2,
S101 = 3,
S1011 = 4;
reg [2:0] cur_state, next_state;
assign out = cur_state == S1011 ? 1 : 0;
always @ (posedge clk) begin
if (!rstn)
cur_state <= IDLE;
else
cur_state <= next_state;
end
always @ (cur_state or in) begin
case (cur_state)
IDLE : begin
if (in) next_state = S1;
else next_state = IDLE;
end
S1: begin
if (in) next_state = S1; // Fix for bug found in v1.0
else next_state = S10;
end
S10 : begin
if (in) next_state = S101;
else next_state = IDLE;
end
S101 : begin
if (in) next_state = S1011;
else next_state = S10; // Fix for bug found in v1.1
end
S1011: begin
if (in) next_state = S1; // Fix for bug found in v1.2
else next_state = S10; // Fix for bug found in v1.3
end
endcase
end
endmodule
** Report counts by severity
UVM_INFO : 527
UVM_WARNING : 0
UVM_ERROR : 0
UVM_FATAL : 0
** Report counts by id
[RNTST] 1
[SCBD] 523
[SEQ] 1
[TEST_DONE] 1
[UVM/RELNOTES] 1
The error count is finally zero for this seed. That is not yet sign-off: the same test should now pass across hundreds of random seeds. In practice, the designer would also run the testbench before each release, which avoids most of these round trips.

Quick Check
Q: Why does the scoreboard check out before updating exp_out, instead of after?
Show Answer
The design raises out one clock after the last bit of the pattern arrives. The value checked on each clock must therefore be the expectation computed on the previous clock. Updating first would compare the output with a prediction for the next clock and report false errors.
Common Mistakes
Mistake #1: Declaring write() without a void return type
Without void, write() becomes a function that returns a 1-bit value. The analysis imp calls it as a plain statement, which many simulators flag with a warning, and the return value is meaningless. Always declare it as function void write(...).
Mistake #2: Setting a test field after super.build_phase()
If test_1011 assigned pattern after calling super.build_phase(), the base test would already have stored the old value in the configuration database, and the scoreboard would check the wrong pattern.
Mistake #3: Stopping at zero errors on one seed
Random stimulus only covers what that seed happened to generate. Run many seeds, and add functional coverage on the states and transitions, before calling the design verified.
Summary
In this article, you learned:
- A complete UVM testbench for a serial pattern detector, from sequence item to top module
- How a scoreboard models the design and accounts for its one-clock output latency
- How a pattern stored in
uvm_config_dbmakes the environment reusable - How each scoreboard error points to a specific missing state transition
- Why one passing seed is only the start of sign-off