UVM Scoreboard: Comparing Expected vs Actual Transactions

In any constrained-random testbench, driving stimulus is only half the job. The other half is deciding, automatically, whether the design did the right thing. That decision lives in the UVM scoreboard — the component that collects transactions observed on the design under test (DUT), works out what the correct result should have been, and compares the two. If you have ever wondered how a UVM environment “knows” a test passed without a human reading every waveform, the answer is almost always a scoreboard quietly comparing expected versus actual transactions.

A quick, honest note on running these examples: the UVM blocks below depend on the UVM base-class library, which free simulators like Icarus Verilog and Verilator do not fully support. They are written for a UVM-capable simulator (industry-standard simulation tools, VCS, industry-standard simulators) or EDA Playground, and are shown here as canonical, syntax-correct structure you can paste into such a tool. The one runnable block near the end is plain SystemVerilog — no UVM library — so it executes on a free simulator and demonstrates the same comparison idea.

What a UVM scoreboard actually does

A scoreboard sits at the top of the verification environment, usually inside the uvm_env, alongside one or more agents. It is a passive, analysis-level component: it never drives pins. Instead it subscribes to the broadcasts that monitors publish. Its responsibilities are three:

  • Collect every transaction the monitor sees on the DUT interface.
  • Predict the correct behaviour, either by recomputing a result with a reference model or by receiving a predicted transaction from a separate predictor.
  • Compare expected against actual, and flag any mismatch through the UVM reporting system.

Because it is decoupled from the driver and sequencer, the same scoreboard works whether you run one directed test or ten thousand random ones.

The three pieces: uvm_scoreboard, uvm_analysis_imp, write()

Three names do most of the work, and it helps to keep them straight:

ElementRole
uvm_scoreboardBase class you extend. It is a thin specialisation of uvm_component that marks the component’s intent; the comparison logic is yours to write.
uvm_analysis_impAn analysis “implementation” port. The monitor has an analysis port; the scoreboard has an analysis imp. Connecting them lets the monitor push transactions into the scoreboard.
write()The callback the analysis imp invokes. Every time the monitor broadcasts a transaction, UVM calls your write() exactly once with that transaction.

The key mental model: an analysis port is a one-to-many broadcaster, and each subscriber exposes an analysis imp whose write() runs on every broadcast. You do not call write() yourself — the connection does.

Step 1: the transaction

Expected-versus-actual comparison only makes sense if the transaction carries both the stimulus and the observed response. For a simple 8-bit adder, that means the operands and the DUT’s result.

class alu_txn extends uvm_sequence_item;
  rand bit [7:0] a;
  rand bit [7:0] b;
  bit  [8:0] result;            // captured from the DUT by the monitor

  `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

Step 2: the scoreboard and its write() method

The simplest useful scoreboard uses a single analysis imp. Because the transaction carries the operands, the scoreboard can recompute the expected result itself — its reference model is a single addition. The write() method is where the comparison happens.

class alu_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(alu_scoreboard)

  // One analysis imp. Its write() runs for every item the monitor broadcasts.
  uvm_analysis_imp #(alu_txn, alu_scoreboard) ap_imp;

  int num_pass;
  int num_fail;

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

  // Must be named 'write': uvm_analysis_imp calls exactly this method.
  function void write(alu_txn actual);
    bit [8:0] expected;
    expected = actual.a + actual.b;          // reference model
    if (actual.result === expected) begin
      num_pass++;
      `uvm_info("SB", $sformatf("PASS a=%0d b=%0d result=%0d",
                actual.a, actual.b, actual.result), UVM_HIGH)
    end
    else begin
      num_fail++;
      `uvm_error("SB", $sformatf("FAIL a=%0d b=%0d exp=%0d got=%0d",
                actual.a, actual.b, expected, actual.result))
    end
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf("Totals: %0d passed, %0d failed",
              num_pass, num_fail), UVM_NONE)
  endfunction
endclass

Two details matter. First, the method must be called write — the analysis imp is hard-wired to that name. Second, a mismatch is reported with `uvm_error, which increments UVM’s error count so the test ends with a non-zero status. Use `uvm_info with a low verbosity for passes so clean runs stay quiet.

Step 3: connecting the monitor to the scoreboard

The wiring lives in connect_phase of the environment. The monitor’s analysis port connects to the scoreboard’s analysis imp.

class alu_env extends uvm_env;
  `uvm_component_utils(alu_env)

  alu_agent      agt;
  alu_scoreboard sb;

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

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

  function void connect_phase(uvm_phase phase);
    // monitor's analysis PORT  ->  scoreboard's analysis IMP
    agt.mon.item_collected_port.connect(sb.ap_imp);
  endfunction
endclass

When one write() is not enough: analysis_imp_decl

The single-imp pattern works when the scoreboard can predict the answer from the observed inputs. But many designs need two independent streams — for example a predicted stream from a reference model and an actual stream from the DUT monitor — and each stream needs its own callback. The problem: a class can hold only one method literally named write.

UVM solves this with the `uvm_analysis_imp_decl macro, which generates a specialised imp whose callback is write_<suffix>.

// Generate two specialised imps, each with its own write method
`uvm_analysis_imp_decl(_expected)
`uvm_analysis_imp_decl(_actual)

class alu_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(alu_scoreboard)

  uvm_analysis_imp_expected #(alu_txn, alu_scoreboard) exp_imp;
  uvm_analysis_imp_actual   #(alu_txn, alu_scoreboard) act_imp;

  alu_txn exp_q[$];            // expected items waiting to be matched

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

  // Predictor broadcasts land here
  function void write_expected(alu_txn t);
    exp_q.push_back(t);
  endfunction

  // DUT monitor broadcasts land here
  function void write_actual(alu_txn actual);
    alu_txn exp;
    if (exp_q.size() == 0) begin
      `uvm_error("SB", "Actual arrived with no matching expected item")
      return;
    end
    exp = exp_q.pop_front();
    if (exp.result !== actual.result)
      `uvm_error("SB", $sformatf("MISMATCH exp=%0d act=%0d",
                 exp.result, actual.result))
    else
      `uvm_info("SB", "MATCH", UVM_HIGH)
  endfunction
endclass

Here a queue (exp_q) decouples the two streams in time — essential for pipelined DUTs, where an expected item may be produced several cycles before the matching actual appears. The two styles cover most cases: recompute-in-place for simple blocks, dual-imp with a queue when a separate predictor or latency is involved.

Expected vs actual: in-order or out-of-order?

StrategyWhen to use
In-order queue (FIFO compare)DUT preserves transaction order. Pop the oldest expected item and compare. Simple and fast.
Associative array / key matchDUT reorders items (out-of-order completion, tagged responses). Store expected items keyed by an ID and look up on arrival.

The same idea, running without UVM

To see the comparison engine work on a free simulator, here is a scoreboard stripped of the UVM library. It keeps the essentials: a write() entry point, a reference model that predicts the sum, and an expected-versus-actual check. The third transaction carries an injected bug so you can watch a mismatch be caught.

module tb;
  // A minimal scoreboard for an adder DUT, written WITHOUT the UVM library
  // so it runs on a free simulator. It shows the core idea: a write()
  // entry point that compares each ACTUAL result against the EXPECTED one.

  // Expected transactions, filled by a reference model (one queue per field)
  int exp_a[$];
  int exp_b[$];
  int exp_sum[$];

  int pass_count = 0;
  int fail_count = 0;

  // write(): called once per ACTUAL transaction coming from the monitor.
  task automatic write(int a, int b, int sum);
    int ea, eb, es;
    if (exp_sum.size() == 0) begin
      $display("[SB] ERROR unexpected actual a=%0d b=%0d sum=%0d", a, b, sum);
      fail_count++;
      return;
    end
    ea = exp_a.pop_front();
    eb = exp_b.pop_front();
    es = exp_sum.pop_front();
    if (es === sum)
      begin
        $display("[SB] PASS  a=%0d b=%0d exp_sum=%0d act_sum=%0d", a, b, es, sum);
        pass_count++;
      end
    else
      begin
        $display("[SB] FAIL  a=%0d b=%0d exp_sum=%0d act_sum=%0d", a, b, es, sum);
        fail_count++;
      end
  endtask

  // reference model: what the adder SHOULD produce
  task automatic expect_add(int a, int b);
    exp_a.push_back(a);
    exp_b.push_back(b);
    exp_sum.push_back(a + b);
  endtask

  initial begin
    // Reference model predicts each result
    expect_add(2, 3);
    expect_add(10, 7);
    expect_add(4, 4);

    // DUT feeds its ACTUAL outputs to the scoreboard via write()
    write(2, 3, 5);    // correct
    write(10, 7, 17);  // correct
    write(4, 4, 9);    // injected bug: 4+4 should be 8

    $display("[SB] SUMMARY: %0d passed, %0d failed", pass_count, fail_count);
    $finish;
  end
endmodule


[SB] PASS a=2 b=3 exp_sum=5 act_sum=5
[SB] PASS a=10 b=7 exp_sum=17 act_sum=17
[SB] FAIL a=4 b=4 exp_sum=8 act_sum=9
[SB] SUMMARY: 2 passed, 1 failed
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmptmdhtuhx/tb.sv:56: $finish called at 0 (1s)

[SB] PASS  a=2 b=3 exp_sum=5 act_sum=5
[SB] PASS  a=10 b=7 exp_sum=17 act_sum=17
[SB] FAIL  a=4 b=4 exp_sum=8 act_sum=9
[SB] SUMMARY: 2 passed, 1 failed

The structure maps one-to-one onto the UVM version: write() is the analysis-imp callback, the expect_add helper is the predictor, and the queues are the scoreboard’s memory of what it still expects to see. Swap the queues for UVM transactions and the broadcast for an analysis port, and you have the real thing.

Common mistakes to avoid

  • Misnaming the callback. A single uvm_analysis_imp calls write — not write_item or receive. For extra streams, use `uvm_analysis_imp_decl and the matching write_<suffix>.
  • Reporting a pass as an error, or a fail silently. Route mismatches through `uvm_error so the test’s pass/fail status is honest.
  • Forgetting the connect. If connect_phase never links the monitor port to the scoreboard imp, write() is never called and every test “passes” — because nothing was ever checked.
  • No end-of-test check. If expected items remain in the queue when simulation ends, the DUT dropped responses. Verify the queue is empty in check_phase or report_phase.

Quick check

Why must the scoreboard’s callback be named exactly write()?

Because uvm_analysis_imp is hard-coded to invoke a method called write on the component it belongs to. When the connected analysis port broadcasts a transaction, it calls write() on every subscriber. Rename it and the imp has nothing to call, so your comparison logic never runs.

What is the difference between an analysis port and an analysis imp?

An analysis port (on the monitor) is the broadcaster — it sends one transaction out to many listeners. An analysis imp (on the scoreboard) is the receiver that implements write(). In connect_phase you connect the monitor’s port to the scoreboard’s imp so broadcasts reach the callback.

How do you give a scoreboard two separate input streams?

Use the `uvm_analysis_imp_decl(_suffix) macro to generate specialised imps such as uvm_analysis_imp_expected and uvm_analysis_imp_actual. Each gets its own callback — write_expected() and write_actual() — so a predictor stream and a DUT stream can arrive independently.

Why keep expected transactions in a queue instead of comparing immediately?

Because the expected result is often known before the DUT produces the actual one, especially in pipelined designs. The queue stores expected items until the matching actual arrives, letting you compare in order even when there is latency between prediction and observation.

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