Three deliveries land in the same week. The architects send a SystemC TLM-2.0 model of the whole subsystem. The algorithm team sends a C++ library that computes the expected output of the DMA's new compression mode. The firmware team wants to run their boot code on the CPU in the RTL and see the DMA move data. And a new hire asks why the team down the hall verifies its blocks in Python. Your bench is UVM. Which of these does it connect to, how, and who decides?
UVM is good at structure: stimulus, checking, coverage, configuration and reuse. It is not where an algorithm's truth lives. That truth is usually written first by someone else, in the language they work in: architects in SystemC, algorithm engineers in C++ or Python, firmware engineers in C. Rewriting it in SystemVerilog creates a second copy that drifts from the first. So the usual answer is to call the original, and the engineering question is only how the call crosses the language boundary.
Think of an embassy. Your bench is the home country; each other language is a foreign one with its own laws. A bridge such as DPI is the embassy: a narrow, well-defined place where things cross, with rules about what may pass (simple data types, who owns memory, who may block). Most multi-language trouble comes from treating the border as if it were not there.
Calling a C reference model from SystemVerilog is covered with the course bench in DPI-C reference models: dma_predictor_dpi calls dma_c_model and agrees with the SystemVerilog predictor on 14 of 14 words. At project scale three more things matter. First, the C side sometimes needs to call back into SystemVerilog: to fetch data on demand, to log through the UVM report server, or to wait for simulated time. That is a DPI export, and the import that triggers it must be declared context. Second, models with state need one instance per DUT instance, usually held as a chandle returned by an init function. Third, a C++ model needs extern "C" on every function SystemVerilog sees, or the link fails on a mangled name (the course bench hit exactly this link error when its C model was first added).
A small complete example: the C model pulls its input words from SystemVerilog one call at a time, then reports back through an exported SV function.
// not-run: needs model.c compiled with it (verilator --binary top.sv model.c)
module top;
// The C side calls back into SV: export makes an SV function callable from C.
export "DPI-C" function sv_get_word;
export "DPI-C" function sv_note;
// context: the C function may call exported SV functions.
import "DPI-C" context function int unsigned c_dma_job(int unsigned key, int unsigned len);
int unsigned mem [16];
int unsigned n_notes;
function int unsigned sv_get_word(int unsigned idx);
return mem[idx];
endfunction
function void sv_note(string msg);
n_notes++;
$display("[SV] note from C: %s", msg);
endfunction
initial begin
int unsigned sum, exp;
foreach (mem[i]) mem[i] = 32'h1000_0000 + i;
sum = c_dma_job(32'hA5A5_0F0F, 5);
exp = 0;
for (int i = 0; i < 5; i++) exp += mem[i] ^ 32'hA5A5_0F0F;
$display("[SV] C model checksum %08h, SV expects %08h: %s, %0d note(s)",
sum, exp, sum == exp ? "PASS" : "FAIL", n_notes);
$finish;
end
endmodule#include <stdio.h>
#include "svdpi.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Exported from SV (the prototypes Verilator also writes to Vtop__Dpi.h). */
extern unsigned int sv_get_word(unsigned int idx);
extern void sv_note(const char *msg);
/* A C model that pulls its input words from the testbench, one call per word. */
unsigned int c_dma_job(unsigned int key, unsigned int len) {
unsigned int sum = 0;
char msg[64];
for (unsigned int i = 0; i < len; i++) sum += sv_get_word(i) ^ key;
snprintf(msg, sizeof msg, "job of %u words, key %08x", len, key);
sv_note(msg);
return sum;
}
#ifdef __cplusplus
}
#endif$ verilator --binary top.sv model.c --top-module top && ./obj_dir/Vtop
[SV] note from C: job of 5 words, key a5a50f0f
[SV] C model checksum 8c394b41, SV expects 8c394b41: PASS, 1 note(s)Two details make this work. The import is context, which tells the simulator the C function may call back into SystemVerilog; without it, calling an export is undefined. And the exported functions are functions, so they take no simulated time. If the C side must wait for the design ("run 100 clocks, then read"), export an SV task instead and import the C routine as a context task; the C call then blocks while simulated time advances, which only works on threads the simulator controls.
Architecture teams often build a virtual prototype in SystemC with TLM-2.0 sockets and the generic payload: the same transport concepts that UVM's own TLM-2.0 classes copy (see Sockets and the generic payload). Connecting the two means passing a generic payload across the SV/SystemC border. Simulators that run both languages in one kernel provide this through their own TLM bridge; UVM Connect, an open-source library originally from Mentor (now Siemens EDA), does it by registering each SystemVerilog port or socket and each SystemC socket under a shared string name and pairing them. Uses you will see:
The SystemC model as the reference model. UVM monitors send observed transactions to it; it returns predictions for the scoreboard. One model serves architecture, software and DV.
The SystemC model as the stimulus source. A software stack running on the virtual prototype issues transactions that UVM drivers turn into pin activity on the RTL.
Swapping a block. In a subsystem, one block runs as RTL while its neighbours stay as fast TLM models, so the RTL block sees realistic traffic early.
The hard parts are not the bridge. They are timing (a loosely timed model has no clock; its transactions carry approximate delays, and checking must not compare cycle timing against it) and ownership (a TLM-2.0 payload is passed by reference and may be reused by its initiator, so copy what you keep, the same rule as for UVM monitors).
When the SoC has a processor, many system-level tests are C programs compiled for that CPU, loaded into the memory model (for example with $readmemh on the RAM, or through a backdoor), and run on the RTL. The firmware does what real software will do: program the DMA, start it, take the interrupt. UVM stays in charge of the environment around it: external interfaces, scoreboards on the buses, and the verdict. The two sides talk through a mailbox: a reserved address that the C code writes to ("test passed", "test failed with code 3", a character for printf) and that a UVM monitor on the memory bus decodes into messages and objections. Synchronization works the same way in reverse: UVM writes a flag the C program polls.
This is also where Portable Stimulus (Accellera PSS) appears: one abstract scenario model from which a tool generates both UVM sequences for block level and C tests for the embedded CPU at SoC level, so a scenario written once runs at both levels.
cocotb runs testbenches written in Python against a standard HDL simulator (open-source ones such as Icarus and Verilator, and the commercial ones), driving and sampling signals through the simulator's VPI/VHPI interface with coroutines. Teams use it because Python is quick to write and its libraries (numpy, scipy) are often where the reference algorithm already exists. pyuvm implements UVM's classes in Python on top of cocotb for teams that want the structure without SystemVerilog. The listing below shows the shape of a cocotb test for the DMA's stream path.
# not-run: needs cocotb and a simulator
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
@cocotb.test()
async def xor_path(dut):
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
key, word = 0xA5A50F0F, 0x12345678
# ... program KEY over APB and start a one-word job, then:
while not (dut.m_tvalid.value and dut.m_tready.value):
await RisingEdge(dut.clk)
assert int(dut.m_tdata.value) == word ^ key, "output word mismatch"In real companies, cocotb and UVM usually coexist by block, not inside one bench. Small blocks, algorithm-heavy datapaths and FPGA teams often use cocotb; blocks that need reusable VIP, a register model, commercial coverage closure or integration into a UVM SoC environment stay in UVM. What crosses between them is models: the same C or Python reference used by both benches. Mixing Python components into a running UVM environment is possible through DPI and an embedded interpreter, but it is rare and fragile, so treat it as a last resort.
Situation | A good fit |
|---|---|
Reusable protocol VIP, register model, SoC integration | UVM in SystemVerilog |
Bit-exact algorithm already written in C/C++ | UVM with the model called through DPI |
Architecture model in SystemC already exists | UVM with a TLM bridge, or the model as the reference |
SoC scenarios that real software will perform | C tests on the CPU, with a UVM environment around them (or PSS for both levels) |
Small block, fast bring-up, Python-native team or models | cocotb (or pyuvm) |
A team called a C++ image-filter model through DPI and saw rare mismatches only in long regressions. The DUT was blamed for two weeks. The cause was on the border: the SystemVerilog side passed an open array to C, and the C++ model kept the pointer to use on the next call. The DPI standard says such a pointer is valid only for the duration of the call; the simulator later reused that memory, so the model sometimes read the next frame's pixels. The fix was to copy the data inside the call. The general rule: anything that crosses DPI (arrays, strings, handles) belongs to the side that created it, for as long as that side says, and a model that wants to keep data must copy it.
The simulator is blocked inside the C call, so simulated time never advances and the signal never changes: the run hangs. The C side must hand control back to the simulator: export an SV task that waits the cycles, import the C routine as a context task, and call the exported task from C. Better still, restructure the model so SystemVerilog calls it once per step.
The end of the C program must reach UVM through the mailbox: the program writes a pass or fail code to the reserved address, and a UVM monitor on that bus decodes it and drops the test's objection (or reports a failure). If the monitor never sees the write (wrong address, a cache holding the write, or no monitor at all), the objection stays raised and the run times out.
Keep the block-level UVM environment, because other teams reuse it and it uses shared VIP and a register model that cocotb would have to duplicate. If the attraction is Python reference models, share those with the UVM bench through DPI or a C export. cocotb is a better fit for a new standalone block or a Python-native team, not for a block whose environment is part of a larger UVM system.
Next: SystemC, C and Python are all still digital. Mixed-Signal Verification with UVM deals with the parts of the chip whose values are voltages.