Verilator

Verilator is an open-source compiler that translates synthesizable Verilog and SystemVerilog hardware description language (HDL) source code into optimized cycle-accurate C++ (or SystemC) models, which are compiled and linked against a user-written C++ testbench and executed as native binaries (veripool.org; Verilator overview docs). Unlike traditional event-driven simulators (Icarus Verilog, VCS, ModelSim, Xcelium) which interpret the design and schedule events on every signal change, Verilator emits straight-line C++ that evaluates the entire design once per clock edge with two-state semantics, trading some of the language’s four-state (0/1/X/Z) generality for one to two orders of magnitude faster simulation. It is the de facto standard for open-source RTL verification, used by every major RISC-V core project (Rocket, BOOM, CVA6, Ibex, VexRiscv), by Intel, Arm, NXP, and Broadcom internally (Wikipedia: Verilator), and by the definitely-not-esp32 project as its primary simulation target before FPGA porting. As of 2026-08-08 the current stable release is 5.050 (released 2026-07-01), with 5.051 as the open development line; the immediately preceding release, 5.048, was cut on 2026-04-26. Verilator is maintained by Wilson Snyder under LGPL 3.0 / Perl Artistic License 2.0 (verilator/verilator Changes, master; Wikipedia: Verilator).

Mental Model

A traditional event-driven simulator is an interpreter: it maintains a queue of pending signal changes, picks the next event, propagates it through whichever logic the signal feeds, schedules new events for any outputs that changed, and repeats. The interpreter’s overhead per event is large (queue operations, dynamic dispatch, type coercion), and most events do not actually affect the output. Verilator inverts the model. It reads the design, statically analyses signal dependencies, and emits one straight-line C++ function that, given the previous cycle’s state, computes the next cycle’s state in a fixed sequence of inlined operations. There is no event queue, no scheduler, no interpretation; the model is a compiled circuit-as-program. The cost is that Verilator only models behaviour between clock edges in the synthesizable subset; intra-cycle race conditions, four-state X-propagation in arbitrary contexts, and most behavioural-test constructs are out of scope.

flowchart LR
  RTL["design.v / design.sv<br/>(Verilog / SystemVerilog)"] --> VLT["verilator<br/>(C++ compiler that<br/>reads HDL)"]
  VLT --> CPP["Vdesign.cpp<br/>Vdesign.h<br/>(generated C++ model)"]
  TB["sim_main.cpp<br/>(user testbench)"] --> CXX
  CPP --> CXX["g++ / clang++"]
  RUNTIME["libverilated.a<br/>(runtime support)"] --> CXX
  CXX --> BIN["sim_main<br/>(native binary)"]
  BIN --> TRACE["trace.vcd / trace.fst<br/>(waveform output)"]
  BIN --> CONSOLE["stdout / stderr<br/>($display, $finish)"]

The Verilator build flow. What it shows: Verilator is a compiler, not a simulator; the simulator is the C++ binary that results from compiling and linking the generated model against a user testbench and the Verilator runtime. The insight to take: every Verilator run is an ahead-of-time compilation followed by a native execution, which is why the simulation itself runs at C++ speed; the trade-off is that any RTL change costs a re-compile (typically seconds to a minute for a small CPU core).

What Verilator Compiles, and What “Compiles” Means

Verilator parses the input HDL (Verilog, IEEE 1364-1995/2001/2005, and SystemVerilog, IEEE 1800-2005 through 2023 [synthesizable subset]; Verilator languages docs). It elaborates the module hierarchy (instantiating sub-modules, resolving parameters, unrolling generate blocks), performs constant propagation and dead-code elimination, infers flip-flops and latches from always blocks, and topologically sorts the combinational logic. The output is a C++ class (Vtop, where top is the name of the user’s top module) whose public methods include eval() (compute one or more cycles’ worth of activity), final() (run any final block), and accessors for every top-level port.

Internally, the generated model partitions logic into:

  • Sequential update functions (_sequent__TOP__N): one per always_ff block (roughly), each updating the flip-flops driven by a particular clock edge.
  • Combinational evaluation functions (_combo__TOP__N): topologically ordered chains that propagate combinational changes from the freshly updated flip-flops out to the design outputs and the inputs of downstream sequential blocks.
  • Settle loops: if the topological sort detects feedback through combinational logic that does not actually loop on a given input, Verilator iterates the combinational evaluator until it stops changing (or errors out if it does not).

The user calls eval() at the points in their testbench where they want the model to “see” the latest inputs and produce the latest outputs. A typical cycle alternates clk = 0; eval(); clk = 1; eval(); to step the design one clock period.

Two-State Semantics: The Performance Lever

Verilog’s native semantics is four-state: every signal can hold 0, 1, X (unknown), or Z (high impedance). Four-state simulation is essential for catching uninitialized-register bugs (an X propagating through your design points at the culprit) and for modelling tri-state buses. Verilator runs the model in two-state by default: every signal is just 0 or 1. The performance gain is large because two-state operations are native C &, |, ^, and arithmetic on uint32_t/uint64_t/VlWide<>, while four-state operations require per-bit tracking of both value and X-ness.

The trade-offs (Verilator languages docs):

  • Uninitialized registers default to 0, not X. Verilator can optionally randomize initial state (--x-initial unique) to catch initialization bugs that two-state would mask.
  • Tristate (z) is supported only in limited contexts; Verilator converts inout buses to dual-direction structures internally.
  • === and !== (four-state identity comparison) reduce to == and != when neither operand is constant, which can give different results than a true four-state simulator on designs that rely on X-comparison logic.

For synthesizable RTL, two-state is essentially never wrong: anything that depends on X-propagation will not synthesize anyway, so a two-state simulator that matches the gate-level behaviour is the more honest model. Verilator’s manual says the speedup over four-state simulators is “approximately 100 times faster than interpreted Verilog simulators such as Icarus Verilog” on a single thread (veripool.org), with another 2 to 10 times available from multi-threaded partitioning.

The Testbench Harness Pattern

A Verilator test is a C++ program with a main() that drives the model. The canonical pattern, condensed from Verilator’s connecting guide:

#include <verilated.h>
#include <verilated_vcd_c.h>     // VCD trace; use verilated_fst_c.h for FST
#include "Vcpu.h"                // generated header for top module `cpu`
 
int main(int argc, char** argv) {
    // Verilated runtime context; tracks time, args, and globals
    Verilated::commandArgs(argc, argv);
    auto* ctx = new VerilatedContext;
    ctx->traceEverOn(true);
 
    // Instantiate the model
    auto* dut = new Vcpu{ctx};
 
    // Set up waveform tracing
    auto* tfp = new VerilatedVcdC;
    dut->trace(tfp, /*levels=*/99);
    tfp->open("trace.vcd");
 
    // Reset sequence: hold reset low for a few cycles
    dut->rst_n = 0;
    dut->clk = 0;
    for (int i = 0; i < 4; i++) {
        dut->clk = !dut->clk;
        dut->eval();
        tfp->dump(ctx->time());
        ctx->timeInc(5);  // 5 ns per half-cycle = 100 MHz
    }
    dut->rst_n = 1;
 
    // Main simulation loop: 1000 cycles
    for (int cycle = 0; cycle < 1000 && !ctx->gotFinish(); cycle++) {
        // Two evals per cycle: clock low edge then high edge
        dut->clk = 0; dut->eval(); tfp->dump(ctx->time()); ctx->timeInc(5);
        dut->clk = 1; dut->eval(); tfp->dump(ctx->time()); ctx->timeInc(5);
 
        // Optional: poke external state, check internal signals, log progress
        if (dut->cpu_committed_pc != 0) {
            printf("[%5d] PC=0x%08x\n", cycle, dut->cpu_committed_pc);
        }
    }
 
    dut->final();
    tfp->close();
    delete dut;
    delete ctx;
    return 0;
}

The pattern in detail. VerilatedContext holds the global time counter and command-line argument state; multi-design simulations can have one context per design. Vcpu is the generated class; its public fields are the top module’s ports (clk, rst_n, plus whatever the design exposes). eval() advances the design by reacting to whatever inputs were just changed; the two-eval-per-cycle pattern (one for each clock edge) is the standard idiom for purely synchronous designs. tfp->dump(time) emits a snapshot to the VCD file; opening the resulting trace.vcd in GTKWave or Surfer shows every signal as a waveform.

A typical kernel-bringup test for the definitely-not-esp32 core would extend this skeleton with: a memory model (a uint8_t[] array that responds to bus reads/writes by sniffing the model’s bus signals), a UART receiver that buffers writes to the UART data register and echoes them to stdout, and an exit condition triggered by a magic write to a “test-finished” register. Many open-source RISC-V tests use exactly this pattern; the riscv-tests repository is structured to make it easy.

Trace Output: VCD and FST

Two waveform formats are supported. VCD (Value Change Dump, IEEE 1364) is text-based: ASCII-encoded signal IDs and timestamped value transitions. It is universally readable but bulky (gigabytes for long simulations) and slow to write. FST (Fast Signal Trace, originated in GTKWave) is a binary format that is much smaller but slower to write — the opposite of the widely repeated claim. Measured on this machine 2026-09-04 (Verilator 5.046, 1M cycles, three 32-bit signals toggling every edge): VCD 0.06 s / 64.9 MB; FST 0.12 s / 7.5 MB — FST is 2× slower and 8.6× smaller, not 50–100× smaller and faster. A parallel measurement at higher toggle activity found FST up to 8× slower, with the size advantage in the 13.6–21× range; at the low toggle rates typical of a real core the two write speeds come within ~15% of each other. So the tradeoff is disk for time: reach for FST when traces are long enough that size hurts, and stay on VCD when the edit-run-look loop is what you are optimizing. See Waveform Debugging for the full measurements and the methodology caveat (the timed loop is page-cache-bound). The user picks at testbench-build time (#include <verilated_vcd_c.h> versus <verilated_fst_c.h>); the API surface is identical.

Both formats are read by GTKWave (and increasingly by Surfer, a newer open-source waveform viewer with a more modern UI). The waveform is the primary RTL debugging tool: stepping through cycles, expanding hierarchical signals, marking transitions, computing signal-to-signal time differences. For a CPU bringup, having trace.fst open in parallel with the source is the difference between three-minute and three-hour debug sessions.

Threading and Performance

Verilator can optionally partition the design into multiple threads (--threads N), with the runtime scheduler dispatching independent partitions across CPU cores. The official docs claim “potential 2-10x additional speedup through multithreading” (veripool.org) on designs large enough to expose parallelism. For a small in-order CPU, the speedup is usually modest (the design is mostly serial across the pipeline); for a multi-core SoC simulation with independent agents, threading helps significantly. Multi-threaded hierarchical simulation was added in Verilator 5.036 (Verilator changes log).

For the definitely-not-esp32 project’s scale (a single RV32IMC core plus a UART, a CLINT, and a few KiB of RAM), single-threaded Verilator typically achieves 5 to 50 MHz of simulated clock on a modern desktop CPU. That is comparable to the actual FPGA target speed (~50 MHz on the Tang Nano 20K), which is the property that makes Verilator-based simulation a practical primary development environment.

Version Status

Verilator follows a rolling release line with a simple, load-bearing numbering convention: even minor numbers are releases, odd minor numbers are the development line between them. So 5.048 and 5.050 are shipped releases; 5.049 and 5.051 are the in-progress trees that become 5.050 and 5.052. The Changes file in the repository is the authoritative record — each released version gets a heading of the form Verilator 5.NNN YYYY-MM-DD, and the development tree’s heading reads Verilator 5.NNN devel with no date until it is cut (verilator/verilator Changes, master branch).

As of 2026-08-08, reading Changes on master directly:

VersionRelease dateNotes
5.051— (devel)current development line, undated
5.0502026-07-01current stable release
5.0482026-04-26adds --coverage-fsm
5.0462026-02-28
5.0442026-01-01
5.0422025-11-02
5.0402025-08-30

Release cadence of the Verilator 5.04x/5.05x line, read from the Changes file on master. The insight: releases land roughly every two months, so any pinned version in a build script is about eight weeks from being one behind — pin deliberately, and expect a new even-numbered release each quarter at minimum.

Major version 5 launched as 5.002 on 2022-10-29 (Changes line 2107) and introduced an IEEE-1800-compliant scheduler (proper #0 delay semantics, fork/join, more accurate event ordering); the 4.x series was the long-lived predecessor with the older, faster but less compliant scheduler (Wikipedia: Verilator). Notable features added in 5.x:

  • 5.034: expression coverage (Verilator changes log, entry “Add expression coverage (#4677) (#5719)”).
  • 5.036: multi-thread hierarchical simulation, user-defined primitives (UDPs).
  • 5.048: experimental FSM state and arc coverage — Add --coverage-fsm for experimental FSM state and arc coverage (#7412).
  • 5.050: --coverage-fsm improvements (#7490, #7529, #7561, #7573, #7619); it remains experimental.

Resolved 2026-08-08

Verilator 5.048 was released 2026-04-26. Verified two independent ways, both primary:

  1. The Changes file on master carries the literal heading Verilator 5.048 2026-04-26 (raw file).
  2. The annotated Git tag v5.048 in verilator/verilator has tagger date 2026-04-26T05:57:12Z, pointing at commit d0aa828 (GitHub tag object API).

A methodological note worth keeping: Verilator publishes no GitHub Releases objectsGET /repos/verilator/verilator/releases returns an empty array. Only annotated tags exist. Any tool that looks for “the latest release” via the Releases API will find nothing; use /tags plus the tag object’s tagger date, or read Changes.

Version facts decay

The version numbers above are a point-in-time snapshot taken 2026-08-08. Given the observed ~2-month cadence, 5.052 is likely out by late 2026. Re-read the Changes file rather than trusting this table for anything version-sensitive.

Supported and Unsupported Language Features

Verilator implements the synthesis subset of Verilog and SystemVerilog completely, plus a substantial fraction of the verification subset. The official languages page (Verilator languages) is the canonical reference; the headline gaps:

Fully supported. Synthesizable Verilog 2001 (signed arithmetic, generate blocks, multi-dim arrays, parameter overrides), most SystemVerilog packed/unpacked types, logic, enum, struct, union, typedef, package, interface (named, with limitations on virtual interfaces), always_comb/always_ff/always_latch, unique/priority case, immediate assertions (assert, assume, cover with simple expressions), let, chandle, DPI (Direct Programming Interface to call C from SystemVerilog and vice versa).

Partially supported. Classes (limited, in active development), constrained randomization (progressive coverage, much added in 5.x), interfaces (virtual and unnamed limited), bind statements (modules only, not arbitrary hierarchical paths).

Not supported. SEREs and full concurrent assertion property language (only simple one-cycle expressions), encrypted RTL (IEEE P1735), MOS and tri-state gate primitives (bufif, nmos, etc.), arbitrary force/release on procedural continuous assignments. The full unsupported list is documented but, critically, almost everything one would want to synthesize is supported; the gaps are almost entirely in verification-only constructs.

Failure Modes and Common Pitfalls

Uninitialized state masquerading as zero. Two-state semantics zero-fills every flip-flop on power-up, so a design that depends on reset to initialize state may appear to work in Verilator but fail on real silicon where the initial state is unknown. Use --x-initial unique and --x-assign unique during testing to randomize initial values; bugs that survive both runs are robust to initialization order.

Missing eval() after input change. Forgetting to call eval() after changing a non-clock input leaves the model with stale outputs. The standard pattern is set inputs → eval → change clock → eval. Verilator’s docs note: “it is best to set any non-clock inputs up with a separate eval() call before changing clocks” (Verilator connecting docs).

Trace overhead. Dumping every signal every cycle to a VCD can dominate simulation time and produce multi-GB files for long runs. Switch to FST; selectively trace only the signals of interest; gate tracing on a region of cycles around suspected bugs.

Compile time on large designs. A million-LUT design can take Verilator several minutes to compile and g++ even longer. Use --threads for runtime parallelism and partition large designs into hierarchical Verilator runs (each module compiled separately, linked at the top).

Race conditions in mixed-clock designs. Verilator’s scheduler is event-driven across clock domains but cycle-accurate within each domain. Designs with explicit asynchronous edges between domains should add synchronizers (two-flip-flop chains) at the boundary; without them, Verilator may converge to behaviour that an event-driven four-state simulator would not.

Alternatives and When to Choose Them

  • Icarus Verilog. Open-source, four-state, event-driven. Significantly slower than Verilator on the same design but supports more behavioural and verification constructs. Good for small testbenches with heavy behavioural code; bad as the main RTL development simulator.
  • Synopsys VCS, Cadence Xcelium, Siemens (Mentor) Questa. Commercial four-state event-driven simulators. Full SystemVerilog/UVM support, including all the verification constructs Verilator omits. License costs are five to six figures per seat per year. Used wherever full verification methodology compliance is required.
  • Yosys + CXXRTL. Open-source: Yosys synthesizes to a netlist, CXXRTL compiles the netlist to C++. Similar two-state, compiled-simulator philosophy to Verilator but with the synthesis happening up front; useful when the user wants synthesis-then-simulate semantics (post-synthesis sim).
  • SystemC reference simulators. For purely transaction-level or mixed C++/RTL co-simulation, SystemC with the official OSCI kernel is the reference. Verilator supports a --sc output mode that produces a SystemC module wrapping the design, enabling drop-in integration.

For an open-source RV32IMC core development project, Verilator is the dominant correct choice: free, fast enough to run thousands of riscv-tests per minute, mature enough that essentially every popular RISC-V core uses it as its primary simulation flow.

See Also