The SoC team wants to boot the operating system on the RTL before tape-out. Someone does the arithmetic: the boot needs a few billion clock cycles, and the full-chip simulation runs at a few hundred cycles a second. That is months of wall-clock time for one boot. Meanwhile your UVM bench drives the chip's DMA through a driver full of @(posedge vif.clk) and the occasional #2ns. The project buys time on an emulator. What part of your bench can go into the box, and what has to change?
An event-driven simulator evaluates the design one event at a time on a CPU, so it slows down as the design grows. An emulator maps the design onto special hardware (arrays of custom processors or of FPGAs, depending on the vendor) that evaluates the whole design every clock in parallel. An FPGA prototype goes further: the design is synthesized and placed onto FPGAs much like a real chip, and runs faster still, with less visibility. The speeds differ by orders of magnitude, and that is the reason all three exist:
Platform | Typical speed on a large SoC | Visibility | Turnaround after an RTL change | Best for |
|---|---|---|---|---|
RTL simulation | Hz to kHz | Every signal, any time | Minutes | Block and subsystem DV, debug, coverage closure |
Emulation | Hundreds of kHz to a few MHz | Full, within capture windows | Hours | Full-chip tests, OS boot, HW/SW integration, long scenarios, power and performance runs |
FPGA prototype | Several MHz to tens of MHz | Limited: chosen probes | Hours to a day; bring-up takes weeks | Software development, real-world interfaces, demos |
The speeds are orders of magnitude, not benchmarks: real numbers depend on design size, the platform and, above all, on how often the box has to stop and talk to the host. That last point drives every change to the testbench. Think of a factory (the box) and a head office (the host). The factory is fast as long as it is not on the phone. A testbench that calls head office every clock for permission to wiggle a pin makes the factory as slow as the phone line.
This is the split HVL/HDL testbench (also called a dual-top or two-kingdom bench). The HDL side holds the DUT, the clock and reset generation, and BFMs (bus functional models): interfaces whose tasks turn one transaction into pin activity, written in a synthesizable style the emulator compiler accepts. In practice that rules out, on the HDL side, classes, randomization, # delays and wait on time, dynamic arrays and queues, and $display-driven logic; communication with the HVL side goes only through the vendor's transactor channel (SCE-MI pipes, or DPI function calls the compiler supports at the boundary). The HVL side holds every UVM class. A UVM driver becomes a thin proxy: it gets an item and calls a BFM task with its fields. A monitor becomes a proxy that receives completed transactions from a BFM call.
The link between the halves is standardized by Accellera's SCE-MI (Standard Co-Emulation Modeling Interface). Its function-based use model reuses DPI-style calls in both directions; its pipe-based model streams transactions through buffers so the box does not stop for every one. Each vendor adds its own transactor libraries and compilers on top, but the rules for the HDL side are the same everywhere.
The model below is the pattern in miniature, in plain SystemVerilog so it runs anywhere. axis_bfm is the HDL side: one clocked task per beat and a monitor that pushes each completed beat out with a zero-time, one-way call. stream_proxy is what the UVM driver would be: it never touches a pin.
// A split testbench in miniature. The HDL side (interface + top) holds everything clocked;
// the HVL side (package + class) is untimed and talks to it only through task and function calls.
package hvl_pkg;
int unsigned seen[$];
// Inbound, one-way and zero-time: the HDL monitor calls this; it never blocks.
function automatic void mon_write(input logic [31:0] w);
seen.push_back(w);
endfunction
endpackage
// HDL side: the BFM. Clocked tasks only (no # delays, no classes, no hierarchical references).
interface axis_bfm (input logic clk);
logic [31:0] tdata;
logic tvalid = 1'b0;
logic tready;
// One whole beat per call: the call boundary is a transaction, not a pin wiggle.
task automatic send(input logic [31:0] w);
tdata <= w;
tvalid <= 1'b1;
do @(posedge clk); while (!tready);
tvalid <= 1'b0;
endtask
// The monitor lives on the HDL side too, and pushes finished transactions out.
always @(posedge clk) if (tvalid && tready) hvl_pkg::mon_write(tdata);
endinterface
// HVL side: the proxy a UVM driver would be. It only calls the BFM's task.
class stream_proxy;
virtual axis_bfm vif;
function new(virtual axis_bfm vif);
this.vif = vif;
endfunction
task send_pkt(int unsigned n, logic [31:0] base);
for (int unsigned i = 0; i < n; i++) vif.send(base + i);
endtask
endclass
module top;
logic clk = 1'b0;
always #5 clk = ~clk; // in a box, the emulator's clock generator replaces this line
axis_bfm bfm (clk);
// A sink that stalls every third cycle, so the BFM must wait on TREADY.
int unsigned cyc = 0;
always @(posedge clk) cyc <= cyc + 1;
assign bfm.tready = (cyc % 3 != 2);
initial begin
stream_proxy p = new(bfm);
p.send_pkt(5, 32'hC0DE_0000);
@(posedge clk);
$display("HVL saw %0d beats, first %08h, last %08h", hvl_pkg::seen.size(),
hvl_pkg::seen[0], hvl_pkg::seen[hvl_pkg::seen.size() - 1]);
if (hvl_pkg::seen.size() == 5 && hvl_pkg::seen[4] == 32'hC0DE_0004) $display("PASS");
else $display("FAIL");
$finish;
end
endmoduleHVL saw 5 beats, first c0de0000, last c0de0004
PASSFive beats arrive in order even though the sink stalls every third cycle; the proxy never knew about the stall. That is the property you want: all timing knowledge lives in the BFM. The course bench's agents already lean this way, and Interface abstraction shows the abstract-BFM variant (a class handle instead of a virtual interface) that some emulation flows prefer.
In a simulation-only bench | In an emulation-ready bench | Why |
|---|---|---|
Driver wiggles pins through | Driver calls one BFM task per transaction | Each host call stops the box; fewer, bigger calls keep it fast |
| Waits counted in clocks, inside the BFM | The box has no notion of absolute delay except its clocks |
Clocking blocks and | Sampling on clock edges inside the BFM | The compiler maps clocked logic; the class side is untimed |
Hierarchical references ( | Signals brought out through ports, interfaces or the vendor's probe mechanism | Cross-boundary references are not synthesizable |
Monitor builds an item per beat on the host | BFM packs the beat and sends it in one one-way call | Inbound calls should be zero-time and non-blocking |
Clock and reset from | Clock generation inside the HDL top, in the form the tool supports | Clocks belong to the box |
The checking and stimulus layers, which are most of the bench, do not change. Sequences, the scoreboard, the coverage model and the register layer all work on transactions, so a well-layered bench moves with only its drivers and monitors rewritten. Teams that know emulation is coming write the split from day one, so the same bench runs in simulation and in the box.
Even an emulator is slow for a CPU booting an operating system. Hybrid emulation keeps the processor subsystem as a fast instruction-level model in a virtual platform on the host (often SystemC TLM) and puts the rest of the SoC in the box, linked by transactors. Software boots on the fast model, then drives the real RTL of the blocks under test. The trade is accuracy: the CPU is no longer cycle-accurate RTL, so CPU-timing bugs need a pure-RTL run later.
Debug changes too. A box runs too fast to record everything forever. You set triggers (a signal value, a transaction, an assertion firing) and the platform captures a window of full visibility around it, which you then open in a waveform viewer. Emulators keep that capture deep and flexible; FPGA prototypes see only the probes compiled in, so a missing probe means a new compile. A good habit is to reproduce a box failure as a short simulation from a saved state, where every signal is visible.
A team moved their bench to an emulator and measured a speed-up of almost nothing. The profile showed the box idle most of the time, waiting on the host. The culprit was a monitor that called back to the host on every clock edge, valid or not, to let a class decide whether a beat had happened, and a driver that returned to the host between the address and data phases of every access. Each call forced the box and the host to synchronize. Moving the valid check and the whole two-phase access into the BFM, so the host saw one call per completed transaction, gave them the expected speed. The rule to remember: count host calls per DUT clock; the closer to zero, the faster the box.
#3ns after each APB access to meet a hold time on a model. It works in simulation. What happens when you try to compile the bench for emulation, and what is the fix?The delay is in the HVL class, which is untimed, or in an interface the emulator must synthesize; either way absolute delays are not supported in the box. The fix is to express the wait in clocks inside the BFM (repeat (n) @(posedge clk)), or to remove it if it only existed to work around a race, which clocked BFMs avoid.
Every call synchronizes the box with the host, so the emulator runs at host-call speed instead of hardware speed. Move the completion detection (valid and ready, or PSEL/PENABLE/PREADY) into the BFM and call the host once per completed transfer with the packed fields.
The drivers and monitors of the APB, stream and interrupt agents would become proxies plus BFMs in the interfaces, and the top would generate clocks and reset in the tool's supported form. The sequences, dma_scoreboard, the predictor, the coverage model, the register model and the tests stay as they are, because they only see transactions.
Next: the box runs RTL, but the host side often runs models written by other teams in other languages. Multi-Language Benches shows how SystemC, C++ and Python plug into a UVM flow.