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.
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
// 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/* 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
chandle carries opaque C state — SV never inspects it, only passes it back to C.
The wrapper's predict() is the only API the scoreboard sees: pkt_txn in, pkt_txn out.
Packing/unpacking lives in the transaction class (pack_bytes/unpack_bytes) — one place to fix when fields change.
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.