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.
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.
Two common scoreboard topologies exist depending on what the monitor can observe:
| Topology | Monitors | What comparator receives | Typical use |
|---|---|---|---|
| Prediction model | Input monitor (stimulus) + output monitor (response) | Expected from predictor + actual from output monitor | Protocol bus verification — DUT transforms APB writes into register state changes |
| Self-checking | Single monitor observes complete transactions | Two streams from same monitor — reference computed inline from transaction fields | Simple DUTs where expected result is computed directly (ALU, CRC, checksum) |
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
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:
`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
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
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.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
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
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
`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.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;
| Pitfall | Symptom | Fix |
|---|---|---|
| Not implementing phase_ready_to_end | Simulation 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 mismatches | Simulation 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 monitor | Predictor 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 strict | Out-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 queue | DUT 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 values | compare() 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. |
| Item | Key fact |
|---|---|
| Scoreboard base class | uvm_scoreboard extends uvm_component |
| Best architecture | Separate predictor (expected) from comparator (actual vs expected) |
| Predictor input | Stimulus transactions — from the monitor that observes what was driven TO the DUT |
| Comparator inputs | Expected from predictor + actual from response monitor |
| In-order comparator | Two FIFOs + run_phase get(exp) + get(act) + compare() — for strict-FIFO protocols |
| Out-of-order comparator | Queue expected; search queue for match on each actual write_actual() call |
| phase_ready_to_end | Mandatory in FIFOs-based SBs — extend phase if FIFOs non-empty |
| `uvm_error vs `uvm_fatal | Use `uvm_error for mismatches — continues simulation to collect all errors |
| check_phase use | Report unmatched expected transactions (expected_q.size() > 0) |
| report_phase use | Print match/mismatch counts and overall PASS/FAIL |
| compare() customisation | Override do_compare() in seq_item to define which fields constitute a match |
| Self-checking SB | Single stream; SB maintains shadow and computes expected inline — for simple DUTs |
| Parameterised comparator | inorder_comparator #(T) — one class for all transaction types |
| FIFO leftover check | In check_phase: if (fifo.size() > 0) `uvm_error(...) |