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 |
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.
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.
// 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
endpackagechandle 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.
// 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.
// 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;
}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.
// 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 */
}Reproduce with the simulator under a debugger. DPI frames show up as ordinary C stack frames.
Suspect ownership first. Dangling open-array pointers, stored string pointers, and frees across the boundary cause most delayed crashes.
Check prototype agreement argument by argument. An SV int against a C long long corrupts the stack silently on some platforms.
Bisect the boundary. A stub C body that logs arguments and returns a constant separates call bugs from model bugs.
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 |
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.
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.
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