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.
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.
| Role | Who performs it | Phase |
|---|---|---|
| Generate stimulus | Sequence + Sequencer | run_phase |
| Drive DUT pins | Driver | run_phase |
| Observe DUT outputs | Monitor | run_phase |
| Check correctness | Scoreboard | run_phase + check_phase |
| Measure coverage | Coverage collector | run_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.
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.
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() 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.
| Concept | Producer side (Monitor) | Consumer side (Scoreboard) |
|---|---|---|
| Port type | uvm_analysis_port | uvm_analysis_imp |
| Method | port.write(item) — broadcasts | write(item) — receives |
| Blocking? | No — returns immediately | No — must be zero-time function |
| Subscribers | Many (broadcast) | One write() per imp |
| Phase created | build_phase | build_phase |
| Connected | connect_phase in parent environment | |
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
$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.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.
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
!= 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
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.
| Macro | Severity | Simulation continues? | Best used in |
|---|---|---|---|
`uvm_info | Informational | Yes | Successful match messages |
`uvm_warning | Warning (non-fatal) | Yes | Unexpected but recoverable conditions |
`uvm_error | Error (increments error count) | Yes (by default) | Mismatches, missing responses |
`uvm_fatal | Fatal (stops simulation) | No | Configuration failures in build_phase |
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
`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.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
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);
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).
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.
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.
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.
uvm_scoreboard extends uvm_component — it adds no new API, just a semantic classificationuvm_analysis_imp is the subscriber port; implement a write() function in the scoreboard class!== not != to catch X values as real mismatches in 4-state simulation`uvm_analysis_imp_decl(_suffix) creates uniquely named imp types for multi-monitor setups`uvm_error in write() to accumulate errors; use check_phase for the final summarycheck_phase to detect disconnected (silently passing) scoreboardsA 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.
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.
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.
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.