Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Verminator

Automated fault-injection campaigns for Verilator RTL simulations.

Verminator orchestrates fault-injection (FI) campaigns against Verilator simulations. You describe what signals to fault and how in a single TOML file; Verminator verilates your design, generates the instrumentation, builds one simulation binary, and runs every fault scenario in parallel — collecting traces and performance data for each.

Verminator orchestrates the campaign. The actual fault injection at runtime is performed by a companion Verilator DPI extension, which Verminator generates the configuration and C++ hooks for.


Features

  • Single-file config — one verminator.toml describes the whole campaign.
  • Single-signal targets — fault one signal at a time across fault types and time windows.
  • Multi-signal combinations — inject several signals together in one run, each with its own fault type and (optionally) its own timing.
  • Fault types — bit-flip, stuck-at-0, stuck-at-1, with optional time windows.
  • Parallel execution — every fault run is a separate process; scales across all your cores.
  • Three testbench modes — auto-generated driver, your own C++ driver, or a SystemVerilog-driven testbench.
  • Waveforms + perf — FST/VCD traces and a per-run performance summary.

How it works

1. Verilate golden   ->  discover the design's signal tree (no simulation)
2. Generate          ->  fault_config.vlt, faultModel.cpp, sim_main.cpp
3. Build hooked      ->  one instrumented V<top> binary
4. Run campaign      ->  golden run, then all fault runs in parallel
5. Summarise         ->  perf_summary.csv

Results land in a structured directory:

<results_dir>/
├── 01_golden/build/          # netlist JSON
├── 02_generate/              # generated .vlt + .cpp
├── 03_hooked/build/V<top>    # the instrumented binary
└── 04_simulation/
    ├── golden/               # golden trace + perf.json
    ├── run_0000/  run_0001/  # one dir per fault run
    ├── perf_summary.csv
    └── campaign.json         # full fault matrix metadata

Requirements

  • Python 3.11+ (uses tomllib)
  • Verilator built with the Verminator DPI instrumentation extension — get it here: Verilator draft PR
  • Python packages: pydantic, typer

Verminator finds Verilator in this order:

  1. input.hw.verilator_path in your config
  2. $VERILATOR_ROOT/bin/verilator
  3. verilator on your $PATH

Installation

git clone https://github.com/hm-aemy/verminator
cd verminator
python -m venv .venv && source .venv/bin/activate
pip install -e .

Quick start

  1. Discover what you can fault:

    verminator run verminator.toml --list-signals
  2. Preview the campaign without running anything:

    verminator run verminator.toml --dry-run
  3. Run the full campaign (parallel):

    verminator run verminator.toml --jobs 8

Configuration

A minimal verminator.toml:

[input.hw]
top_module    = "tb_counter"
design_files  = ["rtl/counter.sv", "tb/tb_counter.sv"]

[simulation]
tb_mode        = "cpp_driven"     # auto | cpp_driven | sv_driven
driver_file    = "tb/driver.cpp"  # required for cpp_driven
timeout_cycles = 1000
seed           = 42

[simulation.waveform]
enabled = true
format  = "fst"                   # fst | vcd

[simulation.fault_injection]
depth = "custom"                  # only "custom" is supported today

# --- Single-signal target ---
[[simulation.fault_injection.targets]]
name        = "counter_register"
path        = "tb_counter.uut.cut.count_reg"
sv_type     = "logic"
width       = 64
fault_types = ["bit_flip", "stuck_at_0"]
time_windows = [{ begin = 100, end = 250 }]   # optional; omit = whole sim

# --- Multi-signal combination ---
[[simulation.fault_injection.combinations]]
name = "reg_and_count"
# Optional: one time window for the whole combination (applies to every signal
# that does not define its own). Omit for whole-simulation injection.
time_windows = [{ begin = 100, end = 250 }]
signals = [
    # count_reg is also a [[targets]] above, so width/sv_type are inherited.
    { path = "tb_counter.uut.cut.count_reg", fault_type = "bit_flip" },
    # count is NOT a target, so it must declare width/sv_type itself.
    { path = "tb_counter.uut1.cut.count",    fault_type = "stuck_at_0", width = 8, sv_type = "logic" },
]

[output]
results_dir = "results/"

How runs are generated

  • Each target produces one run per (fault_type x time_window), each faulting that single signal.
  • Each combination produces one run that faults all its signals in the same run, each with its own fault type.

Combination signal details

width / sv_type are required, but can be inherited. Every combination signal needs a width and SV type. You can either:

  • declare them inline on the signal (always works), or
  • omit them — Verminator then inherits them from a [[targets]] entry with the same path.

If a signal is neither given inline values nor has a matching target, the build fails with a clear error. (In the example above, count_reg inherits from its target; count has no target, so it declares width/sv_type itself.)

Timing is flexible. Faults in a combination do not have to happen at the same instant. The time window is resolved per signal:

  1. a signal's own time_windows wins, else
  2. the combination-level time_windows applies, else
  3. the fault runs for the whole simulation.

So you can fault the whole combination within one shared window (set it once at the combination level), stagger each signal independently (give each its own window), or mix both (combination default + per-signal overrides):

# Staggered: each signal faults in its own window
[[simulation.fault_injection.combinations]]
name = "staggered"
signals = [
    { path = "...count_reg", fault_type = "bit_flip",   width = 64, sv_type = "logic", time_windows = [{ begin = 100, end = 250 }] },
    { path = "...count",     fault_type = "stuck_at_0", width = 8,  sv_type = "logic", time_windows = [{ begin = 300, end = 500 }] },
]

Bit-level targeting (optional)

By default a fault is applied to the whole signal. To restrict a fault to a single bit or a contiguous bit range, add bit or bit_range to a target or a combination signal (they are mutually exclusive):

# Fault only bit 5 of the register
[[simulation.fault_injection.targets]]
path        = "tb_counter.uut.cut.count_reg"
sv_type     = "logic"
width       = 64
fault_types = ["stuck_at_1"]
bit         = 5                 # single bit  -> emits -bit-pos 5

# Fault only bits [15:8]
[[simulation.fault_injection.targets]]
path        = "tb_counter.uut1.cut.count_reg"
sv_type     = "logic"
width       = 64
fault_types = ["bit_flip"]
bit_range   = [15, 8]           # [hi, lo]     -> emits -bit-range "15:8"

Notes:

  • Omit both bit and bit_range to fault the entire signal (the default, and the common case).
  • bit / bit_range must fit inside width (0 <= lo <= hi < width).
  • A given signal path must use one consistent selection everywhere it appears (across targets and combinations); a conflicting selection for the same path is rejected at build time.
  • The selection is threaded straight into the generated fault_config.vlt (-bit-pos / -bit-range) and the matching faultModel.cpp callback. See How the DPI callback signature is derived.

Testbench modes

Mode You provide Verminator generates
auto nothing sim_main.cpp and a driver.cpp template to fill in
cpp_driven a driver.cpp with drive_simulation() sim_main.cpp
sv_driven a self-driving SV testbench sim_main.cpp (event loop)

For auto/cpp_driven, your drive_simulation() has this shape (with waveforms):

void drive_simulation(V<top>* dut, VerilatedContext* ctx,
                      <TraceClass>* tracep, uint64_t max_time);

(without waveforms, the tracep argument is dropped).


CLI reference

verminator run <config> [options]
verminator validate <config>
verminator example <name>

run options

Flag Description
--output-dir PATH Override the results directory.
--dry-run Validate and print the plan; execute nothing.
--list-signals Verilate, list injectable signals, exit.
--jobs, -j N Number of parallel fault runs (default: CPU count).
--debug Verbose output.
--random-signals N (planned — not yet implemented)

Outputs

  • 04_simulation/run_NNNN/trace.fst — waveform per fault run.
  • 04_simulation/run_NNNN/perf.json — timing per run.
  • 04_simulation/perf_summary.csv — golden vs. average-FI plus per-run detail.
  • 04_simulation/campaign.json — the full fault matrix (effects + runs).

How the DPI callback signature is derived

Verminator generates two artifacts that must agree on a single C ABI: the fault_config.vlt hook directives (consumed by the Verilator DPI-hook extension) and the faultModel.cpp callback definitions. The extension builds the callback's parameter list from the hook directive, so Verminator emits both from the same rules:

Config .vlt directive Callback signature
no bit / bit_range (none) fault_<w>(int id, svBit trigger, <value>)
bit = N -bit-pos N fault_<w>_bitpos(int id, svBit trigger, int bitPos, <value>)
bit_range = [hi, lo] -bit-range "hi:lo" fault_<w>_bitrange(int id, svBit trigger, int bitStartPos, int bitEndPos, <value>)

Two consequences worth knowing:

  • The bit-selection shape is part of the callback name (_bitpos / _bitrange suffix). Signals that share a width but differ in shape would otherwise call the same C symbol with incompatible argument lists — encoding the shape in the name gives each signature its own function.
  • <value> is a pointer for packed vectors, not a by-value scalar: const svLogicVecVal* for 4-state types (logic/reg/wire, accessed via .aval) and const svBitVecVal* for 2-state bit. The generated body dereferences it into a uint64_t before applying the fault. (Emitting a spurious -bit-range for a whole-signal target used to shift every argument by one, so the callback read a bit index where it expected the value.)

Current status

Verminator is under active development. What works today:

  • Golden verilation and signal discovery
  • Custom-depth campaigns (single targets + multi-signal combinations)
  • Whole-signal, single-bit (bit) and bit-range (bit_range) targeting
  • Bit-flip / stuck-at-0 / stuck-at-1 with time windows
  • Parallel per-run execution
  • Waveforms and performance summary

Planned / not yet implemented:

  • Depth presets (minimal / medium / extensive) — only custom works now
  • Analysis layer: golden-vs-injected diff and fault classification
  • --random-signals selection
  • random fault type
  • In-pipeline firmware build (CMake / custom commands)

Signal widths above 64 bits are not currently supported.

For a complete, function-by-function breakdown of the implementation, see ARCHITECTURE.md.


License

TBD.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages