The DMA bench has run clean for months. Then a firmware engineer reports that changing the key between two jobs sometimes scrambles the second job's data, and you write a test that updates KEY while a job is still running. The scoreboard reports five mismatches. Is the DMA broken? You read the RTL and find it ignores KEY writes while it is busy, which is arguably correct. The scoreboard, though, took the new key the moment it saw the write. The model was wrong, not the design, and finding that out took an afternoon because the prediction was buried inside the compare logic.
This lesson pulls the prediction out into its own component, shows what it must model (register rules included) and what it should ignore (timing), and ends with the habits that keep a model honest.
A teacher marking exams uses an answer key. Writing the key is one job: work out what each correct answer is. Marking is another: hold each paper against the key and count the differences. If the same sheet of paper does both, every fix to the marking risks changing the answers, and you cannot hand the key to a second marker.
In a UVM bench the predictor writes the answer key. It subscribes to the monitors on the DUT's inputs, including the register bus, and for each input it publishes an expected item. The function inside it that computes the answer is the reference model. The comparator (a built-in one, or the compare half of a scoreboard) does the marking: it pairs each expected item with an actual item from the output monitor. Split this way, the model can be swapped without touching the compare, the compare can be swapped without touching the model, and two checkers can share one model.
Notice what is missing: no arrow from a sequence or a driver. The predictor learns the key from the APB monitor, so a register write that never reached the DUT never reaches the model either.
dma_predictor has two analysis imps (declared with uvm_analysis_imp_decl so their write methods get distinct names) and one analysis port, exp_ap. The model is the virtual function predict(). Everything else is plumbing: tracking state from register writes and turning each input word into an expected item.
// snippet: from the course DMA bench
// The reference model as its own component: it watches register traffic and
// input words, and publishes one expected output item per input word.
class dma_predictor extends uvm_component;
`uvm_component_utils(dma_predictor)
uvm_analysis_imp_pred_apb #(apb_item, dma_predictor) apb_export;
uvm_analysis_imp_pred_in #(axis_item, dma_predictor) in_export;
uvm_analysis_port #(axis_item) exp_ap;
bit [31:0] key;
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
exp_ap = new("exp_ap", this);
endfunction
// The model itself: one virtual function, so a derived predictor (a buggy one
// for a demo, or one that calls a C model) changes only this.
virtual function bit [31:0] predict(bit [31:0] data);
return data ^ key;
endfunction
virtual function void write_pred_apb(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key = t.data;
endfunction
virtual function void write_pred_in(axis_item t);
axis_item e = axis_item::type_id::create("exp");
e.data = predict(t.data);
e.last = t.last;
exp_ap.write(e);
endfunction
endclass`predict()` is the only thing a variant overrides. dma_predictor_buggy replaces it with data ^ ~key for the comparator demo, and dma_predictor_dpi replaces it with a call into C. Neither touches the plumbing.
Failed writes are filtered. A KEY write that came back with PSLVERR did not change the DUT, so it must not change the model.
A fresh object per expected item. Subscribers may store what they receive, so exp_ap never republishes one reused handle.
dma_env_check extends the block env, so it keeps the original scoreboard and adds the predictor, a built-in comparator and coverage. dma_check_test swaps it in with one type override and otherwise runs the same three random jobs as dma_rand_test.
// snippet: from the course DMA bench
// The block env plus a separate predictor, a built-in comparator and coverage.
// use_class_cmp (config_db, default 0) swaps in uvm_in_order_class_comparator,
// which compares items with compare(): axis_item has no do_compare, so that
// comparator calls every pair a match.
class dma_env_check extends dma_env;
`uvm_component_utils(dma_env_check)
dma_predictor pred;
axis_in_order_comparator cmp;
axis_class_comparator class_cmp;
dma_coverage cov;
bit use_class_cmp;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(bit)::get(this, "", "use_class_cmp", use_class_cmp));
pred = dma_predictor::type_id::create("pred", this);
if (use_class_cmp) class_cmp = axis_class_comparator::type_id::create("class_cmp", this);
else cmp = axis_in_order_comparator::type_id::create("cmp", this);
cov = dma_coverage::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
apb.ap.connect(pred.apb_export);
in_agt.ap.connect(pred.in_export);
if (use_class_cmp) begin
pred.exp_ap.connect(class_cmp.before_export); // expected
out_agt.ap.connect(class_cmp.after_export); // actual
end else begin
pred.exp_ap.connect(cmp.before_export);
out_agt.ap.connect(cmp.after_export);
end
apb.ap.connect(cov.apb_export);
in_agt.ap.connect(cov.in_export);
endfunction
function void report_phase(uvm_phase phase);
if (use_class_cmp)
`uvm_info("CMP", $sformatf("class comparator: %0d matches, %0d mismatches", class_cmp.m_matches, class_cmp.m_mismatches), UVM_LOW)
else
`uvm_info("CMP", $sformatf("in-order comparator: %0d matches, %0d mismatches", cmp.m_matches, cmp.m_mismatches), UVM_LOW)
endfunction
endclass// snippet: from the course DMA bench
// Three random jobs with the checking env: the scoreboard and the separate
// predictor + built-in comparator must agree, and coverage reports its holes.
class dma_check_test extends dma_rand_test;
`uvm_component_utils(dma_check_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_env::type_id::set_type_override(dma_env_check::get_type());
super.build_phase(phase);
endfunction
endclass+UVM_TESTNAME=dma_check_test
UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
UVM_INFO dma_tb.sv(1639) @ 1305000: uvm_test_top.env [CMP] in-order comparator: 14 matches, 0 mismatches
UVM_ERROR : 0
UVM_FATAL : 0Fourteen words is 10 + 1 + 3, the three job lengths (random values differ by simulator and seed). The scoreboard and the predictor-plus-comparator are separate code paths fed by the same monitors, and they agree on every word. The coverage lines in the same log are the subject of Coverage & Closure.
The DMA's output timing depends on things the model should not care about: wait states on APB, gaps in the input stream, and backpressure from the slave. A model that predicts the value of the word survives all of them. A model that predicts the cycle on which the word appears has to reproduce the DUT's pipeline, and every retiming of the RTL breaks it.
Transaction-level model | Cycle-accurate model | |
|---|---|---|
Predicts | What comes out, in what order | What comes out on which clock |
Compare | Timing-agnostic: pair by order or by ID | Cycle by cycle against the pins |
Breaks when | The DUT legally reorders (use an ID-keyed compare) | Any pipeline change, stall or retiming |
Use it for | Almost everything: data paths, protocols, registers | Fixed-latency logic whose cycle timing is the spec |
Timing still matters when the spec makes it a requirement, such as "the interrupt rises within 4 clocks of the last beat" or a maximum latency. Check those with an assertion or a timestamp check next to the scoreboard, not by making the whole model cycle-accurate. The rule is one checker per kind of promise: the model checks data, assertions check timing.
Here is the opening story run for real. A scratch test (not part of the bench) starts an 8-word job with KEY = A5A50F0F, then writes KEY = 0000FFFF 80 ns later, while the DMA is busy. Scratch-run output, Verilator:
UVM_INFO rm_tb.sv(180) @ 275000: uvm_test_top [KEY] KEY written while the DMA is busy
UVM_ERROR dma_tb.sv(396) @ 305000: uvm_test_top.env.sb [SB_MISMATCH] expected b431da84, got 11942a74
UVM_WARNING @ 305000: uvm_test_top.env.cmp [Comparator Mismatch] data=11942a74 differs from data=b431da84
...
UVM_INFO dma_tb.sv(411) @ 665000: uvm_test_top.env.sb [SB] 3 words matched, 5 mismatched
UVM_INFO dma_tb.sv(1639) @ 665000: uvm_test_top.env [CMP] in-order comparator: 3 matches, 5 mismatchesBoth checkers fail the last five words, because both took the new key at once. The DUT did not: its register block writes LEN and KEY only if (!busy). The DMA kept using A5A50F0F for the whole job, which is a sensible design. The model simply did not know the rule. (The comparator's mismatches arrive as warnings, not errors; the built-in comparators lesson explains why that matters.)
The predictor needs one more piece of state, whether a job is running, and it can learn it from the same monitors: GO with a non-zero LEN starts a job, and the job ends when LEN words have gone in or an early TLAST cuts it short.
// snippet: a predictor that knows LEN and KEY are locked while a job runs
class dma_predictor_busy extends dma_predictor;
`uvm_component_utils(dma_predictor_busy)
bit busy;
int unsigned len_reg, words_in;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void write_pred_apb(apb_item t);
if (!t.write || t.slverr) return;
case (t.addr)
LEN: if (!busy) len_reg = t.data[15:0];
KEY: if (!busy) key = t.data;
CTRL: if (t.data[0] && !busy) begin busy = (len_reg != 0); words_in = 0; end // GO is ignored while busy
default: ;
endcase
endfunction
function void write_pred_in(axis_item t);
super.write_pred_in(t); // predict with the key in force
if (busy) begin
words_in++;
if (words_in == len_reg || t.last) busy = 0;
end
endfunction
endclassWith this predictor swapped in by a type override, the same scratch test gives in-order comparator: 8 matches, 0 mismatches, while the unchanged scoreboard still reports 3 words matched, 5 mismatched. That second line is the point: a model inlined in a scoreboard has to be fixed in every scoreboard that copied it; a model in a predictor is fixed once.
When the block has dozens of registers rather than five, decoding writes by hand stops scaling. Read the state from the register model instead: its mirror is kept up to date from the same bus monitor. Two cautions. The register model only knows a rule such as "locked while busy" if someone modelled it, so the predictor may still own that rule. And an analysis port calls its subscribers in an order you must not depend on, so a model that reads the mirror inside its own write from the bus monitor may see the value from before that write.
The DMA's register comment says KEY is read-write. It says nothing about writes during a job. The RTL designer made a choice; the model writer made the opposite one. Neither is a bug until someone decides what the spec means, and the worst outcome is quietly copying the RTL into the model so the mismatches go away: then the bench checks the design against itself.
Ask, in writing. File the question with the architect and point at the test that shows both behaviours.
Put the decision in one place. One function or one configuration knob, named after the rule, so the answer can be flipped without a hunt.
Record the assumption. A uvm_info at start of test ("model assumes KEY is locked while BUSY") makes it visible in every log.
Add it to the test plan. "KEY written while busy" becomes a plan item with coverage, so the case is exercised on purpose from now on.
A model and a design written by the same person from the same misreading agree perfectly, and the bench passes. Independence is what turns two implementations into a check. In practice that means a few rules:
Written from the spec, ideally by a verification engineer, never by porting the RTL's logic.
No hierarchical peeks into DUT internals for expected values. If the model reads dut.key, it agrees with the DUT by construction.
State comes from monitors (or a register model fed by monitors), never from what the stimulus intended. The end-of-test bug in the bench came from the same mistake in another form: the scoreboard decided nothing was pending from what it had seen, and passed with a word missing. The fix counted what the DUT had been asked to do, from the observed LEN and GO writes.
Any bug on the register path. If the APB write fails, lands on the wrong address, or is ignored because the DMA is busy, the DUT's key differs from the test's, and the scoreboard blames the data path or misses the bug. Predicting from the APB monitor checks that the write really happened and really took effect.
No. Keep the predictor transaction-level so data checking survives backpressure and future retiming, and add an assertion (or a timestamp check in the scoreboard) for the 3-clock latency. Each requirement then has one checker, and a latency bug is reported as a latency bug.
Not yet. Either fix the scoreboard's model too, or retire it deliberately once the predictor-plus-comparator path has the checks the scoreboard had (leftover and empty-compare checks included). Running two checkers that disagree, and ignoring one, is how real failures get waved through.
Next: the model is in one virtual function. In DPI-C Reference Models that function calls the architects' C code instead of re-implementing it.