UVM-18: Scoreboards & Predictors — VLSI Trainers
VLSI Trainers UVM Series · UVM-18
UVM Series · UVM-18

Scoreboards & Predictors

The complete scoreboard architecture — prediction vs evaluation separation, the predictor (golden reference model), in-order and out-of-order comparison, implementing a complete scoreboard with phase_ready_to_end, error reporting, and the parameterised comparator pattern.

📋 Scoreboard Architecture

The best scoreboard architecture separates two distinct concerns: prediction (what should happen) and evaluation (did it happen). Keeping them separate makes each part independently testable, reusable across environments, and easy to swap out when the DUT specification changes.

Scoreboard Architecture — Prediction and Evaluation Separated Sequences send stimulus DUT receives stimulus generates response Monitor observes DUT builds actual txn stimulus txn (from monitor.ap) actual txn Predictor (golden reference model) expected_ap.write(pred_txn) expected txn Comparator get expected + actual txn.compare() each pair PASS / FAIL Scoreboard (env component) vlsitrainers.com
Figure 1 — Scoreboard architecture with prediction and evaluation separated. The predictor receives the same stimulus that the DUT received (from the monitor’s analysis port on the input side), computes the expected response, and sends it to the comparator. The comparator also receives the actual DUT response from the output monitor and compares each pair.

Two common scoreboard topologies exist depending on what the monitor can observe:

TopologyMonitorsWhat comparator receivesTypical use
Prediction modelInput monitor (stimulus) + output monitor (response)Expected from predictor + actual from output monitorProtocol bus verification — DUT transforms APB writes into register state changes
Self-checkingSingle monitor observes complete transactionsTwo streams from same monitor — reference computed inline from transaction fieldsSimple DUTs where expected result is computed directly (ALU, CRC, checksum)

📋 The Predictor

The predictor (also called the golden reference model) receives stimulus transactions from the monitor and computes what the DUT’s response should be. It writes the predicted response into its own analysis port, which the comparator subscribes to.

The predictor must faithfully implement the DUT specification — it is the ground truth against which the DUT is measured. If the predictor has a bug, the scoreboard will either miss real DUT bugs or flag false failures.

class gpio_predictor extends uvm_subscriber #(apb_seq_item);
  `uvm_component_utils(gpio_predictor)

  // Analysis port: sends predicted response to comparator
  uvm_analysis_port #(apb_seq_item) expected_ap;

  // Internal model — mirror of DUT register state
  logic [31:0] reg_shadow [0:4];  // mirrors DUT register bank

  function new(string name, uvm_component parent);
    super.new(name, parent);
    foreach (reg_shadow[i]) reg_shadow[i] = 32'h0;
  endfunction

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

  // write() called when stimulus monitor broadcasts a transaction
  function void write(apb_seq_item stimulus);
    apb_seq_item predicted;
    int reg_idx = stimulus.addr >> 2;

    predicted = apb_seq_item::type_id::create("predicted");
    predicted.addr           = stimulus.addr;
    predicted.read_not_write = stimulus.read_not_write;

    if (!stimulus.read_not_write) begin
      // Write: update shadow register, predict zero error
      foreach (stimulus.byte_en[i])
        if (stimulus.byte_en[i])
          reg_shadow[reg_idx][i*8 +: 8] = stimulus.write_data[i*8 +: 8];
      predicted.error = 0;
    end else begin
      // Read: predict register contents, zero error
      predicted.read_data = reg_shadow[reg_idx];
      predicted.error     = 0;
    end

    expected_ap.write(predicted);   // send to comparator
  endfunction
endclass
The predictor and the scoreboard must receive transactions from the same monitor instance. If the predictor subscribes to the input monitor (which sees what was driven) and the comparator subscribes to the output monitor (which sees what the DUT returned), they must be correctly wired — a common bug is subscribing both to the same monitor, causing the predictor to predict based on the output rather than the input.

📋 In-Order Comparator

An in-order comparator assumes that transactions from the expected and actual streams arrive in the same order. The first expected is compared to the first actual, the second expected to the second actual, and so on. This is the correct model for protocols that process transactions in strict FIFO order (APB, I2C, SPI).

The cleanest implementation uses two tlm_analysis_fifo instances — one for expected, one for actual — and a run_phase task that gets one from each and compares:

In-Order Comparator — Two FIFO Queues, One Pair at a Time Predictor.ap expected_fifo Monitor.ap actual_fifo run_phase fifo.get(exp) + fifo.get(act) exp.compare(act) → PASS/FAIL PASS / FAIL + report vlsitrainers.com
Figure 2 — In-order comparator using two analysis FIFOs. The predictor writes expected transactions into expected_fifo; the monitor writes actual transactions into actual_fifo. The run_phase task gets one from each (blocking) and compares them. The FIFOs handle timing differences — if the predictor runs faster than the monitor, expected transactions accumulate in the FIFO until the actuals arrive.
`uvm_analysis_imp_decl(_expected)
`uvm_analysis_imp_decl(_actual)

class gpio_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(gpio_scoreboard)

  // Two analysis inputs
  uvm_analysis_imp_expected #(apb_seq_item, gpio_scoreboard) expected_export;
  uvm_analysis_imp_actual   #(apb_seq_item, gpio_scoreboard) actual_export;

  // Internal FIFOs — decouple write() from comparison
  tlm_analysis_fifo #(apb_seq_item) m_expected_fifo;
  tlm_analysis_fifo #(apb_seq_item) m_actual_fifo;

  int m_matches    = 0;
  int m_mismatches = 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_export  = new("expected_export", this);
    actual_export    = new("actual_export",   this);
    m_expected_fifo  = new("m_expected_fifo", this);
    m_actual_fifo    = new("m_actual_fifo",   this);
  endfunction

  // write_expected: push into expected FIFO
  function void write_expected(apb_seq_item t);
    m_expected_fifo.write(t);
  endfunction

  // write_actual: push into actual FIFO
  function void write_actual(apb_seq_item t);
    m_actual_fifo.write(t);
  endfunction

  task run_phase(uvm_phase phase);
    apb_seq_item exp_txn, act_txn;
    forever begin
      // Block until one of each arrives
      m_expected_fifo.get(exp_txn);
      m_actual_fifo.get(act_txn);
      compare(exp_txn, act_txn);
    end
  endtask

  function void compare(apb_seq_item exp, apb_seq_item act);
    if (exp.compare(act)) begin
      m_matches++;
      `uvm_info("SB", $sformatf("MATCH ✓  %s", act.convert2string()), UVM_HIGH)
    end else begin
      m_mismatches++;
      `uvm_error("SB", $sformatf(
        "MISMATCH!\n  expected: %s\n  actual:   %s",
        exp.convert2string(), act.convert2string()))
    end
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "Scoreboard: %0d matches, %0d mismatches",
      m_matches, m_mismatches), UVM_LOW)
    if (m_mismatches > 0)
      `uvm_error("SB", "TEST FAILED — mismatches found")
    else
      `uvm_info("SB", "TEST PASSED", UVM_LOW)
  endfunction
endclass

📋 Out-of-Order Comparator

An out-of-order comparator is needed when the DUT may reorder transactions — for example, a DMA controller that services multiple channels and may complete requests in a different order than they were issued. Instead of comparing first-to-first, it searches the expected queue for a match to each actual transaction received.

class ooo_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(ooo_scoreboard)

  uvm_analysis_imp_expected #(my_txn, ooo_scoreboard) expected_export;
  uvm_analysis_imp_actual   #(my_txn, ooo_scoreboard) actual_export;

  // Expected stored in an associative queue — not FIFO
  my_txn expected_q[$];
  int m_matches = 0, m_mismatches = 0, m_missing = 0;

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

  function void write_expected(my_txn t);
    expected_q.push_back(t);    // enqueue — order does not matter
  endfunction

  function void write_actual(my_txn actual);
    // Search queue for a matching expected transaction
    foreach (expected_q[i]) begin
      if (expected_q[i].compare(actual)) begin
        m_matches++;
        expected_q.delete(i);   // remove matched entry
        `uvm_info("SB", "MATCH ✓", UVM_HIGH)
        return;
      end
    end
    // No match found
    m_mismatches++;
    `uvm_error("SB", $sformatf(
      "No expected match for: %s", actual.convert2string()))
  endfunction

  function void check_phase(uvm_phase phase);
    // Report any expected transactions that never arrived
    m_missing = expected_q.size();
    if (m_missing > 0)
      `uvm_error("SB", $sformatf(
        "%0d expected transactions never received from DUT", m_missing))
  endfunction
endclass
Out-of-order comparators need a key for matching. Calling compare() on the full transaction may be too strict — minor differences in timestamp or status fields will prevent a match even when the payload is correct. Override do_compare() in the sequence item to compare only the fields that define equivalence (address, data, transfer type) and exclude response-time fields.

📋 Complete Scoreboard Example

A self-checking scoreboard for a GPIO register block — the simplest architecture: the scoreboard receives complete read transactions and computes the expected read data itself from its own shadow register:

class gpio_self_sb extends uvm_subscriber #(apb_seq_item);
  `uvm_component_utils(gpio_self_sb)

  // Shadow register file — updated on every write
  logic [31:0] regs [0:4];
  int m_write_checks = 0;
  int m_read_checks  = 0;
  int m_errors       = 0;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    foreach(regs[i]) regs[i] = 0;
  endfunction

  // write() — called by monitor's analysis port for every transfer
  function void write(apb_seq_item txn);
    int idx = txn.addr >> 2;
    if (!txn.read_not_write) begin
      // Write: update shadow, check no error
      foreach (txn.byte_en[i])
        if (txn.byte_en[i]) regs[idx][i*8 +: 8] = txn.write_data[i*8 +: 8];
      m_write_checks++;
      if (txn.error)
        report_err("Write PSLVERR unexpected", txn);
    end else begin
      // Read: compare actual read data against shadow
      m_read_checks++;
      if (txn.read_data !== regs[idx])
        report_err($sformatf(
          "Read mismatch @0x%03h: exp=0x%08h act=0x%08h",
          txn.addr, regs[idx], txn.read_data), txn);
    end
  endfunction

  function void report_err(string msg, apb_seq_item txn);
    m_errors++;
    `uvm_error("SB", {msg, "\n  txn: ", txn.convert2string()})
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "Checked: %0d writes, %0d reads, %0d errors",
      m_write_checks, m_read_checks, m_errors), UVM_LOW)
  endfunction
endclass

📋 phase_ready_to_end in Scoreboards

A common scoreboard bug: the simulation ends while transactions are still in the FIFOs or queues, not yet compared. The fix is phase_ready_to_end() — when the phase is about to end, the scoreboard checks if it still has pending work and, if so, raises a new objection to extend the phase.

function void phase_ready_to_end(uvm_phase phase);
  if (phase.get_name() != "run") return;

  if (m_expected_fifo.size() > 0 || m_actual_fifo.size() > 0) begin
    // Pending transactions — extend the run phase
    phase.raise_objection(this, "SB still has pending txns");
    fork begin
      // Wait until both FIFOs drain
      wait(m_expected_fifo.size() == 0 &&
           m_actual_fifo.size() == 0);
      // Extra time for any in-flight comparisons
      #100ns;
      phase.drop_objection(this);
    end join_none
  end
endfunction
Always implement phase_ready_to_end in scoreboards that use FIFOs. Without it, the run_phase ends when the last sequence objection drops — but the scoreboard’s FIFOs may still hold transactions that arrived just before the end. Those transactions are never compared. The missed transactions appear as silent false passes — the most dangerous type of bug because the simulation claims to pass while real DUT errors go undetected.

📋 Error Reporting and Statistics

Good scoreboard error reporting tells you exactly what went wrong, where, and how many times. The error message should include the full transaction content (use convert2string()), the expected vs actual values side by side, and a running count of matches and mismatches.

function void report_phase(uvm_phase phase);
  string result = (m_mismatches == 0) ? "PASSED" : "FAILED";
  `uvm_info("SB", $sformatf(
    "\n=== Scoreboard Report ===\n"
    "  Transactions checked: %0d\n"
    "  Matches:             %0d\n"
    "  Mismatches:          %0d\n"
    "  Unmatched expected:  %0d\n"
    "  Result: %s",
    m_matches + m_mismatches,
    m_matches, m_mismatches, m_missing, result),
    UVM_LOW)

  // Raise error so simulator marks test FAILED
  if (m_mismatches > 0 || m_missing > 0)
    `uvm_error("SB", "TEST FAILED — see report above")
endfunction
Use `uvm_error not `uvm_fatal for mismatches. `uvm_fatal stops the simulation immediately — you see only the first mismatch and miss all subsequent ones. `uvm_error records the error and continues — the report_phase shows the full count. Use `uvm_fatal only for unrecoverable infrastructure failures (null pointer, missing config) not for verification failures.

📋 Parameterised Comparator Pattern

When multiple agents use the same scoreboard structure, a parameterised comparator class avoids duplicating the comparison infrastructure. The transaction type is the parameter; the comparison logic is shared.

// ── Generic in-order comparator ───────────────────────────
`uvm_analysis_imp_decl(_expected)
`uvm_analysis_imp_decl(_actual)

class inorder_comparator #(type T = uvm_sequence_item)
    extends uvm_scoreboard;

  uvm_analysis_imp_expected #(T, inorder_comparator #(T)) expected_export;
  uvm_analysis_imp_actual   #(T, inorder_comparator #(T)) actual_export;

  tlm_analysis_fifo #(T) expected_fifo;
  tlm_analysis_fifo #(T) actual_fifo;

  int m_matches = 0, m_mismatches = 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_export = new("expected_export", this);
    actual_export   = new("actual_export",   this);
    expected_fifo   = new("expected_fifo",   this);
    actual_fifo     = new("actual_fifo",     this);
  endfunction

  function void write_expected(T t); expected_fifo.write(t); endfunction
  function void write_actual(T t);   actual_fifo.write(t);   endfunction

  task run_phase(uvm_phase phase);
    T exp, act;
    forever begin
      expected_fifo.get(exp); actual_fifo.get(act);
      if (exp.compare(act)) m_matches++;
      else begin
        m_mismatches++;
        `uvm_error("CMP", $sformatf(
          "MISMATCH\n  exp: %s\n  act: %s",
          exp.convert2string(), act.convert2string()))
      end
    end
  endtask
endclass

// ── Instantiate for specific transaction types ────────────
typedef inorder_comparator #(apb_seq_item) apb_comparator;
typedef inorder_comparator #(ahb_seq_item) ahb_comparator;

📋 Common Pitfalls

PitfallSymptomFix
Not implementing phase_ready_to_endSimulation ends while FIFOs still have items. Transactions silently never compared. Test falsely passes.Implement phase_ready_to_end() — raise objection if FIFOs non-empty, drop when they drain.
Using `uvm_fatal for mismatchesSimulation stops at first mismatch. Total mismatch count unknown.Use `uvm_error — simulation continues, all mismatches reported, count checked in report_phase.
Predictor wired to wrong monitorPredictor predicts based on DUT output, not stimulus. Expected = actual always — silent false pass.Predictor must subscribe to the stimulus monitor (driver-side). Comparator subscribes to the response monitor.
compare() too strictOut-of-order SB never finds a match. All actuals are mismatches even when data is correct.Override do_compare() to compare only payload fields (addr, data). Exclude timestamps, status codes.
Scoreboard not checking leftover expected queueDUT drops responses silently — scoreboard never reports missing transactions.In check_phase, report error if expected_q is non-empty: if (expected_q.size() > 0) `uvm_error(...)
Comparing handles not valuescompare() always returns true — scoreboard sees no mismatches even with wrong data.Ensure do_compare() is implemented in the sequence item — default compare() may compare handles if `uvm_field macros are not used.

📋 Quick Reference

ItemKey fact
Scoreboard base classuvm_scoreboard extends uvm_component
Best architectureSeparate predictor (expected) from comparator (actual vs expected)
Predictor inputStimulus transactions — from the monitor that observes what was driven TO the DUT
Comparator inputsExpected from predictor + actual from response monitor
In-order comparatorTwo FIFOs + run_phase get(exp) + get(act) + compare() — for strict-FIFO protocols
Out-of-order comparatorQueue expected; search queue for match on each actual write_actual() call
phase_ready_to_endMandatory in FIFOs-based SBs — extend phase if FIFOs non-empty
`uvm_error vs `uvm_fatalUse `uvm_error for mismatches — continues simulation to collect all errors
check_phase useReport unmatched expected transactions (expected_q.size() > 0)
report_phase usePrint match/mismatch counts and overall PASS/FAIL
compare() customisationOverride do_compare() in seq_item to define which fields constitute a match
Self-checking SBSingle stream; SB maintains shadow and computes expected inline — for simple DUTs
Parameterised comparatorinorder_comparator #(T) — one class for all transaction types
FIFO leftover checkIn check_phase: if (fifo.size() > 0) `uvm_error(...)
Scroll to Top