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

  • SystemVerilog Language Foundations34
  • Object-Oriented SystemVerilog for Verification33
  • Constraint Randomization — Interview-Critical Deep Dive56
  • SystemVerilog Assertions — Interview-Critical Deep Dive54
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog32
  • Advanced Language & Integration30
    • DPI-C: Calling C from SystemVerilog
      • DPI Imports
      • Exports & Context Calls
      • Passing Arrays & Structs
      • C Reference Models
      • DPI Pitfalls & Debug
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
    • RTL/TB Boundaries, Races & Timing
  • Senior & Interview Preparation29
Loading lesson

‹DPI-C: Calling C from SystemVerilog
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. DPI-C: Calling C from SystemVerilog
  5. C Reference Models

Part 7 · Advanced & Integration · Intermediate

C Reference Models & Debug

Wrap C golden models in SV classes, and build, link, and debug the SV/C boundary.

On this page

    When a C model wins

    Most scoreboards predict expected behavior with SystemVerilog code. A C reference model wins under three conditions.

    • A golden model already exists in C, such as the algorithm team's implementation or the firmware codec. Rewriting it in SV creates a second source of truth that drifts.

    • The DUT is algorithm-heavy, such as compression, crypto, or DSP code. C keeps the bit-exact math with existing test vectors.

    • Speed matters. A C model runs faster than equivalent SV class code and stays unit-testable outside the simulator.

    Keep the model in SV when behavior is protocol or timing centric. A DPI boundary without C build support costs more than slower SV.

    DUT behavior

    Model home

    Reason

    Data transform with golden C code

    C through DPI

    One source of truth, shared test vectors

    Protocol or timing prediction

    SystemVerilog

    No boundary to debug, direct access to clocks

    Wrapper class

    Raw DPI imports leak C details across the testbench. A thin SV wrapper class hides handles and buffers behind a transaction-in and transaction-out interface.

    Diagram
    REFERENCE MODEL INTEGRATION [SV] [C]
    
      monitor (input side)                    monitor (output side)
            |                                        |
            v req_txn                                v act_txn
      +---------------------+  exp_txn   +----------------------+
      |  ref_model wrapper  | ---------> |     scoreboard       |
      |  [SV class]         |            |  compare exp vs act  |
      |   pack txn -> bytes |            +----------------------+
      |   DPI calls         |
      |   bytes -> txn      |
      +---------+-----------+
                | chandle (opaque C state)
                v
      +---------------------+
      |  C golden model     |  <- versioned with the spec,
      |  model_create()     |    unit-tested standalone
      |  model_process()    |
      |  model_destroy()    |
      +---------------------+

    The next listing shows the wrapper with a minimal transaction type. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    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
    49
    50
    51
    52
    // not-run: needs its C half linked, and Verilator 5.048 rejects dynamic-array actuals for open arrays (Icarus 13 has no DPI)
    // ref_model_pkg.sv
    package ref_model_pkg;
      localparam int MAX_PKT = 4096;
    
      import "DPI-C" function chandle model_create(input int cfg);
      import "DPI-C" function void    model_destroy(input chandle h);
      import "DPI-C" function int     model_process(input  chandle h,
                                                    input  byte req[],
                                                    output byte rsp[]);
    
      typedef byte byte_da_t[];   // array return types need a typedef
    
      class pkt_txn;
        byte payload[];
        function byte_da_t pack_bytes();
          pack_bytes = new[payload.size()];
          for (int i = 0; i < payload.size(); i++)
            pack_bytes[i] = payload[i];
        endfunction
        function void unpack_bytes(byte data[]);
          payload = new[data.size()];
          for (int i = 0; i < data.size(); i++)
            payload[i] = data[i];
        endfunction
      endclass
    
      class ref_model;
        local chandle h;
    
        function new(int cfg);
          h = model_create(cfg);          // C owns the state
        endfunction
    
        // transaction in -> predicted transaction out
        function pkt_txn predict(pkt_txn req);
          byte_da_t req_b, rsp_b;
          int  n;
          pkt_txn exp = new();
          req_b = req.pack_bytes();       // txn -> flat bytes
          rsp_b = new[MAX_PKT];
          n     = model_process(h, req_b, rsp_b);
          rsp_b = new[n](rsp_b);
          exp.unpack_bytes(rsp_b);        // flat bytes -> txn
          return exp;
        endfunction
    
        function void cleanup();
          model_destroy(h);               // C frees its own state
        endfunction
      endclass
    endpackage
    • chandle carries opaque C state. SV never inspects it and only passes it back to C.

    • The wrapper predict call is the only API the scoreboard sees: transaction in, transaction out.

    • Packing lives in the transaction class, so field changes touch one place.

    • model_process is a thin DPI shim, while golden_process is the unmodified, spec-versioned model.

    The next listing is the thin C shim with a bound check on the stack buffers. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    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
    // not-run: illustrative C shim; golden.h and the golden model are not shown, and the course runner builds SV only
    /* golden_model.c — same source the algorithm team ships */
    #include <stdlib.h>
    #include "svdpi.h"
    #include "golden.h"          /* the spec-versioned model proper */
    
    void* model_create(int cfg)        { return golden_init(cfg); }
    void  model_destroy(void* h)       { golden_free(h); }
    
    int model_process(void* h, const svOpenArrayHandle req,
                      svOpenArrayHandle rsp)
    {
        int n = svSize(req, 1);
        unsigned char in[4096], out[4096];
        if (n > 4096) return 0;   /* guard: packet exceeds the stack buffers */
        for (int i = 0; i < n; i++)
            in[i] = *(unsigned char*)svGetArrElemPtr1(req, svLow(req,1)+i);
    
        int m = golden_process(h, in, n, out);   /* the real model */
        if (m > svSize(rsp, 1)) return 0;        /* guard: rsp is too small */
    
        for (int i = 0; i < m; i++)
            *(unsigned char*)svGetArrElemPtr1(rsp, svLow(rsp,1)+i) = out[i];
        return m;
    }

    The C model lives in its own repository with tags that match spec revisions. The testbench build pulls one tagged version and prints it in the test banner.

    The next listing unit-tests the golden model with plain gcc outside any simulator. It is a standalone C test, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // not-run: standalone C test built with gcc outside the simulator
    /* gcc -g -fsanitize=address -o golden_unit golden_unit.c golden.c */
    #include <stdio.h>
    #include "golden.h"
    
    int main(void)
    {
        unsigned char in[4] = {0x11u, 0x22u, 0x33u, 0x44u};
        unsigned char out[4];
        void *h = golden_init(0);
        int m = golden_process(h, in, 4, out);
    
        printf("vectors=%d first=%u\n", m, (unsigned)out[0]);
        golden_free(h);
        return 0;
    }

    Common pitfall

    Letting the testbench build pull the model head revision changes regression behavior with no testbench diff. Pin one tagged version and print it at run time.

    Build and threading

    Diagram
    DPI BUILD & LOAD FLOW [C] [SV]
    
      model.c --> gcc -c -fPIC --> model.o --+
      shim.c  --> gcc -c -fPIC --> shim.o  --+--> gcc -shared --> libmodel.so
    
      tb.sv (import "DPI-C" ...) --> compile and elaborate,
        then the simulator loads libmodel.so
        (-sv_lib libmodel or the vendor switch).
        Symbol "checksum_add" found? yes --> run.
        Otherwise elaboration reports an unresolved DPI import.
    • C++ name mangling hides DPI symbols from the SV import. Every DPI-visible function uses extern "C".

    • The C symbol must match the SV import name exactly, or the rename form is used.

    • Missing -fPIC breaks the shared link. A stale shared object on the load path breaks the run, so clean builds matter.

    Threads that the C model spawns must never call exported SV functions or any svdpi API. Worker threads write results into a C-side queue that the simulator thread drains.

    The next listing shows that queue pattern. It needs its C half linked through DPI-C, so it is not run in this course's checks.

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: illustrative C; the queue type and heavy_compute are not shown, and the course runner builds SV only
    /* SAFE: foreign thread -> queue -> simulator thread */
    #include "svdpi.h"
    
    static result_q q;                 /* mutex-protected C queue */
    
    extern void sv_result_ready(int id, int val);
    
    void* worker(void* arg) {          /* thread body: NO sv calls here */
        result r = heavy_compute(arg);
        q_push(&q, r);                 /* C-side queue only */
        return 0;
    }
    
    void poll_results(void)            /* context import, simulator thread */
    {
        result r;
        while (q_try_pop(&q, &r))
            sv_result_ready(r.id, r.val);   /* export: safe HERE */
    }

    Debug at the boundary

    1. Reproduce with the simulator under a debugger. DPI frames show up as ordinary C stack frames.

    2. Suspect ownership first. Dangling open-array pointers, stored string pointers, and frees across the boundary cause most delayed crashes.

    3. Check prototype agreement argument by argument. An SV int against a C long long corrupts the stack silently on some platforms.

    4. Bisect the boundary. A stub C body that logs arguments and returns a constant separates call bugs from model bugs.

    5. Build the C side with debug symbols and sanitizers for standalone unit tests. That catches the ownership bugs DPI hides.

    DPI is a function-call binding with near-zero overhead and no design introspection. VPI is a handle API for walking the design and registering callbacks, and it costs more per call.

    Need

    Interface

    Reason

    Run software models, codecs, utilities

    DPI

    Plain prototypes on both sides

    Inspect or instrument the design

    VPI

    Hierarchy handles and value callbacks

    Interview

    State the one-liner: DPI runs software through fast function binding, while VPI inspects the design through a slower handle API. Modern verification covers nearly all needs with DPI.

    Check yourself A regression crashes inside the simulator weeks after a C model change with no testbench diff. Which two versioning checks come first?

    The tag of the C model the build pulled and the version line in the test banner. An untagged model pull explains behavior changes with no testbench diff.

    Testbench

    The next listing proves the wrapper shape in plain SystemVerilog: a golden function, a thin task, and an oversize guard. It runs in this course's checks.

    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
    module tb_golden_wrap;
      localparam int BUF = 8;
      logic [7:0] vec [0:7];
      logic [7:0] got [0:7];
      logic [7:0] expv [0:7];
      int bad;
      int i;
      int n_out;
    
      function automatic logic [7:0] golden_step(input logic [7:0] din);
        golden_step = din ^ 8'h5A;
      endfunction
    
      task automatic dut_step(input logic [7:0] din, output logic [7:0] dout);
        dout = din ^ 8'h5A;
      endtask
    
      task automatic guarded_run(input int n, output int m);
        if (n > BUF) m = 0;   // oversize call flagged, nothing written
        else begin
          for (i = 0; i < n; i++) dut_step(vec[i], got[i]);
          m = n;
        end
      endtask
    
      initial begin
        bad = 0;
        for (i = 0; i < 8; i++) begin
          vec[i] = 8'h10 + i[7:0];
          expv[i] = golden_step(vec[i]);
        end
        guarded_run(8, n_out);
        if (n_out !== 8) bad++;
        for (i = 0; i < 8; i++) if (got[i] !== expv[i]) bad++;
        guarded_run(10, n_out);
        if (n_out !== 0) bad++;
        if (bad == 0) $display("PASS: wrapper matched the golden model on 8 vectors and flagged 1 oversize call");
        else $fatal(1, "FAIL: wrapper gave a wrong prediction");
        $finish;
      end
    endmodule

    Related topics

    • DPI Imports

    • Monitors and scoreboards

    Previous

    241 / 296

    Next: DPI Pitfalls & Debug

    Up next

    Up nextDPI Pitfalls & Debug

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • DPI Imports
    • Exports & Context Calls