The short answer: A UVM scoreboard checks that the DUT’s outputs are correct. It receives observed inputs from an input monitor, uses a reference model (predictor) to compute expected results, stores them (a queue for in-order designs, a map keyed by ID or address for out-of-order ones), and compares each observed output from the output monitor against them. In check_phase it reports any expected items that never arrived, which catches lost transactions.
The scoreboard is where bugs are actually found. Stimulus and coverage tell you what you tested; the scoreboard tells you whether the DUT got it right. This post covers the structure I use and the mistakes that make scoreboards miss bugs.
- The flow
- An in-order scoreboard
- Out-of-order and multi-stream designs
- Where the reference model lives
- Mistakes that hide bugs
Table of Contents
Part 1: The flow

Part 2: An in-order scoreboard
`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)
class my_sb extends uvm_scoreboard;
`uvm_component_utils(my_sb)
uvm_analysis_imp_in #(bus_item, my_sb) in_imp;
uvm_analysis_imp_out #(bus_item, my_sb) out_imp;
bus_item exp_q[$];
int n_match, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
in_imp = new("in_imp", this); out_imp = new("out_imp", this);
endfunction
function void write_in(bus_item t);
exp_q.push_back(predict(t)); // reference model
endfunction
function void write_out(bus_item t);
bus_item exp;
if (exp_q.size() == 0) begin
`uvm_error("SB", {"unexpected output: ", t.convert2string()}) return;
end
exp = exp_q.pop_front();
if (t.compare(exp)) n_match++;
else begin
n_mismatch++;
`uvm_error("SB", $sformatf("exp %s got %s", exp.convert2string(), t.convert2string()))
end
endfunction
function void check_phase(uvm_phase phase);
if (exp_q.size())
`uvm_error("SB", $sformatf("%0d expected items never arrived", exp_q.size()))
`uvm_info("SB", $sformatf("matched %0d, mismatched %0d", n_match, n_mismatch), UVM_LOW)
endfunction
endclass
Part 3: Out-of-order and multi-stream designs
- Key the expected store by transaction ID (an associative array of queues) when responses can return out of order, as in AXI.
- For memories and register files, keep a model of the memory and compare reads against it instead of a list of expected outputs.
- For packet switches, keep one queue per output port.
- When a DUT can legally drop or reorder items, encode those rules in the predictor, not as loose comparisons.
Part 4: Where the reference model lives
Small models go straight into a predict() function. Larger ones become their own component (a predictor) between the input monitor and the scoreboard, or a C/C++ model called through DPI. Keep the model independent of the RTL: written from the spec, ideally by someone other than the designer.
Part 5: Mistakes that hide bugs
- Comparing against the driver’s intent instead of what the input monitor observed.
- Storing the monitor’s object without cloning it, so it changes after you stored it.
- No end-of-test check, so lost transactions pass silently.
- Comparing fields the DUT doesn’t produce (like stimulus delays), causing false failures that people learn to ignore.
- Only printing mismatches without failing the test.
Practise this on VLSI Forge
I built VLSI Forge so you can write RTL in your browser, run it on a real simulator and check every signal in the waveform. Free, nothing to install.
Previous in my UVM series: TLM and analysis ports. Next: Coverage and assertions in UVM.
FAQ
What does a UVM scoreboard do?
It predicts expected results from observed inputs, compares them with the DUT’s observed outputs, and reports mismatches and missing transactions.
How do I handle out-of-order responses?
Store expected items in an associative array keyed by transaction ID, and match each response by its ID.
Should the scoreboard get inputs from the driver or a monitor?
From an input monitor, so it checks what really reached the DUT.
What should the scoreboard check at the end of the test?
That no expected items are left over, and it should print match and mismatch counts.
