The Scoreboard
The monitor from earlier in this section broadcasts every transaction it observes — but broadcasting isn't checking. Something has to actually decide whether what came out of the FIFO was correct, and that judgment is the scoreboard's entire reason to exist.
The reference-model pattern
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp #(fifo_txn, fifo_scoreboard) ap_imp;
bit [7:0] expected_q[$];
function new(string name, uvm_component parent);
super.new(name, parent);
ap_imp = new("ap_imp", this);
endfunction
function void write(fifo_txn txn);
if (txn.op == fifo_txn::WRITE) begin
expected_q.push_back(txn.wr_data);
`uvm_info("SB", $sformatf("Pushed expected data=%0h", txn.wr_data), UVM_HIGH)
end else begin
bit [7:0] expected;
if (expected_q.size() == 0) begin
`uvm_error("SB", "Read observed with nothing expected in the FIFO")
return;
end
expected = expected_q.pop_front();
if (txn.rd_data !== expected)
`uvm_error("SB", $sformatf("Mismatch: expected=%0h actual=%0h", expected, txn.rd_data))
else
`uvm_info("SB", $sformatf("Match: data=%0h", txn.rd_data), UVM_HIGH)
end
endfunction
endclass
The core idea: expected_q is an independent model of what the FIFO should contain — sometimes called a predictor in UVM literature — built entirely from the monitor's own observations, with zero connection to anything the DUT actually did. A WRITE transaction pushes the value that should eventually come back out; a READ transaction pops the oldest expected value and compares it against what the monitor actually saw on rd_data. If DUT behavior and reference model ever disagree, expected_q says so — because it was never capable of agreeing with the DUT by construction, only by the DUT actually being correct.
Manual wiring, not uvm_subscriber
This class implements uvm_analysis_imp #(fifo_txn, fifo_scoreboard) ap_imp and constructs it by hand in the constructor — unlike the coverage collector from the previous page, which got analysis_export for free by extending uvm_subscriber. The scoreboard doesn't use that shortcut because it needs to be a full uvm_scoreboard (a distinct base class, semantically signaling "this component judges pass/fail," separate from uvm_subscriber's narrower "this component just consumes a stream" role) — the small amount of manual wiring here is the cost of that more specific identity. Both approaches end up with the same shape: a write(fifo_txn t) method invoked automatically whenever a connected analysis port fires.
Why a queue, not a single "last value" check
A tempting simplification would be a single expected variable instead of a queue — but a FIFO can have multiple writes queued up before any read happens, so "the most recent write" and "what the next read should return" are two different things. The queue models the FIFO's actual ordering guarantee directly: first data in, first data out, exactly matching what expected_q.pop_front() does. This is the general shape a reference model takes whenever the DUT itself has ordering or state to track — the checker needs its own model of that same state, not just a memory of the last stimulus applied.
The empty-queue guard
The expected_q.size() == 0 check protects against a specific and genuinely informative failure: a READ transaction observed with nothing in the reference model to compare it against. That's not a bookkeeping edge case to silently ignore — on real hardware it would mean either the testbench's model of FIFO state has drifted out of sync with the DUT, or the DUT itself produced a read where the model expected empty to have blocked it. Reporting it as an explicit `uvm_error rather than skipping the comparison keeps that class of bug visible instead of silently swallowed.
Scaling to multiple monitors
This example's scoreboard only ever receives transactions from one monitor, so a single uvm_analysis_imp is enough. A busier verification environment often needs one scoreboard fed by two or more independent monitors — for instance, comparing a protocol's request-side and response-side activity as two separate streams instead of folding everything through one transaction type. Because a class can only implement one method literally named write(), a second plain uvm_analysis_imp of the same declared shape would have nowhere else to route its calls. UVM's answer is the `uvm_analysis_imp_decl macro, which generates a uniquely-named imp type per suffix you give it (e.g. _wr/_rd) — each one calling its own differently-named write method (write_wr/write_rd) instead of colliding on a single write(). It's a scaling detail worth knowing exists, even though this page's single-monitor FIFO example never needs it.
What's next
Driver, monitor, sequencer/sequence, and scoreboard now exist as standalone classes. The next page bundles them into the two components that give the whole testbench its shape — the agent and the environment.