Back to ChipVerify AI

Open EDA / Simulation

Verilator: The Practical Guide to Fast Open-Source Verilog Simulation

Verilator is the fastest free Verilog/SystemVerilog simulator and the only open one that scales to a full Linux-capable RISC-V SoC. Unlike VCS, Xcelium, or Questa, it is not an event-driven simulator: it compiles a synthesizable subset of Verilog into C++ (or SystemC) and runs it as a cycle-based, 2-state model. This guide covers the model, lint mode, building a simulation, tracing, coverage, the warnings you will actually hit, and the cases where you still need an event-driven simulator.

If you have ever waited 40 minutes for a commercial sim to boot Linux to a login prompt, Verilator is why that run can finish in a fraction of the time. It transpiles your RTL into a C++ class, your testbench links against it, and the whole thing compiles to a native binary that evaluates combinational and sequential logic in one pass per clock edge. There is no event queue, no per-delta scheduling, and no SDF back-annotation — which is exactly why it is so fast and why it is not a drop-in replacement for an event-driven sim doing gate-level signoff.

What Verilator is

Verilator is a free, open-source Verilog/SystemVerilog-to-C++ (or SystemC) compiler maintained by Wilson Snyder and the Verilator team at verilator.org (source on GitHub). The current stable line is the 5.x series, which also ships in the oss-cad-suite binary release alongside Yosys and SymbiYosys. It is dual-licensed under LGPL-3 and Artistic-2, so you can ship the generated C++ inside a proprietary product. verilator --version prints the build.

The mental model that matters: Verilator is a compiler first and a simulator second. It elaborates the design, schedules it, and writes C++ that is the model. You then compile and run that C++. Everything below follows from that.

The cycle-based 2-state model (and how it differs from event-driven)

A traditional event-driven simulator keeps an event queue and re-evaluates logic whenever a signal changes, honouring every #delay, every delta cycle, and the full IEEE 4-state value set (0, 1, X, Z). That is the correct model for gate delays, SDF back-annotation, and X-pessimism analysis — and it is also why those simulators are comparatively slow.

Verilator schedules the whole module hierarchy into a single evaluation function. For each clock edge it evaluates combinational logic in topologically sorted order via eval() and updates next-state. Two consequences follow:

  • 2-state by default. Signals are 0 or 1; there is no native X/Z the way a 4-state simulator models it. Verilator does handle tri-state/pull resolution and some unknown propagation, but it is not a full 4-state simulator. --x-assign and --x-initial control how uninitialised and X values resolve (0, 1, or random/unique) so you can deliberately stress reset and initialisation.
  • Cycle-based, not timed. Arbitrary intra-cycle #delay timing in the design under test is not modelled the way an event-driven sim does. Modern Verilator --timing does add support for delays, event controls, and fork/join — invaluable for behavioural testbenches — but the DUT itself is best kept to a synthesizable subset.

See blocking vs non-blocking assignments for why a cycle-based scheduler is unforgiving about ordering bugs that an event-driven sim might quietly tolerate.

Lint mode: the fastest win

Before you simulate anything, run Verilator as a linter. --lint-only elaborates and statically checks the design without emitting or compiling any C++, in milliseconds. It is a respected standalone static check even on teams that simulate with a different tool.

# Static check only — no C++, no build, runs in milliseconds.
verilator --lint-only -Wall top.sv

# Make the warnings you care about fatal in CI:
verilator --lint-only -Wall -Werror-WIDTH -Werror-CASEINCOMPLETE top.sv

-Wall turns on the strict warning set. Promote any message to a hard failure with -Werror-<MSG> (for example -Werror-WIDTH), and silence a known-safe one with -Wno-<MSG> or an inline /* verilator lint_off WIDTH */ pragma. Pair this with Verible lint for style rules Verilator does not enforce.

Building a simulation

Verilator produces C++. The 5.x --binary shortcut wraps verilate + compile + link in a single command and bakes in Verilator’s own default main(), so it is the path for SV-only designs and self-testing SystemVerilog testbenches — you do not pass a C++ harness with --binary. When you want your own C++ harness to drive the clock and reset, use the classic flow: --cc to emit C++, --exe to fold in your harness, and --build to run the generated makefile.

# 5.x one-shot: --binary verilates, compiles, and links Verilator's own
# default main -- so you do NOT pass a C++ harness here. Use it for SV-only
# designs, default-main runs, and self-testing SystemVerilog testbenches.
verilator --binary -j 0 -Wall --trace-fst top.sv -o sim
./obj_dir/sim +verilator+rand+reset+2   # --binary writes the exe under obj_dir/

# Classic flow: fold in your own C++ harness. --cc emits C++, --exe names the
# harness, --build runs the generated makefile.
verilator -Wall --cc --exe --build top.sv tb.cpp -o sim
./obj_dir/sim

# SystemC output for TLM / mixed-language integration
verilator --sc top.sv

A minimal C++ harness toggles the clock, manages reset, and (optionally) opens a waveform. eval() re-evaluates the design; final() runs any $final blocks at the end.

#include "Vtop.h"
#include "verilated.h"
#include "verilated_fst_c.h"   // FST tracing (use verilated_vcd_c.h for VCD)

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

    auto* dut = new Vtop;
    auto* tfp = new VerilatedFstC;
    dut->trace(tfp, 99);          // trace 99 levels of hierarchy
    tfp->open("dump.fst");

    vluint64_t t = 0;
    dut->rst = 1;
    for (int i = 0; i < 4000 && !Verilated::gotFinish(); ++i) {
        dut->clk = 0; dut->eval(); tfp->dump(t++);
        dut->clk = 1; dut->eval(); tfp->dump(t++);
        if (i == 4) dut->rst = 0; // deassert reset after a few cycles
    }

    tfp->close();
    dut->final();                  // run any $final blocks
    delete dut;
    return 0;
}

If you prefer Python, Verilator is a first-class cocotb backend: cocotb drives the same generated model through its own coroutine-based test harness, so you can write stimulus and checkers in Python while keeping Verilator’s speed.

Tracing: VCD and FST

Add --trace for a classic VCD dump, or --trace-fst for FST, which is substantially smaller and faster to write for long runs. In the harness, call Verilated::traceEverOn(true), open the trace, and dump(time) after each eval() (as in the harness above). FST opens directly in GTKWave or Surfer.

Once you have a dump, the hard part is reading it. See waveform intelligence for turning a multi-megabyte FST into a short list of suspect edges instead of scrubbing a timeline by hand.

Coverage

Verilator collects line, toggle, expression, FSM-state, and user (functional) coverage and writes a coverage.dat file. Enable the kinds you want at compile time, then merge and render with verilator_coverage:

# --coverage is the alias for every coverage kind (line, toggle, expression,
# FSM, and user). Build + run (self-testing SV, default main).
verilator --binary -j 0 -Wall --coverage top.sv -o sim
./obj_dir/sim                          # writes coverage.dat

# Merge runs into an LCOV report and render HTML.
verilator_coverage --write-info coverage.info coverage.dat
genhtml coverage.info -o cov_html
  • --coverage-line — basic block / statement coverage.
  • --coverage-toggle — every bit of every signal toggled 0→1 and 1→0.
  • --coverage-user — functional points you place with SVA cover / $coverage-style hooks. --coverage turns on the full set at once: line, toggle, expression, FSM (toggle of FSM states), and user coverage — not just these three.

Coverage shows what was exercised, not what is correct. A high toggle-coverage number with a weak checker still ships bugs — it is a completeness metric, not a correctness one.

The warnings you will actually hit

Verilator’s warning IDs are stable and worth memorising:

  • WIDTH — implicit width cast, e.g. a logic [3:0] assigned a 32-bit literal. See width mismatches in Verilog for why this one hides real bugs.
  • UNOPTFLAT — combinational loop or a design that can’t be flat-scheduled. Usually a real feedback bug; occasionally a false loop through a wide vector.
  • LATCH — a level-sensitive latch was inferred from a block you meant to be combinational. Same root cause as the Yosys/synthesis inferred latch warning.
  • CASEINCOMPLETE — a case without full selector coverage and no default; the usual precursor to a LATCH.
  • BLKANDNBLK — a signal written with both blocking and non-blocking assignments. Almost always a bug.
  • MULTIDRIVEN — a non-bus signal driven by two processes.
  • UNUSEDSIGNAL / UNUSEDPARAM — a signal or parameter declared but never read. (The older UNUSED name was retired in v5.000.) UNDRIVEN — read but never assigned — is the dual, but it is disabled by default; enable it with -Wwarn-UNDRIVEN. Frequently a typo or a generate block that elided itself for a parameter value.

Limitations, and when you still need an event-driven sim

  • Gate-level / SDF signoff. Verilator does not consume SDF and is not the tool for back-annotated gate-level timing simulation. That stays on an event-driven sim.
  • Full 4-state X-propagation. If your verification relies on precise IEEE 4-state X semantics and X-pessimism, use a 4-state simulator. Verilator’s --x-assign modes are a good stress test, not a replacement.
  • Full SVA and UVM. Verilator supports immediate assertions and a growing subset of concurrent SVA, and --timing has widened testbench support, but complex multi-clock properties and a full UVM environment (constrained randomisation, factories) still belong on a commercial simulator or a dedicated formal flow.

Verilator vs Icarus vs commercial

Pick the tool by the job, not by reputation:

  • Verilator — fastest by a wide margin for cycle accurate, synthesizable-subset simulation; the right choice for full-SoC throughput, CI, and C/C++ co-simulation. Compile step up front; 2-state by default.
  • Icarus Verilog — interpreted, event-driven, native 4-state. Slower, but no compile step and faithful to event/timing semantics for small designs and quick experiments.
  • Commercial (VCS, Xcelium, Questa) — event-driven, full 4-state, complete SVA/UVM, SDF/gate-level signoff. The reference for production verification and the tool you sign off on.

A common, healthy flow is: lint and fast iteration on Verilator, regressions and coverage on Verilator for throughput, and full UVM / gate-level signoff on a commercial sim. None of these is a substitute for the others. For static issues Verilator does not catch, pair it with Yosys synthesis checks and a broader RTL verification strategy.

How ChipVerify AI uses Verilator

ChipVerify AI runs Verilator as its coverage-oriented simulation engine (Icarus Verilog is the default simulation path). Simulation needs a testbench — one you upload or one ChipVerify generates — and with Verilator selected we build the DUT with --coverage-line --coverage-toggle, capture the waveform for download, and merge per-run coverage into the unified score. The WIDTH, UNUSEDSIGNAL, and BLKANDNBLK warnings surface as evidence chips next to matching Yosys and Verible findings. ChipVerify is a pre-signoff evidence tool in closed beta — it is not foundry signoff, and it does not replace the commercial sims described above.

FAQ

Is Verilator a 2-state or a 4-state simulator?

Verilator is 2-state by default: every signal is 0 or 1, and there is no native X (unknown) or Z (high-impedance) value the way an IEEE event-driven simulator models them. It does model some 4-state behaviour — tri-state and pull resolution, and unknown propagation in specific cases — but it is not a full 4-state simulator. Use --x-assign and --x-initial to control how uninitialised and explicitly-X values resolve (0, 1, or a random/unique pattern) so you can stress reset and initialisation. If your verification depends on precise IEEE 4-state X-propagation semantics, run a 4-state event-driven simulator instead.

What is the difference between Verilator and an event-driven simulator like VCS or Icarus?

An event-driven simulator (VCS, Xcelium, Questa, Icarus Verilog) maintains an event queue and re-evaluates logic whenever a signal changes, honouring #delays and delta cycles. Verilator instead compiles synthesizable RTL into a C++ class and evaluates the whole design once per call to eval(), in topologically sorted order. That cycle-based model is far faster but does not natively model arbitrary intra-cycle timing in the design under test. Recent Verilator (the --timing flag) supports delays, event controls, and fork/join in testbench-style code, but the DUT is still best kept to a synthesizable subset.

How do I run Verilator with just lint, without building a simulation?

Run verilator --lint-only -Wall top.sv. This elaborates and statically checks the design in milliseconds without generating C++ or compiling anything. -Wall enables the strict warning set (WIDTH, UNUSEDSIGNAL, UNUSEDPARAM, CASEINCOMPLETE, LATCH, and more); promote any warning to a hard failure for CI with -Werror-WIDTH (or the matching message name). UNDRIVEN is a real warning but is off by default, so enable it explicitly with -Wwarn-UNDRIVEN if you want it. Lint-only is a respected standalone static check even on teams that simulate with a different tool.

What is an UNOPTFLAT warning in Verilator?

UNOPTFLAT means Verilator found a combinational feedback path it could not order into a single flat evaluation pass — usually an unintended combinational loop, or a signal that feeds back through itself within a cycle. Because Verilator evaluates combinational logic in topological order, a genuine cycle has no valid order. Most UNOPTFLAT warnings are real bugs; the rest are false loops through wide vectors that you resolve by splitting the vector or restructuring the logic rather than by silencing the warning.

How do I collect coverage with Verilator?

Compile with --coverage-line for line coverage, --coverage-toggle for toggle coverage, --coverage-user for functional/user coverage points, or --coverage to enable the full set (line, toggle, expression, FSM-state-toggle, and user). The run writes a coverage.dat file. Merge one or more runs and emit an LCOV .info report with verilator_coverage --write-info coverage.info coverage.dat, then render HTML with genhtml. Coverage tells you what was exercised, not whether it was correct — a high number with a weak checker still ships bugs.

Related reading

Run Verilator on your RTL inside ChipVerify AI

Drop a Verilog or SystemVerilog file into the public Tiny Tapeout scanner at /tinytapeout for an instant Verilator + Yosys + Verible report, or request access to the full project workspace with simulation evidence, waveforms, and coverage merge, every finding carrying a file:line reference.

Run Verilator lint and simulation on your RTL

Sign in and ChipVerify AI runs Verilator lint and simulation on your design, returning warnings, waveforms, and coverage as pre-signoff evidence with file-and-line findings — structural and dynamic checks, not a foundry signoff.