UVM Testbench Architecture: Full Component Hierarchy

When you first open a real verification environment, the sheer number of classes can feel overwhelming: sequences, sequencers, drivers, monitors, agents, environments, scoreboards. But the UVM testbench architecture is not a random pile of classes. It is a deliberate layering that separates what you want to test (the stimulus intent) from how that stimulus reaches the pins, and separates driving the design from checking it. Once you see the layers, every class has an obvious home.

This page builds the architecture from first principles using one tiny example: an 8-bit adder DUT with inputs a, b and output result. We will trace a single transaction from its birth in a sequence all the way to the scoreboard that judges it, and label every hop along the way.

A note on running the code: the UVM examples below are written for a UVM-capable simulator such as industry-standard simulation tools, VCS, industry-standard simulators, or the free EDA Playground UVM flow. They depend on the UVM base-class library, so they will not compile under bare Icarus Verilog or Verilator. Treat them as canonical structure to read and adapt. Further down, a self-contained plain-SystemVerilog block illustrates the same data flow with no UVM library at all, and that one you can actually run.

Why the testbench is layered

A good testbench answers three independent questions, and UVM gives each its own layer so you can change one without disturbing the others:

  • What stimulus do I want? Expressed as transactions and sequences — abstract, protocol-level, reusable.
  • How does stimulus reach the DUT? Handled by the driver and a virtual interface, the only place that knows about clocks and pin wiggling.
  • Is the DUT correct? Decided by the monitor (which observes) and the scoreboard (which compares against a reference model).

Because these concerns are separated, the same adder sequence can run against an RTL model, a gate-level netlist, or a faster abstract model with zero changes. That reuse is the entire point of the architecture.

The component hierarchy

UVM components are built top-down and form a tree. The test sits at the root; the environment holds reusable blocks; each agent wraps the three classes that talk to one interface. Here is the hierarchy for our adder, from the outside in:

LevelComponentResponsibility
1Top moduleStatic world: instantiate DUT and interface, generate clock, call run_test().
2Test (uvm_test)Picks the environment configuration and which sequence to run.
3Environment (uvm_env)Holds the agent(s) and the scoreboard; wires analysis connections.
4Agent (uvm_agent)Bundles sequencer, driver and monitor for one interface.
5Sequencer / Driver / MonitorArbitrate, drive, and observe transactions.
6Sequence & transactionThe stimulus content itself (objects, not components).

Note the distinction in the last row: sequences and transactions are uvm_objects, not uvm_components. They are dynamic data that flows through the static component tree rather than being a permanent part of it.

Following one transaction: the data flow

This is the heart of the UVM testbench architecture. Read it as a relay race where a single alu_txn object is the baton:

  1. Sequence → sequencer. The sequence creates an alu_txn, randomizes it, and offers it with start_item/finish_item. The sequencer is a traffic controller: if several sequences compete, it arbitrates and lets one item through at a time.
  2. Sequencer → driver. The driver pulls the item with get_next_item(). It is the first component that knows the item is real and ready to be applied.
  3. Driver → DUT. The driver translates abstract fields into pin activity on the virtual interface, respecting the clock. For the adder it assigns a and b, waits a clock edge, then calls item_done() to release the sequencer for the next item.
  4. DUT → monitor. Independently of the driver, the monitor watches the same interface, samples a, b and result at the sampling point, and reconstructs them into a fresh transaction. The monitor never drives anything — it only observes.
  5. Monitor → scoreboard. The monitor broadcasts its captured transaction through an uvm_analysis_port. The scoreboard, subscribed via an uvm_analysis_imp, receives each one in its write() method, computes the expected sum with its own reference model, and reports a match or mismatch.

The driver and monitor are two separate paths deliberately: stimulus flows in through the driver, results flow out through the monitor, and the scoreboard sees only observed behaviour. If you ever let the scoreboard trust the driver’s intended values instead of the monitor’s observed values, a bug that corrupts the interface could pass silently.

Building the pieces in code

Below are the canonical UVM classes for the adder, in the order data flows through them.

Transaction and sequence

SystemVerilog
class alu_txn extends uvm_sequence_item;
  rand bit [7:0] a;
  rand bit [7:0] b;
  bit  [8:0] result;

  `uvm_object_utils_begin(alu_txn)
    `uvm_field_int(a,      UVM_ALL_ON)
    `uvm_field_int(b,      UVM_ALL_ON)
    `uvm_field_int(result, UVM_ALL_ON)
  `uvm_object_utils_end

  function new(string name = "alu_txn");
    super.new(name);
  endfunction
endclass

class add_sequence extends uvm_sequence #(alu_txn);
  `uvm_object_utils(add_sequence)

  function new(string name = "add_sequence");
    super.new(name);
  endfunction

  task body();
    repeat (8) begin
      alu_txn tr = alu_txn::type_id::create("tr");
      start_item(tr);
      assert(tr.randomize());
      finish_item(tr);
    end
  endtask
endclass

Driver

SystemVerilog
class alu_driver extends uvm_driver #(alu_txn);
  `uvm_component_utils(alu_driver)
  virtual alu_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual alu_if)::get(this, "", "vif", vif))
      `uvm_fatal("DRV", "virtual interface not set")
  endfunction

  task run_phase(uvm_phase phase);
    forever begin
      alu_txn tr;
      seq_item_port.get_next_item(tr);   // from sequencer
      vif.a <= tr.a;                      // drive DUT pins
      vif.b <= tr.b;
      @(posedge vif.clk);
      seq_item_port.item_done();
    end
  endtask
endclass

Monitor

SystemVerilog
class alu_monitor extends uvm_monitor;
  `uvm_component_utils(alu_monitor)
  virtual alu_if vif;
  uvm_analysis_port #(alu_txn) ap;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual alu_if)::get(this, "", "vif", vif))
      `uvm_fatal("MON", "virtual interface not set")
  endfunction

  task run_phase(uvm_phase phase);
    forever begin
      alu_txn tr = alu_txn::type_id::create("tr");
      @(posedge vif.clk);
      tr.a      = vif.a;        // observe only
      tr.b      = vif.b;
      tr.result = vif.result;
      ap.write(tr);            // broadcast to subscribers
    end
  endtask
endclass

Scoreboard

SystemVerilog
class alu_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(alu_scoreboard)
  uvm_analysis_imp #(alu_txn, alu_scoreboard) item_collected;
  int matched, mismatched;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    item_collected = new("item_collected", this);
  endfunction

  function void write(alu_txn tr);        // called by monitor's ap
    bit [8:0] expected = tr.a + tr.b;     // reference model
    if (tr.result === expected)
      matched++;
    else begin
      mismatched++;
      `uvm_error("SCB", $sformatf("a=%0d b=%0d got=%0d exp=%0d",
                 tr.a, tr.b, tr.result, expected))
    end
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SCB", $sformatf("matched=%0d mismatched=%0d",
              matched, mismatched), UVM_LOW)
  endfunction
endclass

Assembling the tree: agent, env, test, top

SystemVerilog
class alu_agent extends uvm_agent;
  `uvm_component_utils(alu_agent)
  alu_driver               drv;
  alu_monitor              mon;
  uvm_sequencer #(alu_txn) seqr;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    mon = alu_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      drv  = alu_driver::type_id::create("drv", this);
      seqr = uvm_sequencer#(alu_txn)::type_id::create("seqr", this);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE)
      drv.seq_item_port.connect(seqr.seq_item_export);
  endfunction
endclass

class alu_env extends uvm_env;
  `uvm_component_utils(alu_env)
  alu_agent      agt;
  alu_scoreboard scb;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agt = alu_agent::type_id::create("agt", this);
    scb = alu_scoreboard::type_id::create("scb", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    agt.mon.ap.connect(scb.item_collected);   // monitor -> scoreboard
  endfunction
endclass

class alu_test extends uvm_test;
  `uvm_component_utils(alu_test)
  alu_env env;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = alu_env::type_id::create("env", this);
  endfunction

  task run_phase(uvm_phase phase);
    add_sequence seq = add_sequence::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(env.agt.seqr);   // launch stimulus on the sequencer
    phase.drop_objection(this);
  endtask
endclass

module top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  bit clk;
  always #5 clk = ~clk;

  alu_if  intf(clk);
  alu_dut dut(.clk(intf.clk), .a(intf.a), .b(intf.b), .result(intf.result));

  initial begin
    uvm_config_db#(virtual alu_if)::set(null, "*", "vif", intf);
    run_test("alu_test");
  end
endmodule

Trace the connections and the whole architecture becomes one sentence: the test builds the env, the env builds an agent plus a scoreboard, the agent connects its driver to the sequencer and exposes its monitor’s analysis port, and the env connects that port to the scoreboard. Stimulus is poured in at seq.start(env.agt.seqr); verdicts come out at report_phase.

A runnable illustration without the UVM library

To make the data flow concrete on a free simulator, here is the same idea stripped of UVM entirely. A real adder DUT is instantiated; the stimulus list plays the role of a sequence, the pin assignments play the driver, the sampling plays the monitor, and the reference function plays the scoreboard. This compiles and runs under Icarus Verilog or Verilator.

SystemVerilog
module dut (
  input  logic [7:0] a,
  input  logic [7:0] b,
  output logic [8:0] sum
);
  assign sum = a + b;
endmodule

module tb;
  logic [7:0] a, b;
  logic [8:0] sum;
  int errors = 0;

  dut u_dut (.a(a), .b(b), .sum(sum));

  // SCOREBOARD reference model
  function automatic logic [8:0] ref_add(input logic [7:0] x, y);
    return x + y;
  endfunction

  // SEQUENCE: the stimulus content
  logic [7:0] stim_a [4];
  logic [7:0] stim_b [4];

  initial begin
    stim_a = '{8'd10, 8'd200, 8'd255, 8'd1};
    stim_b = '{8'd5,  8'd100, 8'd255, 8'd254};

    foreach (stim_a[i]) begin
      // SEQUENCER -> DRIVER: hand over one transaction
      a = stim_a[i];
      b = stim_b[i];
      #1;                    // DRIVER drives pins, DUT settles

      // MONITOR samples, SCOREBOARD compares
      if (sum !== ref_add(a, b)) begin
        errors++;
        $display("FAIL  a=%0d b=%0d dut=%0d exp=%0d", a, b, sum, ref_add(a, b));
      end else begin
        $display("PASS  a=%0d b=%0d sum=%0d", a, b, sum);
      end
    end

    if (errors == 0)
      $display("SCOREBOARD: all %0d transactions matched", $size(stim_a));
    else
      $display("SCOREBOARD: %0d mismatches", errors);
    $finish;
  end
endmodule
PASS  a=10 b=5 sum=15
PASS  a=200 b=100 sum=300
PASS  a=255 b=255 sum=510
PASS  a=1 b=254 sum=255
SCOREBOARD: all 4 transactions matched
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp4knv8do9/tb.sv:48: $finish called at 4 (1s)

Each labelled comment maps to a UVM role. When you later replace the fixed list with a randomized add_sequence, the inline pin assignments with an alu_driver, and the if check with an alu_scoreboard fed by a monitor, you have grown this flat testbench into the full layered architecture — same data path, now reusable and scalable.

Quick check

Why are sequences and transactions objects rather than components?

They are dynamic data that is created, used, and discarded as stimulus flows through the testbench. Components (uvm_component) form the permanent, hierarchical structure that exists for the whole simulation, while transactions (uvm_object) are the baton that travels through that structure.

What is the sequencer’s job between the sequence and the driver?

It arbitrates. When multiple sequences request access, the sequencer decides which transaction is handed to the driver next, enforcing one-item-at-a-time delivery through the get_next_item/item_done handshake.

Why must the scoreboard receive data from the monitor, not the driver?

The monitor reports what actually appeared on the interface, while the driver only knows what it intended to send. Checking observed values catches bugs that corrupt the signals between intent and pins; trusting the driver would let such bugs pass silently.

Which UVM connection carries the monitor’s captured transactions to the scoreboard?

An analysis connection: the monitor publishes through a uvm_analysis_port and the scoreboard subscribes with a uvm_analysis_imp, whose write() method is invoked for every broadcast transaction. The env wires them together in connect_phase.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD