Why Work Through a Full Example? Knowing what a driver, monitor and scoreboard are is different from watching them catch real bugs. This example follows a small design through five releases, and shows how the same UVM testbench finds a new bug in each one until the error count reaches zero.

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:

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

Full log for design v1.0

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:

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

Full log for design v1.1

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:

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

Full log for design v1.2

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:

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

Full log for design v1.3

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
Output
** 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.

Full log for design v1.4

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_db makes 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
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5