Day 10 / 25
← Day 09 Day 11 →
HomeVerificationDay 10 — UVM Scoreboard
Track 2 — UVM Core

UVM Scoreboard

By EcrioniX · Updated Jun 22, 2026

The scoreboard is the intelligence of your testbench — it compares what the DUT actually produces against what a reference model says it should produce. Master uvm_scoreboard, analysis_imp, the write() callback mechanism, queue-based reference models, and error reporting across an entire test run.

⏰ 28 min read📖 Day 10 of 25🎯 Scoreboard · analysis_imp · Reference Model
Sequencer Driver DUT Monitor Scoreboard analysis_imp write() ref model queue seq_item pin drive observe ap.write()
Contents
  1. What Is a Scoreboard?
  2. uvm_scoreboard Base Class
  3. analysis_imp and the write() Callback
  4. Reference Model — Queue-Based Pattern
  5. Expected vs Actual Comparison
  6. Error Counting in check_phase
  7. Multiple Analysis Ports with uvm_analysis_imp_decl
  8. Complete FIFO Scoreboard Example
  9. Common Pitfalls
  10. FAQ

1. What Is a Scoreboard?

In every functional verification environment, someone has to answer the question: did the DUT produce the right output? That job belongs to the scoreboard. The scoreboard sits at the end of the analysis network — it receives fully reconstructed transactions from one or more monitors, feeds them through an internal reference model to compute what the answer should be, then compares that prediction against what the DUT actually produced.

The scoreboard is completely passive from the DUT's perspective. It never drives any signals. It is purely a checking component. This means it can be enabled or disabled without changing DUT behavior, and it can be reused across different tests that exercise the same protocol.

RoleWho performs itPhase
Generate stimulusSequence + Sequencerrun_phase
Drive DUT pinsDriverrun_phase
Observe DUT outputsMonitorrun_phase
Check correctnessScoreboardrun_phase + check_phase
Measure coverageCoverage collectorrun_phase + report_phase

A well-designed scoreboard has three internal layers: a reference model that computes expected outputs, a comparison engine that matches expected against actual, and an error tracker that accumulates and reports mismatches. This page builds each layer from first principles.

2. uvm_scoreboard Base Class

uvm_scoreboard is a thin base class that extends uvm_component. It adds no new methods — its value is purely semantic. By inheriting from it instead of directly from uvm_component, your class is clearly identified as a scoreboard in the UVM component hierarchy, and tools like coverage dashboards and linting checkers can find and instrument it automatically.

// Minimal scoreboard skeleton
class fifo_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(fifo_scoreboard)

  // Analysis imp -- monitor connects here to deliver observed items
  uvm_analysis_imp #(fifo_seq_item, fifo_scoreboard) ap;

  // Internal counters
  int transactions_checked = 0;
  int error_count          = 0;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);   // instantiate the analysis imp
  endfunction

  // write() is called automatically whenever monitor calls ap.write(item)
  function void write(fifo_seq_item item);
    // compare item against reference model
  endfunction

  function void check_phase(uvm_phase phase);
    if (error_count > 0)
      `uvm_error("SCOREBOARD",
        $sformatf("%0d errors detected out of %0d transactions",
                  error_count, transactions_checked))
    else
      `uvm_info("SCOREBOARD",
        $sformatf("PASS -- all %0d transactions matched", transactions_checked),
        UVM_LOW)
  endfunction
endclass

The key insight: uvm_analysis_imp is parameterized with both the transaction type and the scoreboard type. UVM uses the scoreboard type parameter to know which class implements the write() method it will call. This is a static binding through parameterization, not runtime polymorphism.

3. analysis_imp and the write() Callback

The uvm_analysis_imp is UVM's subscriber port. When a monitor holds a uvm_analysis_port and calls port.write(item), UVM automatically invokes the write(item) method on every component that has connected a uvm_analysis_imp to that port. The scoreboard does not need to poll, wait, or use threads for this — the call arrives synchronously in simulation time.

// In the monitor (producer side)
class fifo_monitor extends uvm_monitor;
  `uvm_component_utils(fifo_monitor)

  uvm_analysis_port #(fifo_seq_item) ap;   // analysis PORT (output)

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);
  endfunction

  task run_phase(uvm_phase phase);
    fifo_seq_item item;
    forever begin
      collect_item(item);   // sample DUT outputs
      ap.write(item);       // broadcast to all subscribers
    end
  endtask
endclass

// In the environment -- connect monitor's port to scoreboard's imp
function void connect_phase(uvm_phase phase);
  mon.ap.connect(sb.ap);   // one line wires the entire data path
endfunction
write() is a function, not a task: The analysis write() method must be declared as a function, never a task. This is because uvm_analysis_port.write() is itself a function, and functions cannot call tasks in SystemVerilog. All processing inside write() must be zero-time.

The connection happens in connect_phase of the environment, which always runs after build_phase. UVM guarantees that all components are built before any connections are made, so you can safely call .connect() without worrying about null pointer dereferences on either side.

ConceptProducer side (Monitor)Consumer side (Scoreboard)
Port typeuvm_analysis_portuvm_analysis_imp
Methodport.write(item) — broadcastswrite(item) — receives
Blocking?No — returns immediatelyNo — must be zero-time function
SubscribersMany (broadcast)One write() per imp
Phase createdbuild_phasebuild_phase
Connectedconnect_phase in parent environment

4. Reference Model — Queue-Based Pattern

The reference model is the software replica of the DUT's intended behavior. It receives the same stimulus as the DUT (either by observing the input monitor or by computing from the sequence item itself) and predicts what the DUT output should be. The most common and robust implementation uses a SystemVerilog queue as a FIFO of expected values.

// Single-imp scoreboard pattern where the transaction carries a direction field
class fifo_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(fifo_scoreboard)

  // Queue that holds predicted output data in order of arrival
  fifo_seq_item exp_q[$];   // expected-output queue

  uvm_analysis_imp #(fifo_seq_item, fifo_scoreboard) ap;

  int checked = 0, errors = 0;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);
  endfunction

  // Reference model -- given input item, predict the DUT output
  function fifo_seq_item predict_output(fifo_seq_item in_item);
    fifo_seq_item pred;
    pred = fifo_seq_item::type_id::create("pred");
    // A FIFO passes data unchanged -- only ordering is verified
    pred.data  = in_item.data;
    pred.valid = 1;
    return pred;
  endfunction

  function void write(fifo_seq_item item);
    fifo_seq_item expected;
    if (item.direction == WRITE) begin
      // Input side: push a prediction onto the expected queue
      expected = predict_output(item);
      exp_q.push_back(expected);
    end else begin
      // Output side: pop prediction and compare
      compare_item(item);
    end
  endfunction
endclass
Clone before queuing: Always call $cast(clone, item.clone()) before pushing an item onto the expected queue. The monitor reuses the same object handle across cycles. If you store the raw handle, the queue will contain multiple references to the same (now overwritten) object — one of the most common scoreboard bugs.

Why a queue instead of a single variable?

Real DUTs have pipeline latency. The DUT may accept input on cycle 5 and produce output on cycle 8 (three cycles of pipeline delay). If you store only one expected value, you will overwrite it when the next input arrives on cycle 6 before cycle 8's output is compared. A queue naturally models any pipeline depth: inputs push to the back, outputs pop from the front. The queue length at any moment equals the number of in-flight transactions.

5. Expected vs Actual Comparison

The comparison function receives an observed output item from the monitor and a predicted item from the front of the expected queue, then field-by-field checks whether they match. Using `uvm_error (not `uvm_fatal) allows the test to continue after a mismatch and accumulate the total error count for the entire run.

// Compare an actual DUT output item against the expected prediction
function void compare_item(fifo_seq_item actual);
  fifo_seq_item expected;

  // Guard: if queue is empty, we got more outputs than inputs
  if (exp_q.size() == 0) begin
    `uvm_error("SCOREBOARD",
      $sformatf("Unexpected output: data=0x%0h -- expected queue is empty",
                actual.data))
    errors++;
    return;
  end

  expected = exp_q.pop_front();   // FIFO order -- oldest prediction first
  checked++;

  // Use !== (4-state) not != (2-state) to catch X values
  if (actual.data !== expected.data) begin
    `uvm_error("SCOREBOARD",
      $sformatf("DATA MISMATCH: expected=0x%0h  actual=0x%0h",
                expected.data, actual.data))
    errors++;
  end

  if (actual.valid !== expected.valid) begin
    `uvm_error("SCOREBOARD",
      $sformatf("VALID MISMATCH: expected=%0b  actual=%0b",
                expected.valid, actual.valid))
    errors++;
  end

  if (errors == 0)
    `uvm_info("SCOREBOARD",
      $sformatf("[%0d] MATCH: data=0x%0h", checked, actual.data),
      UVM_HIGH)
endfunction
Use !== not != for 4-state comparison: In SystemVerilog, != returns X if either operand contains X or Z bits. This can silently pass a check when the DUT produces X (uninitialized) output. Always use !== (case inequality) in scoreboard comparisons so that X values are treated as mismatches, not matches.

For complex sequence items with many fields, leverage the do_compare() method if your item inherits from uvm_object and you have implemented the field macros. You can call expected.compare(actual) which uses the registered field list automatically. This is cleaner for large transaction types but gives less granular error messages per field.

// Using uvm_object built-in compare() -- requires field macros in seq item
function void compare_item(fifo_seq_item actual);
  fifo_seq_item expected;
  if (exp_q.size() == 0) begin
    `uvm_error("SB", "Output with empty expected queue"); errors++; return;
  end
  expected = exp_q.pop_front(); checked++;

  if (!expected.compare(actual)) begin
    `uvm_error("SB",
      $sformatf("Transaction %0d mismatch:\nExpected: %s\nActual:   %s",
                checked, expected.sprint(), actual.sprint()))
    errors++;
  end
endfunction

6. Error Counting in check_phase

The check_phase runs after run_phase drains. By this time all stimulus has been sent and all DUT responses should have been observed. The scoreboard uses check_phase for two things: reporting cumulative error counts, and checking for missing responses — transactions that entered the DUT but never came out.

function void check_phase(uvm_phase phase);
  // 1. Check for leftover items in the expected queue (missing DUT outputs)
  if (exp_q.size() > 0) begin
    `uvm_error("SCOREBOARD",
      $sformatf("%0d expected outputs were never observed from DUT",
                exp_q.size()))
    errors += exp_q.size();
  end

  // 2. Guard: nothing checked at all (likely a connect_phase wiring bug)
  if (checked == 0) begin
    `uvm_error("SCOREBOARD",
      "No transactions checked -- scoreboard may be disconnected")
  end

  // 3. Final pass/fail summary
  if (errors > 0)
    `uvm_error("SCOREBOARD",
      $sformatf("*** TEST FAILED: %0d/%0d transactions had errors ***",
                errors, checked))
  else
    `uvm_info("SCOREBOARD",
      $sformatf("*** TEST PASSED: all %0d transactions matched ***", checked),
      UVM_NONE)
endfunction

The check_phase call to `uvm_error (not `uvm_fatal) is deliberate. `uvm_fatal immediately terminates the simulation. If you call it in check_phase and there are multiple failures, you only see the first one. Using `uvm_error lets all failure messages print, and the UVM test infrastructure will mark the test as FAILED if any `uvm_error was triggered during the run.

MacroSeveritySimulation continues?Best used in
`uvm_infoInformationalYesSuccessful match messages
`uvm_warningWarning (non-fatal)YesUnexpected but recoverable conditions
`uvm_errorError (increments error count)Yes (by default)Mismatches, missing responses
`uvm_fatalFatal (stops simulation)NoConfiguration failures in build_phase

7. Multiple Analysis Ports with uvm_analysis_imp_decl

A real design often has separate monitors for the input interface and the output interface. The scoreboard needs to receive from both — input monitor observations go to the reference model, output monitor observations go to the comparator. A single uvm_analysis_imp cannot be instantiated twice in the same class because both would require a write() method, and SystemVerilog does not allow method overloading.

The solution is the `uvm_analysis_imp_decl macro. It generates a new imp class with a custom suffix, and UVM routes calls to the correspondingly suffixed method in your scoreboard:

// Step 1: Declare two new imp types at package scope (NOT inside a class)
`uvm_analysis_imp_decl(_expected)   // creates uvm_analysis_imp_expected
`uvm_analysis_imp_decl(_actual)     // creates uvm_analysis_imp_actual

class fifo_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(fifo_scoreboard)

  // Step 2: Declare one instance of each imp type
  uvm_analysis_imp_expected #(fifo_seq_item, fifo_scoreboard) expected_ap;
  uvm_analysis_imp_actual   #(fifo_seq_item, fifo_scoreboard) actual_ap;

  fifo_seq_item exp_q[$];
  int checked = 0, errors = 0;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    expected_ap = new("expected_ap", this);
    actual_ap   = new("actual_ap",   this);
  endfunction

  // Step 3: Implement write_expected() -- called when input monitor writes
  function void write_expected(fifo_seq_item item);
    fifo_seq_item clone;
    $cast(clone, item.clone());   // always clone before storing
    exp_q.push_back(clone);
    `uvm_info("SB", $sformatf("Queued expected: data=0x%0h", clone.data), UVM_HIGH)
  endfunction

  // Step 4: Implement write_actual() -- called when output monitor writes
  function void write_actual(fifo_seq_item item);
    compare_item(item);
  endfunction

  function void compare_item(fifo_seq_item actual);
    fifo_seq_item expected;
    if (exp_q.size() == 0) begin
      `uvm_error("SB", "Actual output with no expected item in queue")
      errors++; return;
    end
    expected = exp_q.pop_front(); checked++;
    if (actual.data !== expected.data) begin
      `uvm_error("SB",
        $sformatf("[%0d] MISMATCH exp=0x%0h got=0x%0h",
                  checked, expected.data, actual.data))
      errors++;
    end
  endfunction

  function void check_phase(uvm_phase phase);
    if (exp_q.size() > 0) begin
      `uvm_error("SB", $sformatf("%0d expected outputs never seen", exp_q.size()))
      errors += exp_q.size();
    end
    if (errors > 0)
      `uvm_error("SB", $sformatf("FAIL: %0d errors in %0d checks", errors, checked))
    else
      `uvm_info("SB", $sformatf("PASS: %0d checks all matched", checked), UVM_NONE)
  endfunction
endclass

// In environment connect_phase -- wire both monitors to the scoreboard
function void connect_phase(uvm_phase phase);
  in_mon.ap.connect(sb.expected_ap);   // input monitor -> expected queue
  out_mon.ap.connect(sb.actual_ap);    // output monitor -> comparator
endfunction
Macro scope matters: The `uvm_analysis_imp_decl macro must appear outside any class or module body — at package or file scope. Placing it inside the class will cause a compile error because it tries to define a new class, which is not legal inside another class definition.

8. Complete FIFO Scoreboard Example

This section brings everything together into a self-contained FIFO scoreboard. The DUT is a synchronous FIFO. The input monitor observes push operations; the output monitor observes pop operations. The scoreboard uses the dual-imp pattern and a reference queue to verify that data comes out in exactly the order it went in.

// ============================================================
// fifo_seq_item -- transaction object
// ============================================================
class fifo_seq_item extends uvm_sequence_item;
  `uvm_object_utils_begin(fifo_seq_item)
    `uvm_field_int(data,  UVM_ALL_ON)
    `uvm_field_int(valid, UVM_ALL_ON)
  `uvm_object_utils_end

  rand logic [7:0] data;
  logic            valid;

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

  function string convert2string();
    return $sformatf("data=0x%02h valid=%0b", data, valid);
  endfunction
endclass

// ============================================================
// Imp declarations -- must be at package scope
// ============================================================
`uvm_analysis_imp_decl(_push)
`uvm_analysis_imp_decl(_pop)

// ============================================================
// fifo_scoreboard
// ============================================================
class fifo_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(fifo_scoreboard)

  uvm_analysis_imp_push #(fifo_seq_item, fifo_scoreboard) push_imp;
  uvm_analysis_imp_pop  #(fifo_seq_item, fifo_scoreboard) pop_imp;

  // Reference model -- holds data values in push order
  logic [7:0] ref_q[$];

  int push_count = 0;
  int pop_count  = 0;
  int errors     = 0;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    push_imp = new("push_imp", this);
    pop_imp  = new("pop_imp",  this);
  endfunction

  // Called by input monitor each time data is pushed into FIFO
  function void write_push(fifo_seq_item item);
    ref_q.push_back(item.data);
    push_count++;
    `uvm_info("SB_PUSH",
      $sformatf("[%0d] PUSH  data=0x%02h  q_depth=%0d",
                push_count, item.data, ref_q.size()),
      UVM_HIGH)
  endfunction

  // Called by output monitor each time data is popped from FIFO
  function void write_pop(fifo_seq_item item);
    logic [7:0] exp_data;
    pop_count++;

    if (ref_q.size() == 0) begin
      `uvm_error("SB_POP",
        $sformatf("[%0d] POP with empty reference queue! data=0x%02h",
                  pop_count, item.data))
      errors++; return;
    end

    exp_data = ref_q.pop_front();

    if (item.data !== exp_data) begin
      `uvm_error("SB_POP",
        $sformatf("[%0d] MISMATCH  expected=0x%02h  actual=0x%02h",
                  pop_count, exp_data, item.data))
      errors++;
    end else begin
      `uvm_info("SB_POP",
        $sformatf("[%0d] MATCH     data=0x%02h", pop_count, item.data),
        UVM_HIGH)
    end
  endfunction

  function void check_phase(uvm_phase phase);
    if (ref_q.size() > 0) begin
      `uvm_error("SB",
        $sformatf("%0d items pushed but never popped (FIFO stuck?)",
                  ref_q.size()))
      errors += ref_q.size();
    end
    if (push_count !== pop_count) begin
      `uvm_error("SB",
        $sformatf("Push count (%0d) != Pop count (%0d)",
                  push_count, pop_count))
      errors++;
    end
    if (errors > 0)
      `uvm_error("SB",
        $sformatf("FAIL: %0d errors | push=%0d pop=%0d",
                  errors, push_count, pop_count))
    else
      `uvm_info("SB",
        $sformatf("PASS: %0d push/pop pairs all matched", pop_count),
        UVM_NONE)
  endfunction
endclass

// ============================================================
// Environment -- wiring scoreboard into the testbench
// ============================================================
class fifo_env extends uvm_env;
  `uvm_component_utils(fifo_env)

  fifo_agent      agent;
  fifo_scoreboard sb;

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

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = fifo_agent::type_id::create("agent", this);
    sb    = fifo_scoreboard::type_id::create("sb",  this);
  endfunction

  function void connect_phase(uvm_phase phase);
    agent.push_mon.ap.connect(sb.push_imp);   // input monitor -> push_imp
    agent.pop_mon.ap.connect(sb.pop_imp);     // output monitor -> pop_imp
  endfunction
endclass

9. Common Pitfalls

Pitfall 1: Not cloning items before queuing

The most common scoreboard bug. The monitor calls ap.write(item) where item is the same object handle reused every cycle. If the scoreboard stores item directly, all queue entries point to the same object and will reflect the last observed values, not the values at the time of capture.

// WRONG -- stores handle to monitor's reused object
exp_q.push_back(item);

// CORRECT -- stores an independent copy of the item at this moment
fifo_seq_item cloned;
$cast(cloned, item.clone());
exp_q.push_back(cloned);

Pitfall 2: Using `uvm_fatal for mismatches

Using `uvm_fatal inside write() stops the simulation at the first mismatch. You miss all subsequent errors. Use `uvm_error to count mismatches and let the test run to completion. Reserve `uvm_fatal for infrastructure failures in build_phase (missing virtual interfaces, config_db failures).

Pitfall 3: Scoreboard not connected (silent false pass)

If the connect_phase wiring is wrong — wrong port name, wrong component handle — the scoreboard's write() is never called, checked stays at zero, and the test shows a false pass. Always add the zero-check guard in check_phase: if checked == 0, raise an error.

Pitfall 4: Wrong comparison operator (== vs ===)

Using != instead of !== can produce X results instead of 1/0 when DUT outputs contain X. An X result from != evaluates as false in an if statement, meaning the mismatch goes undetected. Always use !== for 4-state correctness in verification code.

Pitfall 5: Phase ordering — checks in report_phase instead of check_phase

The UVM convention is that check_phase is specifically for correctness checks and report_phase is for printing statistics. The UVM infrastructure watches error counts set during check_phase to determine the final test status. Checks done only in report_phase may not affect the pass/fail result depending on the simulator's UVM library version.

Day 10 Key Takeaways

10. FAQ

What is a UVM scoreboard?

A UVM scoreboard is a component that checks the DUT's output correctness by comparing what the DUT actually produced against what a reference model predicted. It extends uvm_scoreboard, connects to monitors via uvm_analysis_imp, receives observed transactions through write() callbacks, and uses check_phase to report final pass/fail counts. The scoreboard is the single source of truth for functional correctness in a UVM testbench.

What is uvm_analysis_imp and how does it differ from uvm_analysis_port?

uvm_analysis_imp is the subscriber end of a UVM analysis connection. When a monitor calls analysis_port.write(item), UVM routes that call to the write() method of every connected uvm_analysis_imp. The uvm_analysis_port is the producer (monitor side); uvm_analysis_imp is the consumer (scoreboard/coverage side). The scoreboard declares one uvm_analysis_imp per incoming data stream and implements a matching write() method for each.

How do you handle multiple monitors feeding one scoreboard?

Use the `uvm_analysis_imp_decl macro to declare uniquely named analysis imp types. For example, `uvm_analysis_imp_decl(_expected) and `uvm_analysis_imp_decl(_actual) create two distinct imp classes. The scoreboard then declares one instance of each type and implements both write_expected() and write_actual() methods. UVM routes each analysis_port.write() call to the correct method based on the imp type it is connected to.

When should scoreboard checks happen — write() or check_phase?

Both patterns are valid and commonly combined. Immediate checking in write() catches errors as they occur and provides precise error context (cycle number, transaction values). check_phase is used for final summary reporting: checking that queues are empty (no missing responses), printing total error counts, and calling `uvm_error if the test should fail. Avoid calling `uvm_fatal in write() because it aborts the test immediately and you lose later error data.

Next Up — Day 11
UVM RAL (Register Abstraction Layer)
uvm_reg, uvm_reg_block, reg_adapter, frontdoor vs backdoor access, and a complete register file verification example.