Subscriber built reg_coverage, which only ever watches and counts - it never judges whether a transaction was correct. This article builds the component that actually checks correctness: a scoreboard that predicts what reg_block should do, and flags it when reality disagrees.
9 min read | Intermediate Level
What You'll Learn
- What a scoreboard does, and how that's different from what reg_coverage does
- How to declare a
uvm_analysis_impby hand, withoutuvm_subscriber's shortcut - How to build a simple reference model that predicts reg_block's expected contents
- How to connect reg_monitor to a scoreboard
What Is a Scoreboard?
A scoreboard is a verification component that checks whether a design behaved correctly, usually by comparing what actually happened against what should have happened. For a register block like reg_block, the question a scoreboard answers is simple to state: if a write put a certain byte at a certain address, does a later read from that same address return that same byte back?
Answering that question means the scoreboard needs to remember every write it's seen, so it has something to compare a later read against. That remembered, predicted state is sometimes called a reference model, or a predictor - a simplified model of what the DUT's internal state should be, built and maintained entirely in the testbench, with no dependence on reg_block's own implementation.
Building reg_scoreboard
1. Declare the Class
class reg_scoreboard extends uvm_scoreboard;
`uvm_component_utils(reg_scoreboard)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Code for rest of the steps come here
endclass
reg_scoreboard extends uvm_scoreboard rather than plain uvm_component, for the same reason reg_monitor extends uvm_monitor instead: it's recommended practice, and it makes a scoreboard immediately recognizable as one by its class, even though uvm_scoreboard doesn't add any methods on top of uvm_component itself.
2. Declare an Analysis Implementation Port
Subscriber gave reg_coverage its own analysis_export automatically, just by extending uvm_subscriber. uvm_scoreboard doesn't include that convenience, so reg_scoreboard has to declare the same underlying mechanism, uvm_analysis_imp, by hand:
uvm_analysis_imp #(reg_transaction, reg_scoreboard) ap_imp;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
ap_imp is a plain class property, so it needs constructing with new() just like reg_coverage's analysis_export did underneath uvm_subscriber's own constructor - the difference here is that reg_scoreboard does that construction itself, inside its own build_phase, rather than getting it for free.
3. Build the Reference Model
reg_scoreboard needs somewhere to keep track of what it expects reg_block's memory to contain, built up purely from the writes it's observed:
bit [7:0] expected_mem [16];
bit written [16];
virtual function void write(reg_transaction tr);
if (tr.wr_en) begin
expected_mem[tr.addr] = tr.wdata;
written[tr.addr] = 1;
end
else if (tr.rd_en) begin
if (!written[tr.addr]) begin
`uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
end
else if (tr.rdata != expected_mem[tr.addr]) begin
`uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
end
else begin
`uvm_info(get_type_name(), $sformatf("addr=%0d matched: 0x%0h", tr.addr, tr.rdata), UVM_HIGH)
end
end
endfunction
write() is the method ap_imp calls automatically every time reg_monitor broadcasts a captured transaction - the same role reg_coverage's own write() plays, just with checking instead of sampling inside it. On a write, reg_scoreboard simply records what it expects that address to hold from now on. On a read, it compares reg_monitor's captured rdata against that prediction.
written exists to handle a real edge case honestly: a read from an address that's never been written has no known expected value to compare against - reg_block's own memory contents are undefined at that point, not necessarily zero. Comparing against an assumed default there would risk flagging a meaningless mismatch, so reg_scoreboard skips the check entirely until it has actually seen a write to that address.
Quick Check
Q: Why does write() check tr.wr_en and tr.rd_en instead of just checking whether tr.rdata is populated?
Show Answer
reg_transaction's wr_xor_rd_c constraint already guarantees every transaction is clearly one or the other, so checking the flag directly is the more reliable signal - rdata is simply never populated by reg_monitor on a write, but relying on that as an implicit signal would be more fragile than checking the field that actually says what kind of transaction this is.
Connecting reg_monitor to reg_scoreboard
Same as reg_coverage, reg_scoreboard does nothing until something is actually connected to ap_imp. That connection is made wherever both reg_monitor (inside reg_agent) and reg_scoreboard are owned by the same parent - typically the environment, covered in UVM Environment:
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_agent.m_monitor.mon_analysis_port.connect(m_scoreboard.ap_imp);
endfunction
This snippet names m_agent and m_scoreboard as if they already exist side by side in some parent component, since that's exactly the situation UVM Environment builds. The connection itself is no different from the monitor-to-coverage wiring already shown inside reg_agent - one analysis port, connected to one analysis implementation, with reg_monitor never needing to know who's on the other end.
Common Mistakes
Mistake #1: Performing the comparison inside the monitor instead of the scoreboard
It can be tempting to have reg_monitor do the comparison itself, since it already sees every transaction. Keeping that logic in a separate scoreboard instead means the same monitor can feed multiple different checkers, and means changing the checking logic never requires touching the monitor at all.
Mistake #2: Comparing against an assumed default instead of tracking what's actually been written
Skipping the written array and assuming every address starts at a known value - usually zero - produces a scoreboard that occasionally flags failures that aren't real bugs, simply because nothing was ever written to that address yet. A reference model should only ever check what it actually has a basis to predict.
Practice Exercise
Challenge: Count Mismatches Instead of Just Reporting Them
Add a property to reg_scoreboard that counts how many mismatches have occurred across the whole test, and print the total in report_phase.
Hint
Increment a counter inside the same branch that calls `uvm_error, then add a report_phase function that prints it - this phase runs once, near the very end of the simulation, after every transaction has already been processed.
Solution
int unsigned mismatch_count = 0;
virtual function void write(reg_transaction tr);
if (tr.wr_en) begin
expected_mem[tr.addr] = tr.wdata;
written[tr.addr] = 1;
end
else if (tr.rd_en) begin
if (!written[tr.addr]) begin
`uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
end
else if (tr.rdata != expected_mem[tr.addr]) begin
mismatch_count++;
`uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
end
end
endfunction
virtual function void report_phase(uvm_phase phase);
`uvm_info(get_type_name(), $sformatf("Total mismatches: %0d", mismatch_count), UVM_LOW)
endfunction
report_phase is a good fit here specifically because it runs after everything else is done - printing the count any earlier could miss mismatches that hadn't happened yet.
Summary
In this article, you learned:
- A scoreboard predicts a DUT's expected behavior and compares it against what actually happened
uvm_scoreboarddoesn't provide a built-in analysis export the wayuvm_subscriberdoes, so a scoreboard declares its ownuvm_analysis_impand constructs it inbuild_phase- A reference model should only check addresses it has actually seen written, not assume an unwritten address starts at a known value
- Connecting a monitor's analysis port to a scoreboard's implementation port uses the exact same mechanism already used to connect a monitor to coverage
Going Deeper
- Analysis ports and imps: UVM TLM Analysis Port
- Buffering transactions in a FIFO: UVM TLM Fifo