Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
    • Scoreboard Architecture: Expected vs Actual
    • Reference Models: Predictors, DPI-C and Built-in Comparators
      • Predictor and Reference-Model Design
      • DPI-C Reference Models
      • Built-in Comparators and the Class-Comparator Trap
    • Coverage & Closure
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Reference Models: Predictors, DPI-C and Built-in Comparators
  1. Topics
  2. UVM Verification
  3. Checking & Coverage in UVM
  4. Reference Models: Predictors, DPI-C and Built-in Comparators
  5. Predictor and Reference-Model Design

Part 8 · Checking & Coverage · Intermediate

Predictor and Reference-Model Design

Why the reference model is its own component, what it must model and what it must ignore, and how to keep it independent of the RTL it judges.

On this page

    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.

    The idea: the answer key and the marker are different jobs

    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.

    The picture: two inputs, one model, one stream of expectations

    Flowchart: the APB monitor feeds write_pred_apb, which tracks KEY. The stream-in monitor feeds write_pred_in, which calls predict using that key. predict publishes on exp_ap to the comparator's before_export; the stream-out monitor feeds the comparator's after_export.
    Data flow inside dma_env_check. Register writes update the model's state; each input word becomes one expected item on exp_ap; the comparator pairs expected and actual items in order.

    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.

    Build it: a predictor with one virtual model function

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 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.

    Wiring it into the env

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // 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
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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

    Watch it run: two independent checkers agree

    Diagram
    +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 :    0

    Fourteen 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.

    Predict what, not when

    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.

    What breaks: the key change the model believed

    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:

    Diagram
    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 mismatches

    Both 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 fix: model the register rule, not just the register

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 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
    endclass

    With 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.

    What should the model do when the spec is silent?

    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.

    Keep the model independent of the RTL author

    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.

    Interview

    "Where does your reference model live, and how do you know it is right?"

    A weak answer: "inside the scoreboard; it is right because the tests pass." Passing tests prove the model and the RTL agree, not that either matches the spec.

    A strong answer: "In a predictor component that subscribes to the input monitors and publishes expected transactions, with the model in one virtual function so we can swap a C model or a fault-injected model by factory override. It predicts values, not cycles; timing requirements are assertions. It is written from the spec by someone other than the designer, takes state only from monitors, and every spec ambiguity is a named assumption with a plan item. When it disagrees with the RTL, we resolve it against the spec, not by editing whichever side is easier."

    Follow-ups: What does your model do with a register write that gets SLVERR? (ignore it: the DUT did not change) and When would you make a model cycle-accurate? (only where the cycle timing is the specification, such as a fixed-latency pipeline).

    Check yourself A colleague's predictor gets the key from the test through config_db, "because the test knows what it wrote." What can this hide?

    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.

    Check yourself The DMA gains a mode in which the output lags the input by exactly 3 clocks, and the spec states that latency as a requirement. Do you make the predictor cycle-accurate?

    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.

    Check yourself After you fix the predictor for the busy rule, the comparator passes but the original scoreboard still reports 5 mismatches. Should you delete the scoreboard?

    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.

    Related topics

    • DPI-C reference models

    • Scoreboard architecture

    • Register model (RAL)

    Previous

    134 / 280

    Next: DPI-C Reference Models

    Up next

    Up nextDPI-C Reference Models

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Built-in Comparators and the Class-Comparator Trap