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 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 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.
/* 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 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.
// snippet: the import, inside the bench package
import "DPI-C" function int unsigned dma_c_model(input int unsigned data, input int unsigned key);// 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// 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
endclassThe test is a type override on top of dma_check_test: same jobs, same comparator, different model.
With Verilator, the C file is simply one more source on the command line, plus the define:
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 : 0The 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).
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 |
|---|---|---|
|
| Signed, 2-state |
|
| What the DMA model uses |
|
| From |
|
| Packed vectors as 32-bit chunks; logic adds an X/Z plane |
|
| |
|
| Do not keep the pointer after the call |
|
| An opaque handle the C side owns |
open array |
| Query bounds with |
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.
// 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);/* 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:
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 wordsA 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.
Flow | How the C file gets in |
|---|---|
Verilator | List the |
Commercial simulators (VCS, Xcelium, Questa) | Most accept a |
Any IEEE 1800 simulator | Build a shared library yourself ( |
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.
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.
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.
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.
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.
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.