A testbench that drives stimulus but never checks whether the DUT output is correct is not a verification environment — it is just a waveform generator. The component that closes that loop is the scoreboard: the automated checker that collects what the DUT actually produced, compares it to what it should have produced, and delivers an unambiguous pass or fail verdict. Building a scoreboard well — choosing the right reference model, handling latency correctly, and dealing with out-of-order completion — is one of the core practical skills in verification engineering.
10 min read | Intermediate Level
What You'll Learn
- What a scoreboard does and how it fits into the testbench architecture
- How to build a simple scoreboard with input and expected FIFOs and a comparator
- The difference between transaction-level and cycle-accurate reference models and when to use each
- How to handle pipeline latency and out-of-order completion in scoreboards
What a Scoreboard Does
A scoreboard is the automated checking engine of a testbench. Its job is to answer one question for every transaction the DUT processes: did the DUT produce the correct output? To answer that question, the scoreboard needs two pieces of information — what the DUT actually produced, and what it should have produced. It collects both, compares them, and reports any discrepancy as a test failure.
The two data streams come from different sources. The DUT's actual output is captured by an output monitor — a passive observer that watches the DUT's output interface and records every transaction it sees. The expected output is computed by a reference model — a behavioural model of the DUT that takes the same input the DUT received and predicts what the correct output should be. Both streams feed into the scoreboard, which dequeues one entry from each and compares them field by field.
The scoreboard itself does not drive any signals and does not know about clock edges — it operates at the transaction level. It receives complete, decoded transactions from the monitor and from the reference model and compares their contents. Separating the protocol-level concerns (monitor) from the correctness-checking concerns (scoreboard) keeps each component simple and reusable.
Building a Simple Scoreboard
The minimal scoreboard structure has three components: a FIFO for actual results, a FIFO for expected results, and comparison logic that dequeues one entry from each and checks them for equality.
class scoreboard;
// Queues acting as FIFOs
transaction_t actual_q[$];
transaction_t expected_q[$];
// Called by output monitor when DUT produces a transaction
function void write_actual(transaction_t t);
actual_q.push_back(t);
compare();
endfunction
// Called by reference model when it predicts a result
function void write_expected(transaction_t t);
expected_q.push_back(t);
compare();
endfunction
// Compare head of each queue when both have an entry
function void compare();
if (actual_q.size() > 0 && expected_q.size() > 0) begin
transaction_t act = actual_q.pop_front();
transaction_t exp = expected_q.pop_front();
if (act.data !== exp.data || act.addr !== exp.addr)
$error("[%0t] MISMATCH: exp addr=0x%h data=0x%h, got addr=0x%h data=0x%h",
$time, exp.addr, exp.data, act.addr, act.data);
else
$display("[%0t] PASS: addr=0x%h data=0x%h", $time, act.addr, act.data);
end
endfunction
endclass
The compare() function only fires when both queues are non-empty, so it naturally handles the case where the DUT result arrives before the reference model has finished computing — or vice versa. The queues buffer the mismatch in timing without losing either entry.
Always check that both queues are empty at the end of the test. A non-empty queue at the end of simulation means either the DUT produced transactions that the reference model never predicted (extra outputs) or the reference model predicted transactions that the DUT never produced (missing outputs). Both are bugs, and neither will be caught by the comparison logic alone.
Quick Check
Q: At the end of a test, the actual queue is empty but the expected queue contains two entries. What does this indicate?
Show Answer
The DUT produced fewer outputs than the reference model predicted. The reference model expected two more transactions that the DUT never generated — this is a missing output bug. The test should fail with an error like "simulation ended with 2 unmatched expected transactions."
Reference Models
The reference model is a software implementation of the DUT's intended behaviour. It takes the same inputs as the DUT and produces the expected outputs. The scoreboard trusts the reference model as the ground truth — any discrepancy between the reference model's prediction and the DUT's actual output is flagged as a bug in the DUT.
This means the reference model must be correct. A bug in the reference model produces false failures — the DUT may be right and the model wrong. Reference models should be written independently from the RTL, ideally by someone other than the RTL designer, using a different algorithmic approach wherever possible.
Transaction-Level Reference Models
A transaction-level reference model operates at the same abstraction as the scoreboard — it takes a complete input transaction, computes the expected output transaction, and returns it without any notion of clock cycles or pipeline stages. For most blocks this is the right choice.
// Transaction-level reference model for a simple adder
function transaction_t adder_ref_model(transaction_t in_t);
transaction_t out_t;
out_t.result = in_t.a + in_t.b; // behavioural description
out_t.tag = in_t.tag;
return out_t;
endfunction
Transaction-level models are fast to write, easy to read, and straightforward to maintain when the DUT specification changes. They do not attempt to model timing, pipeline depth, or internal state beyond what is visible in the output transaction. For most block-level verification this is sufficient — the scoreboard handles the timing difference between when the input was applied and when the output appears.
Cycle-Accurate Reference Models
Some blocks have behaviour that is inherently cycle-dependent — a cache, a reorder buffer, or a rate limiter where the output on cycle N depends on what happened on cycles N-1 and N-2. For these, a transaction-level model cannot accurately predict output timing or values, and a cycle-accurate model is needed.
A cycle-accurate model mirrors the DUT's internal state at every clock cycle. It is significantly more complex to write and maintain, and it risks replicating bugs from the RTL if written too closely to the implementation. Use it only when the block's behaviour genuinely cannot be captured at the transaction level.
A cycle-accurate reference model that is written by reading the RTL and translating it line by line is not an independent check — it is a copy of the RTL in a different language. It will replicate RTL bugs rather than detect them. The reference model must be derived from the specification, not from the implementation.
Where This Matters in Practice
For complex arithmetic blocks — floating-point units, cryptographic engines, compression blocks — the reference model is often an existing software implementation of the same algorithm: a C library, a Python function, or a golden reference from the specification body. Calling out to this software from the testbench via DPI (Direct Programming Interface) is common practice and gives an immediately trustworthy reference model without writing one from scratch.
Handling Latency
Most real DUTs do not produce output in the same cycle the input arrives. A pipelined multiplier might take 4 cycles. A memory controller might take a variable number of cycles depending on whether the access hits a buffer or goes to external memory. The scoreboard must account for this latency without losing track of which expected output corresponds to which actual output.
For fixed latency, the simplest approach is to delay the reference model's prediction by the known number of cycles before pushing it into the expected queue. For variable latency, the correct approach is to let both queues fill naturally — the comparison fires when both an actual and an expected entry are present, regardless of when each arrived. As long as the DUT eventually produces the correct output in the correct order, the scoreboard will match them.
// Handling fixed pipeline latency — delay expected entry by N cycles
task apply_stimulus(transaction_t t);
drive_dut(t); // send to DUT now
repeat(PIPELINE_DEPTH) @(posedge clk); // wait for output
sb.write_expected(ref_model(t)); // push expected after latency
endtask
Out-of-Order Completion
Some DUTs — particularly those with multiple parallel execution units or memory systems with hit/miss latency — complete transactions in a different order than they were submitted. A simple FIFO-based scoreboard breaks in this case: it would compare transaction A's actual result against transaction B's expected result, generating false failures.
The solution is tag-based matching. Every transaction is assigned a unique identifier — a tag or transaction ID — when it is submitted. The reference model stores its predicted result in an associative array (a dictionary) keyed by that tag. When the output monitor reports an actual result, the scoreboard looks up the expected result for the same tag rather than dequeuing the head of a FIFO.
class ooo_scoreboard;
// Associative array: tag -> expected transaction
transaction_t expected_aa [int];
function void write_expected(transaction_t t);
expected_aa[t.tag] = t; // store by tag
endfunction
function void write_actual(transaction_t t);
if (!expected_aa.exists(t.tag)) begin
$error("[%0t] unexpected transaction tag=%0d", $time, t.tag);
return;
end
transaction_t exp = expected_aa[t.tag];
expected_aa.delete(t.tag); // remove matched entry
if (t.data !== exp.data)
$error("[%0t] MISMATCH tag=%0d: exp=0x%h got=0x%h",
$time, t.tag, exp.data, t.data);
endfunction
endclass
Common Mistakes
Mistake 1: Not checking for leftover entries in the queues at end of simulation.
Wrong: Letting the simulation end without verifying that both the actual and expected queues are empty.
Right: Add a final check in a cleanup phase that asserts both queues are empty and reports any remaining entries as errors.
Why? Missing outputs — transactions the DUT should have produced but did not — will never be caught by the comparison logic. They simply accumulate in the expected queue and disappear silently at the end of the run, masking real bugs.
Mistake 2: Writing the reference model by translating the RTL into SystemVerilog.
Wrong: Reading the RTL code and implementing the same algorithm in the reference model, line by line.
Right: Implement the reference model from the design specification, using the simplest correct algorithm possible — even if it is less efficient than the RTL.
Why? A reference model derived from the RTL inherits the RTL's bugs. The whole point of an independent reference model is to provide a second opinion. If both opinions come from the same source, the scoreboard only detects transcription errors, not design errors.
Mistake 3: Using a FIFO-based scoreboard for a DUT that can complete transactions out of order.
Wrong: Using pop_front() on both queues to match actual and expected results when the DUT reorders completions.
Right: Use tag-based matching with an associative array keyed by transaction ID.
Why? A FIFO scoreboard on an out-of-order DUT produces false failures on every transaction that completes out of order and silently passes transactions that are actually wrong but happen to match the misaligned expected entry.
Practice Exercise
Challenge: Extend the Scoreboard for Latency
You have a pipelined DUT with a fixed latency of 3 clock cycles between input and output. The simple scoreboard below pushes the expected result immediately when stimulus is applied. Identify the problem this causes and modify the apply_and_predict task to handle the latency correctly.
task apply_and_predict(transaction_t t);
driver.send(t); // drive DUT input now
sb.write_expected(ref_model(t)); // push expected immediately — PROBLEM
endtask
Hint
Think about what happens inside the scoreboard's compare() function when the expected entry arrives 3 cycles before the actual result. Will it wait, or will it trigger a comparison immediately?
Solution
The problem is that write_expected pushes the prediction immediately. If an actual result from a previous transaction is already in the actual queue, the scoreboard will dequeue it and compare it against this new expected entry — a mismatch between two unrelated transactions.
The fix is to delay the expected push by the pipeline depth:
task apply_and_predict(transaction_t t);
driver.send(t); // drive DUT input now
fork
begin
repeat(3) @(posedge clk); // wait 3 cycles (pipeline depth)
sb.write_expected(ref_model(t)); // push expected after latency
end
join_none // non-blocking: don't stall the main thread
endtask
The fork...join_none lets the main stimulus thread continue sending the next transaction while the delayed prediction runs in the background. This correctly models a pipelined DUT where multiple transactions are in flight simultaneously.
Summary
- A scoreboard collects actual DUT output via an output monitor and predicted output from a reference model, then compares them transaction by transaction to produce an automatic pass or fail
- Transaction-level reference models are the right choice for most blocks — they implement the specification directly without modelling timing, and are fast to write and maintain
- Cycle-accurate models are only needed when output values genuinely depend on cycle-by-cycle internal state; they must be derived from the specification, never from the RTL
- Fixed-latency DUTs are handled by delaying the expected push; out-of-order DUTs require tag-based matching with an associative array rather than FIFO dequeuing
- Always verify that both queues are empty at the end of simulation — leftover entries indicate missing outputs that the comparison logic will never catch