Part 7 · Advanced & Integration · Intermediate

C Reference Models

When C models win, wrapper classes that hide DPI, transaction-in/transaction-out interfaces, and keeping the model versioned with the spec.

When a C model wins

Most scoreboards predict expected behavior with SystemVerilog code. Reach for a C reference model when one of three conditions holds:

  • A golden model already exists — the algorithm team's C/C++ implementation, the architect's simulator, or the firmware codec. Re-implementing it in SV creates a second source of truth that will drift.

  • The DUT is algorithm-heavy — compression, crypto, DSP, error correction. C with real integer/float arithmetic and existing test vectors beats reimplementing bit-exact math in SV classes.

  • Speed matters — a C model runs orders of magnitude faster than equivalent SV class code, and can be unit-tested outside the simulator in milliseconds.

Conversely, keep the model in SV when behavior is protocol/timing-centric rather than data-transform-centric, or when the team has no C build infrastructure — a DPI boundary you cannot debug is worse than slower SV.


The wrapper class: hide DPI from the TB

Raw DPI imports leak C-isms (handles, flat byte buffers, init/free calls) all over the testbench. The fix is a thin SV wrapper class with a transaction-in / transaction-out interface: the scoreboard hands it a transaction object and receives a predicted transaction back, with all packing, chandle management, and DPI calls hidden inside.

diagram
REFERENCE MODEL INTEGRATION [SV] [C]

  monitor (input side)                    monitor (output side)
        │                                        │
        ▼ req_txn                                ▼ 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)
            ▼
  ┌─────────────────────┐
  │  C golden model     │  ← versioned with the spec,
  │  model_create()     │    unit-tested standalone
  │  model_process()    │
  │  model_destroy()    │
  └─────────────────────┘

Full integration example

systemverilog
// ref_model_pkg.sv
package ref_model_pkg;

  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[]);

  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 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
c
/* 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];
    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 */

    for (int i = 0; i < m; i++)
        *(unsigned char*)svGetArrElemPtr1(rsp, svLow(rsp,1)+i) = out[i];
    return m;
}

Walkthrough

  1. chandle carries opaque C state — SV never inspects it, only passes it back to C.

  2. The wrapper's predict() is the only API the scoreboard sees: pkt_txn in, pkt_txn out.

  3. Packing/unpacking lives in the transaction class (pack_bytes/unpack_bytes) — one place to fix when fields change.

  4. model_process is a thin DPI shim; golden_process is the unmodified, spec-versioned model.


Keeping the model versioned with the spec

  • Pin the model version: the C model lives in its own repo with tags matching spec revisions; the TB build pulls a tagged version, never head.

  • Expose a version import (string model_version()) and print it in the test banner — mismatched-model debug sessions start with that line.

  • Unit-test the C model standalone with spec test vectors — DPI bugs and model bugs must be separable.

  • Treat model behavior changes like RTL changes: a model update that flips regression results needs the same review as a design fix.

Key takeaways

  • Use C models for existing golden code, algorithm-heavy DUTs, and speed — not for protocol/timing prediction.

  • Wrap DPI in one SV class with a transaction-in/transaction-out API; the scoreboard never sees a chandle.

  • C owns its state via chandle and frees it via an explicit destroy import — symmetric create/destroy.

  • Version the model with the spec and print the version at runtime — drift between model and spec is the classic failure mode.

Common pitfalls

  • Reimplementing an existing golden model in SV 'to avoid DPI' — two sources of truth, guaranteed drift.

  • Scattering raw DPI imports across the scoreboard and sequences — every model change touches the whole TB.

  • Forgetting model_destroy at end of test — C-side leaks accumulate across a long regression process.

  • Letting the TB build pull the model's head revision — regressions change behavior without any TB diff.