Constrained-random stimulus is only useful if it is reproducible. The random behavior of a simulation derives from one root seed, so a logged seed turns a random failure into a replayable bug report.
The precise rule is narrower than folklore: same code plus same seed plus same tool, version, and options gives the same RNG streams. Identical timing additionally needs a race-free testbench, since races, wall-clock seeding, and multithread ordering can all shift a run.
Tool | Seed flag | Scope note |
|---|---|---|
VCS |
| Seeds the solver and the testbench RNG. |
Xcelium |
| Seeds the solver and the testbench RNG. |
Questa |
| Seeds the solver and the testbench RNG. |
Verilator 5.048 |
| Runtime seed for its random generator; |
Icarus 13 | No seed flag | Ignores |
The testbench seed knob keeps working everywhere. The tool flags differ per simulator and Icarus has none, so every runnable block here uses +SEED.
The listing below extends the adder check with a +SEED knob. It parses the seed with $value$plusargs, prints it in the header, and drives 32 vectors from the seeded stream.
It runs under Icarus 13 and Verilator.
module adder8s (
input logic [7:0] a, b,
output logic [7:0] sum
);
timeunit 1ns; timeprecision 1ps;
assign sum = a + b;
endmodule
The default seed 1 keeps smoke runs deterministic. Any other +SEED=N explores a different stream, and the header makes the log self-describing.
Triage is mechanical: read the seed from the failing log header, rerun the same test with the same seed and debug knobs up, and the failure reproduces at the same point.
# Nightly run that failed (dispatcher picked the seed)
simv +SEED=482931 > run.log
# log header: SEED : 482931 ... FAIL: adder8 produced a wrong sum
# Replay: same seed, more visibility
simv +SEED=482931 +VERBOSITY=2 +DUMP_WAVES=1 > replay.log
# Same stream, same result, now with waves and a full logOne seed explores one path through the stimulus space. A seed sweep runs the same test across many seeds and merges the results, which beats hand-writing hundreds of directed tests for breadth.
Random stability is hierarchical and fragile to edits. Replay on the exact failing commit, and debug with extra display output rather than new randomization calls or reordered construction, which re-seed everything downstream.
One compiled image serves smoke, stress, and replay runs through runtime knobs. The testbench parses each knob once at time zero into a config object, and components read only that object.
Knob | Typical values | Effect |
|---|---|---|
| 1, 7, 42, 991 | Selects the random stream; logged in the header. |
| 50, 200, 5000 | Scales the same test from smoke to soak. |
| 0, 10 | Percent of stimulus carrying injected protocol problems; 0 for clean runs. |
| 0 to 3 | Log level; quiet in regression, loud in replay. |
| Flag | Waveform capture, off by default; regressions at scale cannot afford waves. |
Keep defaults sane so a bare run with no knobs passes. Gate error injection behind error_rate > 0, since a zero distribution weight is legal but some solvers warn about it.
Two parsing cautions: $test$plusargs does prefix matching, so +DUMP also matches +DUMP_WAVES; and a misspelled knob is silently ignored, so print every effective value at time zero on a [CFG] line.
A different seed, a different code revision, or a random-stability shift from an edit made during debug. The log header plus the exact commit disambiguate them.
Logs outlive dispatch records. A failing log must contain its own replay recipe years later.
+ERR_RATE=10 is passed but the run injects nothing. What happened?The testbench expects +ERROR_RATE, so the misspelled knob was silently ignored and the default 0 applied. The [CFG] printout at time zero would have shown it.
$test$plusargs("DUMP") also fire when only +DUMP_WAVES was passed?Presence matching is by prefix. Test for the full flag name to avoid accidental matches.