LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Verilator Deep Dive: Open-Source Simulation That Can Actually Replace Your Commercial Tool

verilatoredartlsimulationsystemverilogcppsystemcopen-sourcehardware

For most of its life, Verilator was “the fast open-source simulator that does not really handle SystemVerilog or testbenches.” For the last several years, that has stopped being true. Modern Verilator is a genuinely capable simulator that compiles your RTL to cycle-accurate C++ that runs 10–100× faster than event-driven commercial simulators on real workloads. It cannot do everything Questa or VCS can — no standalone UVM, limited force/release, no four-state X/Z by default — but the things it does do, it does faster, cheaper, and with better CI story than any commercial alternative.

This post is a tour of Verilator in 2026: what it actually does, how its compilation model differs from event-driven simulators, how to build a testbench that integrates with your RTL, how coverage and tracing work, and where Verilator beats commercial tools (and where it does not).

The model: 2-state cycle simulation compiled to C++

Most commercial simulators are event-driven: they maintain a queue of signals scheduled to change, process events in topological order, and re-evaluate the parts of the netlist affected by each event. This is general, accurate, and slow. It also supports 4-state logic (0, 1, X, Z), which is essential for gate-level simulation and useful for RTL when you care about uninitialized state.

Verilator takes a different approach. It reads your (System)Verilog, does the usual elaboration and optimization, and then emits C++ that evaluates the design cycle-by-cycle using 2-state logic. The generated code does not maintain an event queue; it just evaluates the entire design (or the parts the compiler proved relevant) on each clock edge in a fixed order computed from the static dataflow.

Consequences:

  • Fast. On big designs, 10–100× faster than event-driven simulators. A RISC-V core that runs 20 kHz in VCS runs 2 MHz in Verilator.
  • Free-ish. LGPLv3 / Perl Artistic. You can build it into commercial products.
  • 2-state only. No X propagation. Your unknown-state coverage comes from separate tools (or a different simulator).
  • No delays. #10ns is effectively ignored; simulation is purely cycle-accurate around clock events.
  • Good UVM support as of 5.x, but not as mature as commercial simulators. Most people run UVM in Questa/VCS and use Verilator for fast directed tests and formal pre-checks.
  • C++ testbench. Your testbench is C++ code that calls model->eval() and pokes signals. This is unfamiliar to someone used to initial begin ... end, but it is also why Verilator integrates beautifully with CI, with CocoTB, with SystemC, and with real software.

A minimal Verilator example

Imagine a simple counter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// counter.sv
module counter #(parameter WIDTH = 8) (
    input  logic             clk,
    input  logic             rst_n,
    input  logic             enable,
    output logic [WIDTH-1:0] count
);
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)       count <= '0;
        else if (enable)  count <= count + 1;
    end
endmodule

And a C++ testbench:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// sim_main.cpp
#include "Vcounter.h"
#include "verilated.h"
#include "verilated_vcd_c.h"

int main(int argc, char** argv) {
    Verilated::commandArgs(argc, argv);

    auto* top = new Vcounter;
    Verilated::traceEverOn(true);
    auto* tfp = new VerilatedVcdC;
    top->trace(tfp, 99);
    tfp->open("counter.vcd");

    vluint64_t time = 0;

    // Reset
    top->clk    = 0;
    top->rst_n  = 0;
    top->enable = 0;

    auto tick = [&]() {
        top->clk = 0; top->eval(); tfp->dump(time++);
        top->clk = 1; top->eval(); tfp->dump(time++);
    };

    for (int i = 0; i < 5; i++) tick();

    top->rst_n  = 1;
    top->enable = 1;
    for (int i = 0; i < 100; i++) tick();

    tfp->close();
    delete top;
    return 0;
}

Build and run:

1
2
3
4
5
verilator --cc --trace --exe --build -j 0 \
    counter.sv sim_main.cpp

./obj_dir/Vcounter
# counter.vcd is created

That is the whole flow. A GTKWave window on counter.vcd shows the waveform. No vendor license, no long startup, no project file — make and go.

The --cc flag generates C++ (there is also --sc for SystemC). --trace enables VCD trace. --exe says “generate a makefile that links the testbench into an executable.” --build -j 0 runs make for you with all available cores.

How Verilator’s scheduler actually works

Under the hood, when Verilator compiles your design it:

  1. Parses and elaborates the RTL into an AST.
  2. Performs dataflow analysis to determine which signals drive which.
  3. Splits the design into regions based on the IEEE scheduling semantics: active (non-blocking assignments and combinational logic), NBA (non-blocking assignment updates), observed (assertions), postponed ($monitor, etc.).
  4. Topologically sorts the combinational logic within each region.
  5. Emits C++ functions (eval(), eval_step(), and submodule functions).

The eval() method, when you call it after changing inputs, runs enough of the schedule to reach a stable state. In practice for most RTL that means “run all combinational logic fed by the changed signal, then run sequential logic on the next clock edge.”

Important: Verilator does not run in timed simulation the way VCS does. You advance “time” by toggling a clock signal and calling eval(). If you use --timing (newer Verilator versions), delays like #10ns are honored for testbench code — but for production, most designs stay cycle-driven.

CocoTB and Verilator

If you do not want to write C++, CocoTB is a Python testbench framework that runs on top of Verilator (and VCS, Questa, Icarus, Xcelium). You write coroutines that await signal transitions, drive and monitor in Python, and integrate with pytest for a real test framework.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge

@cocotb.test()
async def counter_counts(dut):
    cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())

    dut.rst_n.value  = 0
    dut.enable.value = 0
    await RisingEdge(dut.clk)
    await RisingEdge(dut.clk)

    dut.rst_n.value  = 1
    dut.enable.value = 1

    for expected in range(1, 20):
        await RisingEdge(dut.clk)
        assert int(dut.count.value) == expected, \
            f"expected {expected}, got {int(dut.count.value)}"
1
2
3
4
5
6
7
8
# Makefile for cocotb + Verilator
SIM = verilator
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = $(PWD)/counter.sv
TOPLEVEL = counter
MODULE   = test_counter

include $(shell cocotb-config --makefiles)/Makefile.sim

make and the tests run. CocoTB on Verilator gives you a testbench stack with Python ergonomics and Verilator’s speed — a combination commercial simulators cannot really match.

SystemC integration

Verilator can emit SystemC modules instead of pure C++:

1
verilator --sc --exe --build sim_main.cpp counter.sv

The generated class is a sc_module with sc_in/sc_out ports. This is the path you use when your testbench is already SystemC (TLM models, virtual platform, reference model). Verilator-generated modules play nicely with other SystemC modules in a simulation.

Waveform tracing: VCD and FST

Verilator supports three trace formats:

  • VCD (--trace) — universal, verbose, large files.
  • FST (--trace-fst) — 10–50× smaller, faster to write. Recommended.
  • SAIF — for switching activity / power analysis tools.

FST is usually the right default:

1
verilator --cc --trace-fst --exe --build sim_main.cpp counter.sv

In the testbench, VerilatedFstC replaces VerilatedVcdC. GTKWave, Surfer, and other waveform viewers all support FST natively.

For very large designs, you often want to trace only specific modules or a time window:

1
2
3
4
5
6
auto* tfp = new VerilatedFstC;
top->trace(tfp, 99);           // depth of hierarchy to trace
tfp->open("sim.fst");

// Later, only dump during a specific window:
if (time > START && time < END) tfp->dump(time);

Coverage

Verilator supports line, toggle, and functional coverage. Enable during build:

1
verilator --cc --coverage --exe --build sim_main.cpp counter.sv

At the end of your testbench, write out the coverage:

1
VerilatedCov::write("coverage.dat");

Merge multiple runs with verilator_coverage:

1
2
verilator_coverage --write merged.dat run1.dat run2.dat run3.dat
verilator_coverage --annotate-all --annotate logs/ merged.dat

The annotate output produces annotated source files with coverage counts — readable by humans, not a fancy GUI, but works fine in CI and code review.

For functional coverage, Verilator supports covergroup / coverpoint / cross syntax. The gap vs commercial tools is that commercial tools have richer built-in UCDB-style merging and database tools; Verilator’s story is “merge with verilator_coverage, report with your own scripts.”

Assertions

Verilator supports SystemVerilog Assertions (SVA)assert, assume, cover, concurrent assertions with property and sequence. Enable with --assert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// in the RTL
initial assert (WIDTH > 0) else $error("WIDTH must be positive");

always_ff @(posedge clk) begin
    if (enable) assert (count != 8'hff) else $error("count overflow");
end

// Concurrent
property no_enable_when_reset;
    @(posedge clk) disable iff (!rst_n)
    enable |-> ##[1:3] count != '0;
endproperty
assert property (no_enable_when_reset);

Violations show up as C++ exceptions or errors at runtime; configure their behavior with --assert and +assert/+noassert plusargs.

Verilator covers the commonly-used SVA subset. Some advanced features (e.g., complex implication operators with local variables in certain contexts) are still maturing; your mileage will vary on esoteric assertions.

Timing (--timing) and mixed testbenches

Until recently, Verilator did not support #delay, @event, or wait. Modern Verilator has --timing, which handles:

  • #10ns delays.
  • @(posedge clk) and wait in testbench code.
  • fork...join, fork...join_none, fork...join_any.
  • Event variables.

This lets you write Verilog testbenches that run under Verilator:

1
2
3
4
5
6
7
8
module tb;
    logic clk = 0;
    always #5 clk = ~clk;

    initial begin
        #100 $finish;
    end
endmodule

With --timing, this simulates. There is a performance cost — cycle-accurate code still runs fast, but timed code runs through an event queue. Commercial simulators are still the right tool for deeply timed testbenches; Verilator + --timing is for bridging RTL-level code that happens to have some timed constructs.

UVM runs on top of --timing as of Verilator 5.x. Many components of Accellera UVM work. Full UVM-with-complex-phasing on Verilator is still a work in progress — check your version.

DPI: calling C++ from RTL

SystemVerilog DPI-C lets your RTL call C functions and vice versa. Verilator supports it. Example: a golden-reference model in C++.

1
2
3
4
5
6
7
8
9
import "DPI-C" function int golden_compute(input int a, input int b);

module wrapper;
    int result;
    initial begin
        result = golden_compute(3, 4);
        $display("result = %0d", result);
    end
endmodule
1
2
3
4
5
6
// golden.cpp
#include "Vwrapper__Dpi.h"

extern "C" int golden_compute(int a, int b) {
    return a + b;
}

Verilator generates Vwrapper__Dpi.h with the prototypes; you implement them. DPI is the usual way to bring in constrained-random stimulus (calling a C++ random generator), integrate with tracing/logging infrastructure, or drop in a functional reference model.

$readmemh, $fopen, $display: the Verilog standard library

Most of what you expect from the $* system tasks works:

  • $display, $write, $strobe, $monitor — formatted output.
  • $fopen, $fclose, $fwrite, $fscanf — file I/O.
  • $readmemh, $readmemb — load memory arrays from hex/binary files.
  • $random, $urandom, $urandom_range — pseudo-random numbers. $urandom is seeded from +ntbopts=+verilator_seed+N or the Verilator runtime API.
  • $finish, $stop — terminate simulation.

$dumpfile/$dumpvars work but are generally superseded by the C++ trace API.

Some things are approximated:

  • $time returns the Verilator internal time in units of --timescale. Works as expected.
  • $realtime is an integer in Verilator. Close enough for most purposes.
  • $value$plusargs works.

Parameterization at build time

Verilator compiles parameters into the generated code. You can override with -G:

1
verilator --cc counter.sv -Gcounter.WIDTH=16 ...

For really dynamic testbenches, you regenerate at the right width. This is faster than you might think because Verilator’s compilation is fast for small designs.

Flags and knobs worth knowing

  • -Wall — warn on everything. Enable, fix warnings incrementally, then keep it enabled in CI.
  • -Wno-UNUSED / -Wno-UNDRIVEN — silence noise from generated code.
  • --lint-only — lint without compiling. Great as a cheap pre-commit check.
  • -Wno-fatal — turn warnings from errors back into warnings. Sometimes necessary when integrating third-party IP.
  • --threads N — generate multithreaded simulation that runs the design across N threads. Effective on large designs; overhead hurts on small ones.
  • --output-split 20000 — split generated C++ into files of at most 20k lines. Reduces compilation memory and parallelizes compile.
  • -O3 — aggressive optimization of the generated C++. Default is -O2.
  • --public-flat-rw — expose all signals as C++ public members. Useful for whitebox testbenches; bad for real product code because it defeats some optimizations.
  • --x-assign unique, --x-initial unique — assign unique random values to X/uninitialized signals. Helps catch code that silently depends on initialization.
  • --prof-cfuncs — annotate C++ with comments so perf results map back to RTL. Invaluable when profiling a slow sim.
  • --timing — enable delays and fork/join/wait.
  • --assert — enable SVA.

Multithreaded simulation

Verilator can emit multithreaded C++:

1
verilator --cc --threads 4 --exe --build design.sv sim_main.cpp

This partitions the design evaluation across threads. Not free — thread synchronization has overhead — but on large designs (a SoC with multiple cores, several DMA engines, large interconnect), you can see 2–3× speedup.

--threads-dpi all is needed if DPI functions are called from multiple threads simultaneously.

Where Verilator fits in a real flow

In a production EDA environment, Verilator is rarely the only simulator. A pragmatic flow looks like:

  • Pre-RTL / architecture: Verilator with a C++ or Python testbench for fast, directed tests of new blocks.
  • Block-level verification: CocoTB on Verilator for a huge percentage of tests; cover the common paths with constrained-random and coverage.
  • Full-chip SoC verification: Questa or VCS with UVM. Verilator for the fast-iteration part of the loop.
  • Formal properties and assertions: Run under Verilator as part of normal sim; run under JasperGold/SymbiYosys for real formal proof.
  • Gate-level simulation: Commercial simulator (Verilator does not do 4-state or timed post-synthesis sim).
  • Silicon bring-up / post-silicon: Verilator as the reference model you compare against real silicon traces.
  • CI: Verilator. No license, fast, scales to thousands of jobs. This is where Verilator really wins — you can afford to run all your tests on every commit.

When Verilator wins

  • CI and regression speed. 1000 directed tests in 10 minutes instead of 2 hours.
  • Large software-heavy testbenches. RISC-V cores running Linux — Verilator can simulate seconds of real time overnight.
  • Reference models and co-simulation with software stacks.
  • Early-stage development where a quick turn of edit/compile/simulate is worth more than 4-state accuracy.
  • Open-source / community designs where commercial licenses are a blocker.
  • Educational use. No-license means anyone with a laptop can run real RTL.

When Verilator is not enough

  • 4-state simulation for X-propagation, uninitialized storage bugs, tri-state buses. Use your commercial simulator.
  • Gate-level simulation with timing. Back-annotated SDF is out of scope.
  • Mature, heavy UVM with complex phasing, multi-agent environments, many sequencers. Use Questa/VCS. Verilator is catching up but is not there for big UVM environments.
  • Advanced assertion features like certain local variable uses in concurrent assertions.
  • Commercial IP from vendors that only ships encrypted for specific simulators.

Performance tuning

If your Verilator sim is slower than you expect:

  1. Profile first. --prof-cfuncs plus perf record ./obj_dir/Vtop plus perf report shows you the hot functions in generated code. Usually the bottleneck is one specific combinational cloud or one memory array.
  2. Reduce tracing. VCD is slow. FST is faster. Disable tracing when not needed. Trace only specific scopes.
  3. Compile with -O3. Default is -O2; -O3 can give 10–30% speedup at the cost of longer compile time.
  4. Try --threads. On big designs, it helps. On small ones, it hurts.
  5. --output-split lets more compile happen in parallel and reduces memory pressure on the linker.
  6. Do not use --public unnecessarily. Each exposed signal blocks some optimizations.
  7. Check for inefficient RTL. Huge combinational cones, deep case statements evaluated every cycle — these cost time. The RTL fix is often the right fix.
  8. --x-assign 0 instead of --x-assign unique — slightly faster startup, slightly less debugging.
  9. Consider running a smaller testbench design. Sometimes you are simulating more than you need.

CI integration

This is where Verilator shines. A minimal GitHub Actions workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
name: rtl-sim
on: [push, pull_request]
jobs:
  verilator:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Verilator
        run: |
          sudo apt-get update
          sudo apt-get install -y verilator gtkwave libsystemc-dev
      - name: Build
        run: verilator --cc --trace-fst --exe --build sim_main.cpp rtl/*.sv
      - name: Run tests
        run: ./obj_dir/Vtop --test-dir tests/
      - name: Upload waves on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: waves
          path: '*.fst'

Every PR runs the full regression. No license server, no queuing, no vendor dependency. This is the best feature Verilator has that you will not appreciate until you have lived without it.

Debugging tricks

  • --trace-fst-thread: offload trace writing to a separate thread. Big speedup when tracing is the bottleneck.
  • --trace-underscore: include signals starting with _ (Verilator’s generated temporaries). Occasionally useful for deep debugging.
  • --dump-tree: dump the elaborated AST. Useful when Verilator is elaborating your design in an unexpected way.
  • VL_PRINTF and VL_DEBUG_IF: the C++ macros Verilator uses internally; you can inject diagnostic prints in generated code via DPI.
  • Use --stats: writes obj_dir/Vtop__stats.txt with detailed stats about the generated model (number of vars, eval functions, splits). Useful for understanding simulation performance.
  • Don’t ignore lint warnings. Verilator’s lint is stricter than many commercial tools. Warnings often point at real bugs that will show up in silicon.

Verilator in 2026: what changed in the last few years

A rough summary of what has landed if you last looked at Verilator a few years ago:

  • Version 5.x brought --timing, which changes the simulator’s reach dramatically. Testbenches with delays work.
  • UVM support is usable (though not identical to commercial) as of 5.x.
  • Native FST tracing is stable and the default recommendation.
  • Multithreaded simulation is stable; the heuristics for partitioning designs have improved.
  • SVA support has grown; most common patterns work.
  • LLDB/GDB integration for debugging simulation from source works well.

Wrapping up

Verilator is the open-source tool that made “run your full regression on every commit” affordable for a huge fraction of the industry. It is not a full replacement for a commercial simulator — 4-state sim, gate-level sim, and mature UVM live in Questa and VCS — but it covers enough of the design-verification spectrum that most teams benefit from having it in their toolbox, usually as the primary engine for block-level CI and the secondary engine for SoC-level work.

If you have been putting off Verilator because of bad memories from the 3.x era, it is worth another look. The compilation model is unusual, the testbench pattern is C++-flavored, and the feature set has some gaps — but the speed, the CI integration, the zero-license-cost regressions, and the small-but-real community of people shipping production silicon with Verilator in the flow all add up to a tool worth learning deeply.

Comments