The short answer
Verilator 5.052 covers far more than “synthesizable RTL”: delays, real arithmetic, open-drain pullup/nmos, tri-state and SVA work. It lacks gate-level precision; its worst failures are silent.
How this was established: the same source was given to Verilator 5.052 and VCS 2023.12 for A/B comparison; the support matrix comes from 39 standalone cases (one file per construct), each linted, built and run — not copied out of documentation.
Environment and method
| item | value |
|---|---|
| simulator | Verilator 5.052 (2026-09-05), gcc 12.5.0 |
| platform | CentOS 7.9 / glibc 2.17, no root |
| reference | VCS 2023.12-SP1 |
| method | one file per construct → verilator --lint-only --timing -Wno-fatal → the suspicious ones again with --binary --timing build + run |
| tally | 39 cases: lint 38 PASS / 1 FAIL; the one that FAILs (tran) also fails to build |
What a real design needed changed (brief)
Subject: a 1080p timing controller on a 7-series FPGA, 29 modules, 100 MHz input, 490 MHz LVDS serial output, I2C slave; the testbench runs 43 ms of simulation time.
Four changes are mandatory:
| # | change | why |
|---|---|---|
| 1 | $fsdbDumpfile/$fsdbDumpvars/$fsdbSuppress → $dumpfile/$dumpvars |
no Verdi PLI; $fsdb* is an unknown system task |
| 2 | unisims + secureip → behavioural fast models | secureip is encrypted source Verilator cannot read, and the unisim SERDES depend on it |
| 3 | instantiate glbl glbl(); inside the testbench |
some primitives use the XMR glbl.GSR, and the instance must be named exactly glbl |
| 4 | waveform format → VCD or FST | see “Waveform pitfalls” |
Result: the two BMP frames the TB dumps are byte-identical to the VCS run (same md5), and the end time agrees to 1 ps; build 4.6 s (-j32) against 23 s for VCS, simulation 52 s against 51 s.
Syntax support matrix (measured)
Fully supported, and identical to VCS
| category | constructs |
|---|---|
| sequential / combinational blocks | always_ff / always_comb / always_latch / always @(*) |
| composite types | packed struct, unpacked struct, packed union, typedef enum + '( ) casts |
| packages and interfaces | package + import, interface + modport |
| classes | class, new, virtual function (an implicit static lifetime draws IMPLICITSTATIC) |
| containers | dynamic array int a[], queue int q[$], associative array int tbl[string] |
| strings | string, $sformatf |
| assignment patterns | '{default:1'b1} packed pattern, '{0,1,2,3} array pattern |
| generate | generate for + genvar |
| assertions | immediate assert(...) else $error, concurrent assert property (really executed, see below) |
| coverage | covergroup/coverpoint + --coverage (verilator_coverage reports covergroup bin hit rates) |
| process control | fork/join, join_any, join_none, wait fork, disable fork, disable {named fork} |
| procedural continuous assignment | force / release |
| files and randomness | $readmemh/$readmemb, $fopen/$fwrite/$fclose, $urandom, randcase |
| delays | procedural #(0.250ns), continuous assignment assign #(real variable) |
| tri-state and gate level | pullup/pulldown, nmos/pmos, and/nand/or/nor/xor/buf/not, tri inout |
| other | casez/casex, unique case, signed arithmetic, array ports, let, inside, XMR (u_sub.sig read/write), DPI-C import |
Two conclusions that go against older folklore, both settled by measurement:
disable {named fork}is supported.fork: W ... join_any; disable W;leaves only the winning branch printing;disable forkworks as well.- SVA is not merely parsed. On a violation it really prints
%Error: Assertion failed ... %Error: Verilog $stopand aborts the simulation — treat that as a failure in CI.
Not supported (hard error)
| construct | error |
|---|---|
tran / tranif0 / tranif1 bidirectional switches |
%Error-UNSUPPORTED: Verilog 1995 gate primitive: tranif1, the build fails outright |
| encrypted vendor models (secureip and friends) | the source cannot be read, so it cannot be parsed |
$fsdb* and other Verdi-only system tasks |
no matching PLI, unknown system task |
gate-level / SDF back-annotation, $setup/$hold timing checks |
no equivalent implementation |
Accepted but silently wrong (the class to watch)
| construct | Verilator | VCS | note |
|---|---|---|---|
drive strength: pullup against assign (weak0,weak1) s=0 |
0 |
1 |
no warning; any two drivers are resolved by value, strength plays no part |
specify ... endspecify path delay |
y toggles at 2000 ps | y toggles at 3000 ps | only a %Warning-SPECIFYIGN, the delay is dropped |
$dumpoff / $dumpon |
parsed but has no effect | works | the dump keeps writing to the end of the run |
--no-timing or +notimingchecks |
every #delay becomes %Warning-STMTDLY: Ignoring delay, all edges collapse to t=0 |
— | with -Wno-fatal this silently changes behaviour |
On --timing: 5.x has it on by default, so not passing it gives the same result; what you must guard against is turning it off (or +notimingchecks, which is “accepted and ignored”). Write --timing explicitly in the script anyway, so the intent stays on the command line.
Waveform pitfalls
$dumpvarsneeds--trace(VCD) or--trace-fst(FST) at build time. Without that compile-time switch$dump*is a silent no-op: the run finishes with no file and no message.$dumpoff/$dumpondo nothing (see the table above), so “dump only one window” is impossible; the only knob is the level argument of$dumpvars(level, scope).- Three orders of magnitude in size: for the same 43 ms design with two 490 MHz buses, dumping only the TB and the DUT top level — VCD 12.4 GB, FST 123 MB. There is no equivalent of Verdi’s suppress, so the only levers are scope level or FST.
- Dumping is itself the main cost: same configuration, 52 s with no waveform, 152 s with FST (about 3x), about 98 s with VCD; on the VCS side enabling FSDB adds 0 s. If you want speed, do not dump by default.
- Verdi cannot read FST, GTKWave reads it natively; building FST support also needs the lz4 headers (
fst2vcdconverts to VCD for other tools). $timereturns an integer number of time units: in a 1 ns/1 ps design a%0tprint shows42922750.000 ns, which looks 355 ps worse than VCS, while the last timestamp in the VCD is42922749645 ps— the two agree. Judge the time axis from the waveform timestamps, not from the$timeprint.
Rules when writing TB / RTL (checklist)
- Do not start a comment with
// Verilator. Verilator treats// verilator ...as a pragma and fails with%Error-BADVLTPRAGMA: Unknown verilator comment. Use a prefix such as// NOTE:or// VLNOTE:. - Check whether a behavioural model of the primitive exists. An IP available only as encrypted secureip is a dead end; where a fast behavioural model exists, functional simulation is fully adequate.
- An XMR into a global (such as
glbl.GSR) needs the right instance name: writeglbl glbl();in the testbench (instance named like the module); Verilator resolves it by searching upward. -Wno-fatalmust come with warning triage. Grep theUNSUPPORTED/SPECIFYIGN/STMTDLY/WIDTHEXPAND/MULTIDRIVENcodes out and judge each one; using-Wno-fatalas “zero warnings” lets the silent distortions through.- A failing assertion
$stops the simulation, so a regression script must treat it as a failure in both exit code and log matching, not as “the run finished”. - Do not rely on drive strength or gate delays. The common open-drain plus pull-up topology (“strong0 overrides pull1”) comes out right, but “pull1 overrides weak0” is wrong, and there is no warning.
- Run
--lint-only --timingbefore building: clearing the syntax problems in the lint stage is much faster than trying again with a 4 s compile each time. - Give the dump only the levels you need:
$dumpvars(1, tb)plus$dumpvars(1, dut)is usually enough to locate a problem; dumping the whole hierarchy costs an unreasonable amount of size and time.
When not to use Verilator
| scenario | reason |
|---|---|
| gate-level / post-layout simulation, SDF back-annotation | no specify/gate delays, no timing checks |
| models that depend on drive strength | strength does not take part in resolution, silently wrong |
| IP available only as an encrypted model | the source cannot be read |
a flow tied to Verdi ($fsdb*, KDB) |
no PLI; waveforms only as FST/VCD |
| expecting a large simulation speed-up | here 52 s against 51 s: 5x faster to build, no faster to run (clock edges dominate, not event scheduling) |
How to reproduce
One case per construct, one file each, lint first and build second:
# a single construct
verilator --lint-only --timing -Wno-fatal --top-module m case.sv
# the suspicious ones (assertions / coverage / process control) really run
verilator --binary --timing -Wno-fatal --coverage --top-module m -o V case.sv && ./obj_dir/V
# coverage summary (reports covergroup hit rates)
verilator_coverage coverage.dat
To check “does it agree with the commercial simulator”, give the same file to both simulators and compare the printed times and values (every “identical / different” in this note comes from doing exactly that).
Limitations
The data here comes from a single Verilator version (5.052), 39 syntax cases and one real design of 29 modules, so it does not describe how earlier or later versions behave; program, clocking blocks and DPI-C were only proven to parse/build, their runtime semantics were not checked case by case. Re-run the lint matrix after upgrading Verilator.