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. DPI-C Reference Models

Part 8 · Checking & Coverage · Intermediate

DPI-C Reference Models

Calling a C reference model from a UVM predictor through DPI-C: imports and type mapping, open arrays and chandles, compile flows, and the failures that only C brings.

On this page

    The architecture team has a C model of the block. It is the reference the algorithm was signed off against, it has been run on millions of vectors, and it is a few thousand lines long. Your team could re-code it in SystemVerilog, and then spend a month finding out where the two copies disagree. Or the predictor could call the C model directly. Most teams take the second road, and then lose a day to a link error, a hang that the UVM timeout never catches, or an X that silently becomes 0. This lesson walks the road with the DMA's C model.

    The idea: a C function that looks like a SystemVerilog function

    The Direct Programming Interface (DPI) lets SystemVerilog call C functions, and C call exported SystemVerilog functions, with no wrapper layer such as VPI. An import "DPI-C" declaration tells the compiler the function's name and its argument types; from then on, SystemVerilog code calls it like any function. The call takes zero simulation time: the simulator stops, runs the C code to completion, and carries on with the result. Think of it as plugging a pocket calculator into the bench: fast, exact and trusted, but it has no idea what time it is.

    For a reference model that is exactly right. The predictor already isolates the model in predict(); a derived predictor replaces the body with one DPI call, and nothing else in the bench changes.

    The picture: one prediction, through C

    Sequence diagram: the stream-in monitor calls write_pred_in on dma_predictor_dpi, which calls the C function dma_c_model with data and key, receives data XOR key, and writes the expected item to the comparator.
    One input word on its way to an expected item. The C call returns before simulation time moves, so the expected item is published in the same time step as the input was observed.

    Build it: three small pieces

    The C model

    The DMA's model is one line of real work. The extern "C" guard is not decoration; the war story below is why it is there.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* dma_model.c - the DMA data path as a C reference model, called through DPI.
     * Compile it with the testbench and define DMA_DPI_MODEL.
     * extern "C" keeps the symbol name unmangled when a simulator compiles this
     * file as C++, so the DPI import can find it. */
    #include <stdint.h>
    
    #ifdef __cplusplus
    extern "C"
    #endif
    uint32_t dma_c_model(uint32_t data, uint32_t key) {
        return data ^ key;
    }

    The import and the predictor

    The import declares the C function with SystemVerilog types. int unsigned is a 2-state 32-bit unsigned integer, which maps to uint32_t (an unsigned int) on every common platform. The predictor overrides only predict(). Both sit inside ` ifdef DMA_DPI_MODEL `` in the bench, so a build without the C file still compiles.

    design.sv
    // snippet: the import, inside the bench package
    import "DPI-C" function int unsigned dma_c_model(input int unsigned data, input int unsigned key);
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet: from the course DMA bench
    class dma_predictor_dpi extends dma_predictor;
      `uvm_component_utils(dma_predictor_dpi)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function bit [31:0] predict(bit [31:0] data);
        return dma_c_model(data, key);
      endfunction
    endclass

    The test

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: from the course DMA bench
    // The same random jobs, predicted by the C model.
    class dma_dpi_test extends dma_check_test;
      `uvm_component_utils(dma_dpi_test)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void build_phase(uvm_phase phase);
        dma_predictor::type_id::set_type_override(dma_predictor_dpi::get_type());
        super.build_phase(phase);
      endfunction
    endclass

    The test is a type override on top of dma_check_test: same jobs, same comparator, different model.

    Watch it run

    With Verilator, the C file is simply one more source on the command line, plus the define:

    Diagram
    EXTRA_ARGS="+define+DMA_DPI_MODEL dma_model.c" run.sh dma_tb.sv +UVM_TESTNAME=dma_dpi_test
    
    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

    The SystemVerilog scoreboard and the C model agree on all 14 words. One habit is worth building now: prove the C code ran. Change ^ to + in the C file once and rerun. If the comparator still reports 14 matches, you are not running the model you think you are (a stale shared library, a missing define, or the SV predictor still in place).

    How do SystemVerilog types map to C?

    The import's argument types decide the C signature. Get them wrong and the code still compiles and links, because C symbols carry no types, and then passes garbage. Generate the C prototypes instead of typing them: most simulators can write a header from the imports (for example Questa's vlog -dpiheader), and Verilator writes one into its output folder.

    SystemVerilog

    C

    Note

    byte, shortint, int, longint

    char, short, int, long long

    Signed, 2-state

    int unsigned

    unsigned int (uint32_t)

    What the DMA model uses

    bit / logic (scalar)

    svBit / svLogic

    From svdpi.h

    bit [N:0] / logic [N:0]

    svBitVecVal* / svLogicVecVal*

    Packed vectors as 32-bit chunks; logic adds an X/Z plane

    real

    double

    string (input)

    const char*

    Do not keep the pointer after the call

    chandle

    void*

    An opaque handle the C side owns

    open array int unsigned a[]

    svOpenArrayHandle

    Query bounds with svLow, svHigh, svSize

    Common pitfall

    A 4-state value passed to a 2-state argument loses its X and Z bits: they become 0. If the stream monitor ever captures X on TDATA, the C model sees 0 and predicts happily. Check for X with $isunknown in the monitor or the predictor before calling C, or pass logic vectors and inspect the X/Z plane in C.

    Beyond one word: open arrays and stateful models

    Real C models rarely take two integers. They take whole packets and keep state between calls. Two DPI features cover that. An open array argument ([] in the import) passes a SystemVerilog array of any size; the C side gets a handle and walks it with the svdpi.h accessors. A chandle is an opaque pointer: C allocates a context, returns it, and SystemVerilog passes it back on every call without looking inside.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet: imports for a stateful model and a burst call
    import "DPI-C" function chandle      dma_ctx_new();
    import "DPI-C" function void         dma_ctx_set_key(chandle h, int unsigned key);
    import "DPI-C" function int unsigned dma_ctx_predict(chandle h, int unsigned data);
    import "DPI-C" function void         dma_ctx_free(chandle h);
    import "DPI-C" function void         dma_c_model_burst(input int unsigned din[], output int unsigned dout[],
                                                           input int unsigned key);
    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* snippet: the C side (with the extern "C" guard around it, as in dma_model.c) */
    #include <stdint.h>
    #include <stdlib.h>
    #include "svdpi.h"
    
    typedef struct { uint32_t key; uint32_t words; } dma_ctx_t;
    
    void *dma_ctx_new(void)                      { return calloc(1, sizeof(dma_ctx_t)); }
    void  dma_ctx_set_key(void *h, uint32_t key) { ((dma_ctx_t *)h)->key = key; }
    void  dma_ctx_free(void *h)                  { free(h); }
    
    uint32_t dma_ctx_predict(void *h, uint32_t data) {
        dma_ctx_t *c = (dma_ctx_t *)h;
        c->words++;
        return data ^ c->key;
    }
    
    void dma_c_model_burst(const svOpenArrayHandle din, const svOpenArrayHandle dout, uint32_t key) {
        for (int i = svLow(din, 1); i <= svHigh(din, 1); i++)
            *(uint32_t *)svGetArrElemPtr1(dout, i) = *(uint32_t *)svGetArrElemPtr1(din, i) ^ key;
    }

    The predictor creates the context in build_phase, forwards successful KEY writes to dma_ctx_set_key, calls dma_ctx_predict from predict(), and frees the context in final_phase. In a scratch run on the DMA bench (Verilator) that predictor matched all 14 words of dma_check_test's jobs, and the burst call returned the right four words:

    Diagram
    UVM_INFO rm_tb.sv(153) @ 0: uvm_test_top [BURST] open-array call: a5a50f0f a5a50f0e 5a5af0f0 b7915977
    UVM_INFO dma_tb.sv(1639) @ 1305000: uvm_test_top.env [CMP] in-order comparator: 14 matches, 0 mismatches
    UVM_INFO rm_tb.sv(132) @ 1305000: uvm_test_top.env.pred [CTX] C model predicted 14 words

    A chandle per predictor instance matters more than it looks. A C model with global variables is shared by every instance in the simulation: in the two-DMA subsystem bench, two predictors would overwrite each other's key. Context handles make each instance independent. The imports here are plain; mark one pure only if it has no side effects (the stateful calls do), and context only if the C code calls back into SystemVerilog. Exports and context calls in the SystemVerilog course has the full rules.

    Compiling the C file, simulator by simulator

    Flow

    How the C file gets in

    Verilator

    List the .c file with the sources. It is compiled by the C++ compiler in the generated makefile, which is why the extern "C" guard matters.

    Commercial simulators (VCS, Xcelium, Questa)

    Most accept a .c file on the compile command line and build it for you. Check your tool's DPI chapter for the exact switch and C compiler it uses.

    Any IEEE 1800 simulator

    Build a shared library yourself (gcc -shared -fPIC -o dma_model.so dma_model.c) and load it at run time with -sv_lib dma_model, the standard switch from the LRM, given without the file extension.

    The course's own Verilator flow compiles the UVM library with UVM_NO_DPI, which switches off the library's internal C code (for example HDL backdoor access). Your own imports are unaffected, which is why dma_dpi_test runs there.

    On EDA Playground

    You can add dma_model.c as an extra file next to design.sv and testbench.sv, and add +define+DMA_DPI_MODEL to the compile options. Whether the C file is then compiled depends on the simulator and how the site invokes it: some tools pick up C sources with the rest, others need a separate C compile step that a web flow may not expose. Use the "prove it ran" trick above to find out. If the C file is not compiled, the build fails at the import or the link, and the fallback is simple: run dma_check_test, whose SystemVerilog predictor gives the same answers.

    What breaks: the symbol the linker could not find

    When the DPI model was first added to the bench, the build failed at the link step: the import's function was undefined, although dma_model.c was plainly on the command line and compiled without complaint. The cause was that Verilator compiles user .c files with the C++ compiler. C++ mangles function names to encode their argument types, so the object file contained a decorated name rather than plain dma_c_model, and the DPI import, which always looks for the plain C name, found nothing. The fix is the guard now at the top of dma_model.c: under __cplusplus, the function is declared extern "C", which tells a C++ compiler to emit the unmangled name. The same file then works whether a tool compiles it as C or as C++.

    Two other failures are specific to C models, and both are worth recognising from the symptoms alone:

    • The simulation hangs and the UVM timeout never fires. A C function runs in zero simulation time, so an infinite loop inside it stops the clock for good. set_timeout and +UVM_TIMEOUT measure simulation time, which no longer moves, so they cannot help. Regressions need a wall-clock limit per job, and the C side should bound its own loops. To find the loop, attach a debugger to the stuck process; its stack shows the C frame.

    • The run dies with no UVM summary. A bad pointer in C crashes the whole simulator. There is no report_phase and no summary, and printf output still sitting in a buffer is lost. Call fflush(stdout) after diagnostic prints, and treat "no summary at all" as a crash in foreign code until proven otherwise.

    Interview

    "How would you integrate a C reference model into a UVM bench?"

    A weak answer: "import it with DPI and call it from the scoreboard." That is the syntax, not the integration.

    A strong answer: "Behind the predictor's virtual predict(), in a derived predictor selected by a factory override, so the SV model and the C model are interchangeable and the bench still builds without C. Imports use 2-state types with prototypes generated from the imports, X is checked before the call, state lives in a chandle context per instance, never in C globals. The C file carries an extern C guard so it links whether the tool compiles it as C or C++. I prove the C code ran by breaking it once, and the regression has a wall-clock limit, because a C loop stops simulation time and the UVM timeout with it."

    Follow-ups: What does `context` change on an import? (the C code may call exported SV functions or VPI, and the simulator must preserve the calling scope) and How do you pass a whole packet? (an open array, or a packed struct in a fixed layout).

    Check yourself Your C model works in Verilator but gives the wrong answer for one field on another simulator, with no warnings. Where do you look first?

    The argument types. C symbols carry no type information, so a mismatch between the import and the hand-written C prototype (an int against a long long, or a packed vector passed by value instead of as svBitVecVal*) links cleanly and passes garbage. Generate the prototypes from the imports with the simulator's header option and compile the C file against that header.

    Check yourself The two-DMA subsystem bench uses a C model with a global static uint32_t key. dma0's checks pass and dma1's fail. Why?

    Both predictors call the same C code, and there is one global key for the whole simulation. Each KEY write overwrites the other DMA's key. Move the state into a context struct allocated per predictor and passed as a chandle, so each instance has its own model.

    Check yourself A regression job runs for six hours of wall-clock time, then the farm kills it. The log ends mid-test, with no UVM_FATAL and no summary. What do you suspect, and why did the UVM timeout not fire?

    A hang inside foreign code, most likely the C model looping. The UVM timeout counts simulation time, and a C function runs in zero simulation time, so time stopped and the timeout never came due. Reproduce the job, attach a debugger to the process and read the stack, and give the regression a wall-clock limit so the next hang fails fast.

    Next: a model produces expected items, and something must compare them. Built-in Comparators and the Class-Comparator Trap looks at the comparators UVM ships, including one that agreed with eight wrong predictions.

    Related topics

    • Built-in comparators

    • SystemVerilog: DPI imports

    • SystemVerilog: passing arrays and structs

    • SystemVerilog: C reference models and debug

    Previous

    135 / 280

    Next: Built-in Comparators and the Class-Comparator Trap

    Up next

    Up nextBuilt-in Comparators and the Class-Comparator Trap

    Sign in to save progress

    Practice this lessonReport an error