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

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

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5