Verilator Deep Dive: Open-Source Simulation That Can Actually Replace Your Commercial Tool
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
Xpropagation. Your unknown-state coverage comes from separate tools (or a different simulator). - No delays.
#10nsis 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 toinitial 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:
|
|
And a C++ testbench:
|
|
Build and run:
|
|
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:
- Parses and elaborates the RTL into an AST.
- Performs dataflow analysis to determine which signals drive which.
- 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.). - Topologically sorts the combinational logic within each region.
- 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.
|
|
|
|
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++:
|
|
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:
|
|
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:
|
|
Coverage
Verilator supports line, toggle, and functional coverage. Enable during build:
|
|
At the end of your testbench, write out the coverage:
|
|
Merge multiple runs with verilator_coverage:
|
|
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:
|
|
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:
#10nsdelays.@(posedge clk)andwaitin testbench code.fork...join,fork...join_none,fork...join_any.- Event variables.
This lets you write Verilog testbenches that run under Verilator:
|
|
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++.
|
|
|
|
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.$urandomis seeded from+ntbopts=+verilator_seed+Nor the Verilator runtime API.$finish,$stop— terminate simulation.
$dumpfile/$dumpvars work but are generally superseded by the C++ trace API.
Some things are approximated:
$timereturns the Verilator internal time in units of--timescale. Works as expected.$realtimeis an integer in Verilator. Close enough for most purposes.$value$plusargsworks.
Parameterization at build time
Verilator compiles parameters into the generated code. You can override with -G:
|
|
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 soperfresults 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++:
|
|
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:
- Profile first.
--prof-cfuncsplusperf record ./obj_dir/Vtopplusperf reportshows you the hot functions in generated code. Usually the bottleneck is one specific combinational cloud or one memory array. - Reduce tracing. VCD is slow. FST is faster. Disable tracing when not needed. Trace only specific scopes.
- Compile with
-O3. Default is-O2;-O3can give 10–30% speedup at the cost of longer compile time. - Try
--threads. On big designs, it helps. On small ones, it hurts. --output-splitlets more compile happen in parallel and reduces memory pressure on the linker.- Do not use
--publicunnecessarily. Each exposed signal blocks some optimizations. - Check for inefficient RTL. Huge combinational cones, deep case statements evaluated every cycle — these cost time. The RTL fix is often the right fix.
--x-assign 0instead of--x-assign unique— slightly faster startup, slightly less debugging.- 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:
|
|
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_PRINTFandVL_DEBUG_IF: the C++ macros Verilator uses internally; you can inject diagnostic prints in generated code via DPI.- Use
--stats: writesobj_dir/Vtop__stats.txtwith 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