Protected Course Page
Enter the password to view Day 9 of the UVM 30-Day plan.
Day 9: Advanced Scoreboards — Two-Import Pattern, Out-of-Order Comparison, and uvm_tlm_analysis_fifo
A 30-day SystemVerilog/UVM verification series. The FIFO scoreboard was easy because writes and reads looked different. Real protocols don't. Today the scoreboard gets serious.
The scoreboard from Days 4 and 7 had it easy. Each transaction carried both wr_en and rd_en, so a single write() function could look at the flags and decide which side to update. That works for a FIFO. It falls apart on any real protocol, because:
- Write and read transactions often arrive on different interfaces, from different monitors.
- The transaction types are often different classes — a write command versus a read response, not two variants of one item.
- The DUT can reorder transactions. Two reads issued in order may come back out of order, and the scoreboard must still match them.
- The comparison can be slow — a reference model, a memory lookup, a set of protocol assertions — and it must not slow the monitor.
Today's four mechanisms solve those four problems. They compose into the standard advanced-scoreboard architecture you will find in every mature verification environment.
1. The Two-Import Pattern
When two different monitors publish to the same scoreboard, the scoreboard cannot just define one write() function. UVM provides the `uvm_analysis_imp_decl macro to generate distinct import types, each with its own write_ method:
`uvm_analysis_imp_decl(_wr)
`uvm_analysis_imp_decl(_rd)
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp_wr #(fifo_tx, fifo_scoreboard) wr_export;
uvm_analysis_imp_rd #(fifo_tx, fifo_scoreboard) rd_export;
function new(string name, uvm_component parent);
super.new(name, parent);
wr_export = new("wr_export", this);
rd_export = new("rd_export", this);
endfunction
function void write_wr(fifo_tx tr);
// called only by wr_export
endfunction
function void write_rd(fifo_tx tr);
// called only by rd_export
endfunction
endclass
In the env, connect each monitor to the correct import:
wr_agent.mon.ap.connect(sb.wr_export);
rd_agent.mon.ap.connect(sb.rd_export);
Now the scoreboard has no ambiguity. It knows which side each transaction came from without inspecting the transaction fields. If a third monitor appears (say, a coverage collector), it gets a third import or a third `uvm_analysis_imp_decl.
write() functions? Because both would have the same signature and the same name — the compiler can't distinguish them, and UVM's analysis port calls a single known method. The macro's whole job is to produce a distinct method name per import type.
2. uvm_tlm_analysis_fifo — Decoupling Producer and Consumer
Remember the Day 4 warning: write() is called synchronously by the publisher. If write() does a comparison that takes hundreds of nanoseconds of simulation time, the monitor slows down and starts missing transactions. For a simple scoreboard, that does not matter because comparison is cheap. For a reference model or a slow checker, it matters enormously.
The fix is a buffer between the monitor and the scoreboard: uvm_tlm_analysis_fifo. It has an analysis import on one side and a blocking get on the other. The monitor pushes transactions in; a separate thread in the scoreboard pulls them out at whatever pace it likes. Nothing on the producer side ever blocks.
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_tlm_analysis_fifo #(fifo_tx) wr_fifo;
uvm_tlm_analysis_fifo #(fifo_tx) rd_fifo;
function new(string name, uvm_component parent);
super.new(name, parent);
wr_fifo = new("wr_fifo", this);
rd_fifo = new("rd_fifo", this);
endfunction
virtual task run_phase(uvm_phase phase);
fork
process_writes();
process_reads();
join
endtask
task process_writes();
fifo_tx tr;
forever begin
wr_fifo.get(tr); // blocking pull, in its own thread
// slow comparison — safe here, no monitor is waiting
$display("got write: %0h", tr.wdata);
end
endtask
task process_reads();
fifo_tx tr;
forever begin
rd_fifo.get(tr);
$display("got read: %0h", tr.rdata);
end
endtask
endclass
In the env, connect to the fifo's analysis export rather than the scoreboard directly:
wr_agent.mon.ap.connect(sb.wr_fifo.analysis_export);
rd_agent.mon.ap.connect(sb.rd_fifo.analysis_export);
uvm_analysis_imp is simpler and equally correct.
3. Out-of-Order Comparison
When the DUT can return reads in a different order than they were issued, an in-order shadow queue is wrong. There are two standard answers:
3.1 Sorted comparison by transaction ID
Every read carries a tag or ID. The scoreboard stores the expected result in an associative array keyed by ID. When the observed read arrives, the scoreboard looks up the ID and compares:
fifo_tx expected_by_id [bit [7:0]];
function void write_exp(fifo_tx tr);
expected_by_id[tr.id] = tr;
endfunction
function void write_obs(fifo_tx tr);
fifo_tx expected;
if (!expected_by_id.exists(tr.id)) begin
`uvm_error("SB", $sformatf("unexpected ID %0h", tr.id))
return;
end
expected = expected_by_id[tr.id];
expected_by_id.delete(tr.id);
if (tr.rdata !== expected.rdata)
`uvm_error("SB", $sformatf("ID %0h mismatch: exp %0h got %0h",
tr.id, expected.rdata, tr.rdata))
endfunction
This handles arbitrary reordering and is the most common pattern in bus protocols with transaction IDs (AXI, PCIe, and many custom buses).
3.2 Two analysis fifos, compared in arrival order
If the DUT is in-order but the two sides (expected and observed) arrive at different simulation times, use two uvm_tlm_analysis_fifos and a third thread that pulls one from each and compares:
task process_writes();
fifo_tx exp_tr;
forever begin
wr_fifo.get(exp_tr);
compare_one(exp_tr);
end
endtask
task compare_one(fifo_tx exp_tr);
fifo_tx obs_tr;
rd_fifo.get(obs_tr); // blocking; waits for the matching read
if (obs_tr.rdata !== exp_tr.wdata)
`uvm_error("SB", $sformatf("mismatch: exp %0h got %0h",
exp_tr.wdata, obs_tr.rdata))
endtask
rd_fifo.get(obs_tr) blocks forever, the scoreboard never returns, the objection is never dropped, and the test hangs. Either (a) wrap the get in a timeout with uvm_wait_for_nba_region or a fork/join_any with a #timeout branch, or (b) rely on check_phase to catch the leftover expected reads and trust the run to end via objections from the test. For a real project, choose the timeout — the "hang forever" failure mode wastes far more time than a "test timed out" failure.
4. Reference Model Integration
For anything more complex than a FIFO, the expected value is not the input value. It's the input value transformed by the DUT's specification. That transform lives in a reference model — sometimes called a predictor or a golden model.
The reference model is a component that receives the stimulus side and publishes the expected side:
class fifo_ref_model extends uvm_component;
`uvm_component_utils(fifo_ref_model)
uvm_analysis_imp #(fifo_tx, fifo_ref_model) in_imp;
uvm_analysis_port #(fifo_tx) out_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
in_imp = new("in_imp", this);
out_ap = new("out_ap", this);
endfunction
function void write(fifo_tx tr);
fifo_tx predicted = fifo_tx::type_id::create("predicted");
// model the DUT's transform — e.g., invert, add one, apply a mask
predicted.wr_en = tr.wr_en;
predicted.wdata = ~tr.wdata;
out_ap.write(predicted);
endfunction
endclass
The env then wires three things together: the driver's analysis port (or a copy of the stimulus) into the model, the model's output into the scoreboard's expected side, and the monitor's output into the scoreboard's observed side.
// Env connect_phase
drv.ap.connect(model.in_imp);
model.out_ap.connect(sb.wr_export);
mon.ap.connect(sb.rd_export);
The scoreboard does not know the reference model exists. It just compares two streams. That is the point: each stage knows only its input and its output.
5. Putting It All Together
Here is the full advanced scoreboard, in the "expected by ID, reference model output, buffered on both sides" pattern that real projects use:
`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_obs)
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp_exp #(fifo_tx, fifo_scoreboard) exp_export;
uvm_analysis_imp_obs #(fifo_tx, fifo_scoreboard) obs_export;
fifo_tx expected [bit [7:0]];
int unsigned num_exp, num_obs, num_mismatch, num_orphan;
function new(string name, uvm_component parent);
super.new(name, parent);
exp_export = new("exp_export", this);
obs_export = new("obs_export", this);
endfunction
function void write_exp(fifo_tx tr);
if (expected.exists(tr.id)) begin
`uvm_error("SB", $sformatf("duplicate expected ID %0h", tr.id))
num_mismatch++;
return;
end
expected[tr.id] = tr;
num_exp++;
endfunction
function void write_obs(fifo_tx tr);
fifo_tx e;
if (!expected.exists(tr.id)) begin
`uvm_error("SB", $sformatf("unexpected observed ID %0h", tr.id))
num_orphan++;
return;
end
e = expected[tr.id];
expected.delete(tr.id);
num_obs++;
if (tr.rdata !== e.rdata) begin
num_mismatch++;
`uvm_error("SB", $sformatf("ID %0h mismatch: exp %0h got %0h",
tr.id, e.rdata, tr.rdata))
end
endfunction
function void check_phase(uvm_phase phase);
if (num_exp == 0 && num_obs == 0)
`uvm_error("SB", "no traffic observed")
if (num_mismatch)
`uvm_error("SB", $sformatf("%0d mismatches", num_mismatch))
if (num_orphan)
`uvm_error("SB", $sformatf("%0d orphan observed transactions", num_orphan))
if (expected.size())
`uvm_error("SB", $sformatf("%0d expected transactions never observed",
expected.size()))
endfunction
endclass
Notice the four checks in check_phase: traffic existed, no mismatches, no orphans, and no leftovers. Those four cover the standard failure modes of any scoreboard. If any of them is missing, a real bug can slip through silently.
6. Common Advanced Scoreboard Bugs
| Symptom | Cause |
|---|---|
Scoreboard crashes because write() is defined twice | Forgot `uvm_analysis_imp_decl, or used the wrong suffix in the export type |
| Simulation slows down after adding a checker | Slow work done inside write() instead of in a pulled thread via uvm_tlm_analysis_fifo |
| Test hangs at end | Scoreboard thread blocked on get for a transaction that never came |
| Scoreboard passes a broken DUT | Only checks mismatches, not orphans or leftovers; a dropped transaction looks like success |
| Out-of-order DUT always reports mismatch | In-order shadow queue used on a reordering DUT |
| Reference model checks pass even when the driver is wrong | Expected data came from the monitor instead of from the driver |
7. What Day 10 Will Add
Tomorrow: Functional Coverage. A scoreboard tells you the DUT did the right thing on the transactions it saw. Coverage tells you which transactions it didn't see. That is the difference between "the test passed" and "the test passed and it exercised what I meant it to." You will write covergroups, coverpoints, and crosses, and learn how to tie them to a verification plan so closure is measurable.
8. Quiz
Ten questions on the two-import pattern, uvm_tlm_analysis_fifo, out-of-order comparison, and reference models. Options shuffled per load.
Coming Up Next: Day 10 — Functional Coverage
- Covergroups and coverpoints — how to declare what you meant to exercise, not just what happened to run.
- Cross coverage — the joint conditions that catch real bugs, and why single coverpoints give false confidence.
- Sampling — where and when coverage is sampled, and why sampling at the wrong time silently reports zero hits.
- Tying to a verification plan — mapping each coverpoint to a line in the plan so coverage closure means something concrete.
Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular. Day 7 made it yours. Day 8 made it coordinated. Day 9 made it rigorous.
No comments:
Post a Comment