UVM TUTORIAL SERIES
🎯 UVM Scoreboard Explained
Study Notes • Working Code • Animated Demo
🔍 What is a UVM Scoreboard?
The Scoreboard is the self-checking brain of a UVM testbench. It receives transactions from the monitor, compares DUT behavior against a golden reference (predicted) result, and declares PASS or FAIL.
📥
CollectReceives actual results from the Monitor via TLM analysis ports
🧠
PredictContains a reference model that computes expected results
⚖️
CompareChecks actual vs expected and logs MATCH / MISMATCH
🏁
VerdictDeclares overall pass/fail in report_phase
💡 Golden Rule: A scoreboard never drives stimulus — it only observes and checks. It is always a uvm_component, never a sequence item.
📡 Where does the Scoreboard sit?
Two data streams flow into the scoreboard — the expected results (from a predictor/reference model) and the actual results (from the monitor). Watch them converge:
🧠 Reference Modelexpected results
👀 Monitoractual results
🎯
SCOREBOARD
🔌 How transactions reach the Scoreboard (TLM)
| TLM Construct | Direction | Used By |
uvm_analysis_port | Broadcasts (many subscribers) | Monitor |
uvm_analysis_imp | Receives — calls write() | Scoreboard |
uvm_analysis_export | Pass-through for hierarchies | Env / Subscribers |
uvm_subscriber | Base class with built-in imp | Scoreboard / Coverage |
⚡ Key point: write() in the scoreboard is a function (zero delay), called automatically every time the monitor publishes a transaction.
⚙️ Scoreboard Internals — Live Demo
Expected and actual transactions arrive, get paired up, and the comparator fires its verdict:
EXPECTED QUEUE (from Reference Model)
⚖️ COMPARE
✓ MATCH / ✗ MISMATCH
ACTUAL STREAM (from Monitor)
💻 Example 1: Self-Checking ALU Scoreboard
Step 1 — The Transaction
// alu_txn.sv
class alu_txn extends uvm_sequence_item;
rand bit [7:0] a, b;
rand bit [1:0] op; // 00:ADD 01:SUB 10:AND 11:OR
bit [7:0] result; // captured from DUT
`uvm_object_utils_begin(alu_txn)
`uvm_field_int(a, UVM_ALL_ON)
`uvm_field_int(b, UVM_ALL_ON)
`uvm_field_int(op, UVM_ALL_ON)
`uvm_field_int(result, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "alu_txn");
super.new(name);
endfunction
endclass
Step 2 — The Scoreboard (predictor + checker inside)
// alu_scoreboard.sv
class alu_scoreboard extends uvm_scoreboard;
`uvm_component_utils(alu_scoreboard)
// TLM imp — receives every transaction from the monitor
uvm_analysis_imp #(alu_txn, alu_scoreboard) mon_imp;
int match_count = 0;
int mismatch_count = 0;
function new(string name, uvm_component parent);
super.new(name, parent);
mon_imp = new("mon_imp", this); // 'this' → imp calls our write()
endfunction
// ---------- Reference Model (Predictor) ----------
function bit [7:0] predict(alu_txn t);
case (t.op)
2'b00 : return t.a + t.b; // ADD
2'b01 : return t.a - t.b; // SUB
2'b10 : return t.a & t.b; // AND
2'b11 : return t.a | t.b; // OR
endcase
endfunction
// ---------- Checker ----------
// Called AUTOMATICALLY for every monitor transaction
function void write(alu_txn t);
bit [7:0] exp = predict(t);
if (t.result === exp) begin
match_count++;
`uvm_info("SCOREBOARD",
$sformatf("MATCH : a=%0d b=%0d → %0d", t.a, t.b, t.result), UVM_LOW)
end
else begin
mismatch_count++;
`uvm_error("SCOREBOARD",
$sformatf("MISMATCH: expected=%0d got=%0d", exp, t.result))
end
endfunction
// ---------- Final Verdict ----------
function void report_phase(uvm_phase phase);
`uvm_info("SCOREBOARD", $sformatf("Matches=%0d | Mismatches=%0d",
match_count, mismatch_count), UVM_LOW)
if (mismatch_count == 0)
`uvm_info("SCOREBOARD", "*** TEST PASSED ***", UVM_LOW)
else
`uvm_error("SCOREBOARD", "*** TEST FAILED ***")
endfunction
endclass
Step 3 — Connect it in the Environment
// alu_env.sv
class alu_env extends uvm_env;
alu_agent agent;
alu_scoreboard sb;
`uvm_component_utils(alu_env)
function void build_phase(uvm_phase phase);
agent = alu_agent::type_id::create("agent", this);
sb = alu_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
agent.monitor.ap.connect(sb.mon_imp); // ◀ THE magic line!
endfunction
endclass
💻 Example 2: Queue-Based Scoreboard (2 data streams)
When expected and actual results arrive from different sources, use uvm_analysis_imp_decl to create two imps with separate write() methods, and store expected results in a queue:
// Declare imps with unique write method names
`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp_exp #(mem_txn, fifo_scoreboard) expected_imp;
uvm_analysis_imp_act #(mem_txn, fifo_scoreboard) actual_imp;
mem_txn exp_q[$]; // golden queue
function new(string name, uvm_component parent);
super.new(name, parent);
expected_imp = new("expected_imp", this);
actual_imp = new("actual_imp", this);
endfunction
// From the reference model — store expected
function void write_exp(mem_txn t);
exp_q.push_back(t);
endfunction
// From the monitor — pop & compare (in-order)
function void write_act(mem_txn t);
if (exp_q.size() == 0) begin
`uvm_error("SB", "Actual arrived with empty expected queue!")
return;
end
mem_txn e = exp_q.pop_front();
if (e.data === t.data)
`uvm_info("SB", "MATCH ✓", UVM_HIGH)
else
`uvm_error("SB", $sformatf("MISMATCH exp=%0h got=%0h", e.data, t.data))
endfunction
endclass
🔄 Out-of-order responses? Replace the queue with an associative array keyed by transaction ID, and delete the entry on match instead of popping.
Alternative: uvm_subscriber shortcut
class my_scoreboard extends uvm_subscriber #(alu_txn);
`uvm_component_utils(my_scoreboard)
// imp is built-in — just implement write():
function void write(alu_txn t);
// checking logic here
endfunction
endclass
✅ Key Takeaways
✔ Scoreboard = predict + compare + verdict
✔ Uses analysis TLM connections (monitor.ap → sb.imp)
✔ write() is a function — no time-consuming code inside
✔ Use === (4-state compare) to catch X/Z values
✔ Use queues for in-order, associative arrays for out-of-order
✔ Final PASS/FAIL verdict goes in report_phase / check_phase
🎤 Interview Quick-Fire
Q: uvm_analysis_port vs uvm_analysis_imp?
A: Port broadcasts transactions (monitor side); imp receives them and invokes the attached component's write() (scoreboard side).
Q: Why must write() be a function, not a task?
A: Analysis traffic is instantaneous broadcast — it cannot block or consume simulation time.
Q: How do you handle responses that return out of order?
A: Key an associative array with a unique transaction ID; look up and delete on arrival.
Q: What happens if two scoreboards connect to one analysis port?
A: Both receive every transaction — analysis ports support multiple subscribers.
Q: Where should the predictor live?
A: Either inside the scoreboard, or as a separate component feeding an "expected" analysis stream — the second scales better for complex designs.
⭐ If this helped you, share it with your verification team! ⭐