Icarus, Verilator, AMD's Vivado simulator, Questa–Altera Starter and the slang front end, compared construct by construct — from putting nineteen interview-prep RTL challenges through them and recording every error message.
The starting point was a review of the SystemVerilog coding challenges in three interview-preparation repositories — Interview_FPGA, Interview_DSP and Interview_RTL_LLM_Accelerators. Nineteen .sv files, each a design plus a self-checking testbench.
A full SystemVerilog front end (slang) found the six files that did not compile. It could not find what came next. Once each testbench actually ran — nine in Verilator, the other ten in Vivado xsim 2025.2 — most of them failed:
| Challenge | What simulation revealed |
|---|---|
| GELU approximation | Placeholder coefficients — worst error 818 LSB; 1,102 of 4,095 codes over the limit |
| FIR filter | Never shifted out the Q1.15 coefficient bits, so every output saturated at 32767 — the passband test “passed” because 32767 > 5000 |
| AXI DMA engine | STORE hung: WLAST compared against a burst length that shrank during the burst |
| Async FIFO | Empty/full flags two cycles late — a back-to-back read underflowed |
| RMSNorm | Every output zero: three separate arithmetic and timing bugs in the Newton–Raphson stage |
| exp() LUT | Index taken from the wrong bits — ~100% error |
| Gray counter, FFT butterfly, accumulator tree | Testbenches that sampled on the wrong cycle or ran tests concurrently |
| MAC unit FP16/INT8 | FP32 adder kept the hidden bit in the mantissa, so 2 + 2 − 3 gave 3.5; a clear in one mode wiped the other mode's accumulator |
| Softmax pipeline | exp LUT mapped x = 0 to entry 128; the reciprocal's first guess was ≈1 for any sum below 256; two separate 2× scaling errors |
| KV-cache controller | Command word fixed at 30 bits, so “read K” returned V; every write landed one beat after its data |
| Double-buffer controller | Tile 0 went to the wrong bank; a DMA overwrote a tile not yet computed; one tile hung |
| Systolic array | Did not elaborate (wire dimensions swapped); PE enables ignored the pipeline delay; the reference model computed A·Bᵀ in 8 bits |
| Softmax UVM-lite testbench | Scoreboard would have passed a DUT that dropped outputs — it never checked the count |
A clean compile says the code is legal SystemVerilog. Only a simulator says it does what the comments claim. Which simulator you can use depends on which language features the testbench leans on — hence this deck.
| Tool | Version used | Cost / licence | What it is |
|---|---|---|---|
| Icarus Verilog | 12.0 | Free, GPL | Event-driven interpreter (iverilog → vvp). Strong Verilog-2005, partial SystemVerilog |
| Verilator | 5.020 (Ubuntu 24.04 package) | Free, LGPL/Artistic | Compiles SV to C++. Version 5 added --timing, so delays, fork and events work in testbenches. Newer releases than 5.020 support more |
| AMD Vivado simulator (xsim) | 2025.2 | Free in Vivado ML Standard edition (AMD account) | Full event-driven simulator: classes, constrained random, covergroups, SVA, DPI-C, UVM 1.2 |
| Questa–Altera FPGA Starter | — | Free with Quartus Prime Lite; one-year node-locked licence, renewable | Siemens Questa, capacity-limited; verification features need a paid licence |
| slang | pyslang 11.0 | Free, MIT | Complete SV parser and elaborator with precise diagnostics. Does not simulate |
Commercial simulators (Synopsys VCS, Cadence Xcelium, Siemens Questa) support essentially the whole language, and are the reference the other tools are measured against.
✓ works◐ partly✗ fails? not established
O observed in this exerciseD vendor documentationK widely known, not re-tested here
The Icarus, Verilator and xsim columns are what those versions actually did with the challenge files (xsim 2025.2 ran the ten that the other two could not); cells marked D come from AMD's UG900 (Appendix B) or, for Questa Starter, Altera's product page and support forum.
Tick the features your testbench uses. Each simulator's verdict is the weakest cell across your selection.
Presets: plain RTL + self-checking TB · RTL + SVA assertions · class-based TB with coverage · clear
Built with verilator --binary --timing --assert. Every row is a real message from a real file.
| Construct | Message | Workaround |
|---|---|---|
covergroup | Unsupported: covergroup | None in 5.020 — use xsim, or strip coverage for a functional run |
##1 inside a sequence | Unsupported: ## (in sequence expression) | Rewrite with |=> and $past, which do work |
disable blk from a sibling fork branch | disable isn't underneath a begin with name | Replace the watchdog pattern with a shared done flag, or disable fork |
Fixed array passed to an open-array [] argument | C++ compile: no match for operator= (VlQueue vs VlUnpacked) | Declare the argument with a fixed size |
virtual softmax_if handle for an instance with a parameter override, softmax_if #(.N(16)) | Unsupported: virtual interface never assigned any actual interface | Instantiate the interface with its default parameters, or parameterise the virtual-interface type to match |
Waiting on a clocking block through a virtual interface: @(vif.driver_cb) | Internal Error: MemberSel of non-variable | Wait on the clock edge itself: @(negedge vif.clk) |
typedef logic [7:0] t [N]; in a package | Internal: Unexpected Call | Write the range as [0:N-1] |
Parallel build (-j 8) | Internal Error: attempted to destroy locked Thread Pool | Intermittent; -j 1 is reliable |
What did work: concurrent assertions with |=> and $past, --timing delays and events, fork/join, automatic variables in always_ff, DPI-C with a linked C file, and real maths including $exp.
Compiled with iverilog -g2012. Icarus is the simulator the standalone RTL projects in this GitHub (AI_MMUL_Unit, AXI4_Crossbar, Cache_Controller_MESI, the RISC-V SoC) were verified with — usually under cocotb.
| Construct | Message | Workaround |
|---|---|---|
| Unpacked arrays as task/function ports | sorry: Subroutine ports with unpacked dimensions are not yet supported | Pass a packed vector, or use module-level arrays |
break | sorry: break statements not supported | Loop flag in the loop condition |
Concurrent assertions (assert property) | sorry: concurrent_assertion_item not supported | Compile with -gno-assertions, or use immediate assertions in always blocks |
automatic variable inside a static block | sorry: Overriding the default variable lifetime is not yet supported | Declare at module scope or in an automatic task |
Function with an output argument | Function … port c is not an input port | Make it a task |
always @(*) in place of always_comb “to prevent iverilog infinite re-evaluation loops when blocks read submodule outputs”, and two mesi_handler instances to “avoid combinational loops through a shared mux in iverilog”.always @(*) for the same reason.Ships in every Vivado edition, including the free Vivado ML Standard. It needs no FPGA project: xvlog, xelab and xsim run from the command line.
disable fork; loop jump statementsiffrand/randc, constraints, randomize()cover statements; assertion coveragealways/eventually/until/strong/weak propertiesbind (binding code to scopes)randsequence; tagged unions; pattern matching| Construct | xsim 2025.2 | Workaround |
|---|---|---|
$past() in an assertion's else action | XSIM 43-4463: unable to infer clocking event | Print $sampled(sig) |
Size cast 16'(a - b) in an assign | Not truncated — silently gives −39936 (correct in procedural code; Icarus truncates) | Declare the net at the width you need |
shortreal arithmetic | Held at double precision; only $shortrealtobits rounds (Icarus behaves the same) | Round each step through $shortrealtobits |
$urandom(seed); as a statement | urandom system task is not supported | process::self().srandom(seed) |
illegal_bins … iff (…) | Warning 43-4398: the iff is ignored — the value leaves every other bin and is illegal in any mode | Check the rule with an assertion |
option.cross_auto_bin_max = 0 | Rejected: “String value is expected” | An ignore_bins for the complement |
Everything else the ten needed ran as documented: classes, mailboxes, parameterised virtual interfaces with clocking blocks, named disable across fork branches, ## sequences, break, array arguments, DPI-C, and covergroups with transition, iff and with bins. xsim is also stricter than slang: it rejected a variable driven by an always_ff and another process (three of the original files), an out-of-range constant index, and a module-scope variable used before its declaration — slang passed all three.
Free with Quartus Prime Lite. The licence comes from Altera's Self-Service Licensing Center, is node-locked, and must be renewed every year. The Starter edition is capacity-limited; the paid Altera FPGA Edition raises the limit.
randomize, randcase, randsequence and covergroup need a separate verification licence. That rules out exactly the class-based, coverage-driven testbenches that Icarus and Verilator also struggle with.
It remains a good simulator for plain RTL with self-checking testbenches, and for mixed VHDL/Verilog designs — but for those, Icarus and Verilator already do the job without a licence to renew.
slang parses and elaborates the full IEEE 1800-2017 language and reports errors precisely. Through pip install pyslang it is the quickest way to answer “is this legal SystemVerilog?”.
binsof used outside a cross[*8:$] in a transition bin$abs — not an IEEE system function — elaborated without a warning; Verilator rejected italways_ff and by another process (illegal); a module-scope variable used before its declaration; a constant index one past the end of a wire array — xsim rejected all threeUse it as a pre-commit lint across every file. Then simulate.
These were in the challenge files. They are not simulator limitations — the language forbids them, or leaves the result undefined.
| Mistake | Fix |
|---|---|
Declaration after a statement in a begin…end | Hoist every declaration to the top of the block |
$abs(x) | Not a system function: write (x < 0) ? -x : x in a helper |
struct packed containing unpacked arrays | Use an unpacked struct |
$error("a" "b") — adjacent literals | SV does not concatenate them: use one literal |
$display("=" * 60) | That is Python. Use {60{"="}} |
A variable declared inside a covergroup | Make it a class member |
binsof() in a coverpoint's bins | Only legal in cross bins — use bins b = {v} iff (cond); |
STALLED[*8:$] in a transition bin | Repeat ranges must be bounded: [*8:16] |
drv = new(vif, fifo) where fifo = new() is a sibling declaration | Static initialiser order is undefined — build components in an initial block |
NR_W'(a * b) expecting a wide product | A cast makes its operand self-determined — a * b is only NR_W bits and overflows. Multiply into a wide temporary first |
A variable assigned in an always_ff and also in an initial or a second always_ff | Illegal: an always_ff variable may have no other driver. Merge the logic into one block, or use plain always in testbench code |
DPI open array shortint x[] with a C short * parameter | Open arrays arrive as svOpenArrayHandle. Use a fixed-size SV array to get a plain pointer |
Many of the nineteen challenges needed testbench fixes as well as — or instead of — design fixes. Four patterns kept recurring.
Blocking writes straight after @(posedge clk) race the DUT's flops. Drive on @(negedge clk); sample at @(posedge clk); #1, after the non-blocking updates.
An accumulator tree has log₂N adder levels plus an input register. Waiting log₂N edges reported every result one test late. Wait for valid_out, then check the latency.
Waiting a fixed number of edges and then testing a valid that is high for one cycle only either catches it or silently skips the check. Loop until it appears, with a timeout.
Separate initial blocks all start at time 0. A reset test fired in the middle of a counting test. Sequence tests with flags or a single initial.
The DMA testbench printed PASSED whatever the data, the FIFO scoreboard popped its expected value without comparing it, and several of the xsim ten printed PASS lines no failure could reach. Count failures, report the count, and try deliberate corruptions (mutants) to see each one caught.
All nineteen now simulate and pass: nine in Verilator 5.020, the other ten in Vivado xsim 2025.2, which is the only free simulator here that runs them.
| Repository | Challenge | Status | Why not Verilator / Icarus | Fixed when first run |
|---|---|---|---|---|
| Interview_DSP | FIR filter | passes (Verilator, Icarus), both forms | — | Design: Q1.15 coefficient shift |
| Interview_DSP | FFT butterfly | passes (Verilator) | — | TB sampling |
| Interview_FPGA | Gray-code counter | passes (Verilator, xsim) | — | TB sampling; $past portability |
| Interview_FPGA | Async FIFO | passes (Verilator) | — | Design: flag timing. TB: scoreboard |
| Interview_FPGA | CDC handshake | passes (xsim) | Verilator: ##; Icarus: lifetime override | TB: promised reset test added; assertions now fail the run |
| RTL_LLM_Accelerators | Accumulator tree | passes (Verilator) | — | TB latency |
| RTL_LLM_Accelerators | AXI DMA engine | passes (Verilator) | — | Design: WLAST. TB: verdict |
| RTL_LLM_Accelerators | GELU approximation | passes (Verilator) | — | Design: coefficients |
| RTL_LLM_Accelerators | RMSNorm | passes (Verilator) | — | Design: Newton–Raphson stage |
| RTL_LLM_Accelerators | exp() LUT | passes (Verilator) | — | Design: index bits |
| RTL_LLM_Accelerators | Systolic array | passes (xsim) | Verilator: named disable; Icarus: array ports | Design: wiring, PE enable timing. TB: reference model, verdict |
| RTL_LLM_Accelerators | MAC unit FP16/INT8 | passes (xsim) | Verilator: open-array argument; Icarus: break | Design: FP32 add + RNE, clear, FTZ, saturation. TB: bit-exact checks |
| RTL_LLM_Accelerators | Softmax pipeline | passes (xsim) | Verilator: named disable; Icarus: lifetime override | Design: exp index, reciprocal, scaling, state reuse. TB: per-weight checks |
| RTL_LLM_Accelerators | KV-cache controller | passes (xsim) | Verilator: named disable | Design: command packing, write path, FIFO pop, seq_len. TB: reference memory |
| RTL_LLM_Accelerators | RoPE unit | passes (xsim) | Verilator: named disable; Icarus: array ports | Design: done timing. TB: bit-exact model, real angles |
| RTL_LLM_Accelerators | Tile scheduler | passes (xsim) | Verilator: named disable | Design: double always_ff driver, done pulse. TB: monitor |
| RTL_LLM_Accelerators | Double-buffer controller | passes (xsim) | Verilator: named disable; Icarus: SVA | Design: rewritten bank tracking. TB: bank-content models |
| RTL_LLM_Accelerators | Softmax UVM-lite testbench | passes (xsim, DPI-C) | Verilator: covergroup, virtual interface | TB: output-count check. Portability: RNG seeding |
| RTL_LLM_Accelerators | Coverage plan | passes (xsim) | Verilator: covergroup | Model: bin arrays, K bins, NaN rule, cross. TB: expected coverage |
| Job | Use |
|---|---|
| Is it legal SystemVerilog? | slang — seconds per file, exact diagnostics |
| Synthesisable RTL, directed self-checking testbench | Verilator (fast, compiled) or Icarus (simplest); both with cocotb |
RTL plus |=>/$past assertions | Verilator — avoid ## in 5.020 |
| Classes, mailboxes, constrained random, covergroups, UVM | xsim from free Vivado — the only free option here that covers them |
| Anything, as the final word | A commercial simulator (VCS, Xcelium, Questa) where available |
pip install pyslang
python -c "from pyslang.syntax import SyntaxTree; from pyslang.ast import Compilation
c = Compilation(); c.addSyntaxTree(SyntaxTree.fromFile('tb.sv'))
print([str(d.code) for d in c.getAllDiagnostics() if d.isError()])"apt-get download verilator && dpkg -x verilator_*.deb vl
export VERILATOR_ROOT=$PWD/vl/usr/share/verilator
ln -s $PWD/vl/usr/bin/verilator_bin $VERILATOR_ROOT/verilator_bin # wrapper looks here
vl/usr/bin/verilator --binary --timing --assert -j 1 --top-module tb tb.sv [model.c]
./obj_dir/Vtbsudo apt install iverilog
iverilog -g2012 -s tb -o tb.vvp tb.sv && vvp tb.vvpsource /opt/Xilinx/2025.2/Vivado/settings64.sh # xvlog, xelab, xsim, xsc
xsc softmax_golden.c # builds xsim.dir/work/xsc/dpi.so
xvlog -sv challenge_02_softmax_testbench.sv
xelab softmax_testbench -sv_lib dpi -s snap
xsim snap -R