rustdv 0.3.0

A hardware verification framework in Rust: cocotb-style simulator coroutines plus a UVM-analog testbench library.
Documentation

rustdv

crates.io

rustdv combines simulator-driven Rust coroutines with a UVM-inspired verification library. This guide takes you from a working simulator to your own testbench, then covers typed signal access and API migration. It assumes no previous use of rustdv; Rust for RTL Verification teaches the Rust along the way.

The setup below uses a repository clone, which includes the TinyALU design, worked testbench, runnable book examples, simulator launchers, and verification skills. To depend on the published framework instead, run cargo add rustdv and cargo add --dev rustdv-vpi-stubs in your testbench crate. The library configuration and simulator-launch requirements still apply.

Repository paths and launch commands below are relative to the clone’s root, except dependency paths, which are relative to the testbench manifest.

Install the tools

  1. Rust — one command, from rustup.rs:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustc --version
    
    The repository pins its compiler in rust-toolchain.toml, so inside a clone rustup fetches the right one on its own.
  2. Icarus Verilog — the four-state reference simulator:
    sudo apt install iverilog     # Debian/Ubuntu
    brew install icarus-verilog   # macOS
    # or the prebuilt oss-cad-suite: github.com/YosysHQ/oss-cad-suite-build
    iverilog -V | head -1
    
    The run scripts compile with -g2012, so you need a build with SystemVerilog-2012 support. CI uses the pinned Icarus build installed by ci/install-iverilog.sh; use that build if an older package fails the next-time-step checks.
  3. Verilator — optional, and the fast two-state backend:
    sudo apt install verilator zlib1g-dev liblz4-dev   # Debian/Ubuntu
    brew install verilator lz4                          # macOS
    verilator --version
    
    The shared host requires Verilator 5.050 or newer for runtime VPI loading. Install lz4 if you want waveforms. Both DEBUG and RECORD modes build Verilator's FST writer and need its development headers and library.
  4. Optional but recommended: VS Code + rust-analyzer, an AI coding assistant, and GTKWave for waveforms.

Platform note: rustdv's simulator backend is VPI-based and is developed and tested on Linux and macOS with Icarus and Verilator. Windows users should work inside WSL, and commercial simulators are not supported today. STATUS.md in the repository records what has actually been run, and where.

Run the example

Run the known-good example before using your own RTL, so toolchain problems are separate from testbench bugs.

git clone https://github.com/rustdv/rustdv.git
cd rustdv
sim/run_smoke.sh icarus      # checks the simulator alone   → SMOKE: PASS
sim/run_rustdv.sh            # full Rust testbench on TinyALU → REGRESSION: PASS
cargo test --workspace       # unit tests, no simulator      → all green

These commands check the Icarus/Rust path and pure-Rust unit tests. For Verilator, also run sim/run_rustdv.sh release verilator before using your own design. Resolve any setup failure before growing the testbench.

Set up your project

Keep your project outside the rustdv clone, next to it:

hobby/
├── rustdv/                  ← the framework clone
└── my-core/                 ← yours
    ├── hdl/                 ← the RTL you downloaded
    ├── my_core_tb/          ← your Rust testbench crate
    └── sim/
        ├── timescale.v
        └── run.sh

Use rustdv/tinyalu_tb/Cargo.toml and its src/ directory as the worked example. For the Icarus launch sequence, use output/examples/sim-common/run_sim.sh as a reference and adapt its crate, top-module, and RTL paths for your project.

Create a library crate at my-core/my_core_tb. Add these sections to its Cargo.toml; dependency paths are relative to that manifest:

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
rustdv = { path = "../../rustdv/rustdv" }   # → the clone's facade crate

[dev-dependencies]
rustdv-vpi-stubs = { path = "../../rustdv/rustdv/rustdv-vpi-stubs" }

The dev-dependency satisfies VPI symbols when unit tests run without a simulator. Copy the repository’s rust-toolchain.toml into your external project if you want the same compiler. On macOS, also use its .cargo/config.toml linker settings; a sibling framework clone’s Cargo configuration is not inherited by your testbench.

Build your first testbench

A rustdv testbench is a Rust library that the simulator loads. Your project needs:

  1. Cargo.toml — two settings matter and both are required: crate-type = ["cdylib", "rlib"], plus the rustdv-vpi-stubs dev-dependency so cargo test links. Add use rustdv_vpi_stubs as _; under #[cfg(test)] in the crate root.
  2. src/lib.rs — starts with use rustdv::prelude::*;, one rustdv::vpi_bootstrap!();, and one #[rustdv::test] function that touches a signal and starts a clock if your DUT needs one. A design that already supplies its own clock needs only edge waits. Get this passing before adding the rest of the verification code.
  3. sim/timescale.v — sets time units for RTL that does not declare its own. Follow the repository launcher's convention of compiling it first so delay units match the testbench.
  4. sim/run.sh — builds the library (.so on Linux, .dylib on macOS), copies it as .vpi, compiles your RTL, and runs vvp. Keep generated libraries/designs under /tmp/rustdv-$(id -u)/. Set the crate name, top module name, and RTL file list. Export the selected module as RUSTDV_TOP before starting the simulator; when it is unset, rustdv uses the first top-level module returned by VPI.
  5. Your RTL, unmodified, in hdl/.

Run sim/run.sh and check that the smoke test passes. Then grow the BFM, driver, monitors, scoreboard, and sequences. The complete worked example is rustdv/tinyalu_tb/; the book's Interlude walks through its files.

Reading and writing simulator values

Start with use rustdv::prelude::*;. It brings HandleChildren into scope for hierarchy/member lookup and HandleEvent for logic edges. Import rustdv::sim::handle::HandleBase when you need name() or full_name().

Choosing a handle

Simulator object Lookup from a hierarchy or aggregate Handle
Logic, bit, integer, or enum variable signal(name) LogicHandle
Real variable real(name) RealHandle
String variable string(name) StringHandle
Unpacked struct or union aggregate(name) AggregateHandle
Child module hierarchy(name) HierarchyHandle

child(name) returns a SimHandle. Its as_signal(), as_real(), as_string(), as_aggregate(), and as_hierarchy() conversions return Result; a wrong kind reports HandleError::WrongKind. children() enumerates the objects the simulator exposes.

VPI visibility depends on the simulator and its build settings. Unpacked aggregates require vpiStructVar, vpiUnionVar, and vpiMember support; the framework type tests describe that requirement. Packed aggregates exposed as logic variables use LogicHandle. Verilator uses two-state values; use Icarus for X/Z behavior.

Logic and integer values

Logic handles provide get_bool(), get_u8(), get_u16(), get_u32(), get_u64(), get_u128(), and get_bigint(). Each returns a Result and rejects X/Z; a fixed-width read also rejects a signal wider than its target type. get_bool() requires a one-bit signal. Integer access is unsigned.

Each type has scheduled and immediate setters, such as set_u32() and set_u32_now(). Use BigUint for values wider than the primitive integer types. This example expects a writable 160-bit data signal:

use rustdv::prelude::*;

pub async fn write_wide_data(dut: HierarchyHandle) -> Result<(), TestError> {
    let data = dut.signal("data")?;
    let expected = BigUint::from(1u8) << 159usize;
    data.set_bigint(&expected);
    read_write().await;
    assert_eq!(data.get_bigint()?, expected);
    Ok(())
}

Use get_logic() and set_logic() to preserve all four logic states, and get_binstr() for an MSB-first string. LogicArray::from_binstr() and to_binstr() use that same order; indexed access is LSB-first (value[0] is the least significant bit and returns Option<Logic>). LogicArray::from_vpi_words() and to_vpi_words() preserve the exact width with VPI's (aval, bval) word representation.

Convert a LogicArray with u64::try_from(&value) or the corresponding primitive/BigUint conversion. These reject X/Z. Fixed-width conversions of an already-read LogicArray keep the low bits; use the handle's typed getter when you want a width check.

Real, string, and aggregate values

Real handles read and write f64. String reads return owned Rust strings; string writes copy their input and return an error for embedded NUL bytes. Read and write aggregate values through their named members, including nested aggregates. Union members share storage.

This example uses the objects in the framework's probe.sv:

use rustdv::prelude::*;

pub async fn write_record(dut: HierarchyHandle) -> Result<(), TestError> {
    let gain = dut.real("real_sig")?;
    let label = dut.string("string_sig")?;
    let record = dut.aggregate("record_sig")?;
    gain.set(1.25);
    label.set("ready")?;
    record.signal("number")?.set_u32(42);
    read_write().await;
    assert_eq!(gain.get()?, 1.25);
    assert_eq!(label.get()?, "ready");
    assert_eq!(record.signal("number")?.get_u32()?, 42);
    Ok(())
}

Scheduled writes apply at ReadWrite, retain the last write to each handle, and preserve the ordering of distinct handles. A write made during ReadWrite applies immediately. Immediate setters skip buffering: logic uses names such as set_u32_now() and set_logic_now(), while real/string use set_now(). Both paths reject writes from ReadOnly.

Selecting the DUT root

Export RUSTDV_TOP before loading the testbench to select an exact root module name. Without it, rustdv uses the first root returned by VPI. Select the DUT explicitly when you also load a waveform-dump module. The shipped run scripts already export their selected DUT name.

Updating an existing testbench

Earlier spelling or behavior Current API
Simulator-layer AnyHandle SimHandle (the low-level GPI enum remains AnyHandle)
handle.as_logic() handle.as_signal()?
Optional handle conversions Result-returning as_*() conversions with kind errors
Logic get() / set() / set_now() get_logic()? / set_logic() / set_logic_now()
Infallible get_binstr() get_binstr()?
LogicArray::to_u64() u64::try_from(&value)?
MSB-first LogicArray::bit(i) LSB-first value[i]; adjust index calculations
Inherent hierarchy, edge, and naming methods Bring HandleChildren, HandleEvent, and HandleBase into scope as needed

Simulator features

Use Icarus for four-state behavior and the book’s reference transcripts. For a VCD, add a dump module and compile it as an additional root with -s dump, alongside the selected DUT root. For a DUT named my_top:

module dump;
  initial begin $dumpfile("waves.vcd"); $dumpvars(0, my_top); end
endmodule

Keep RUSTDV_TOP=my_top when starting that simulation so the framework selects the DUT rather than the dump module.

The repository’s Verilator launcher offers these modes:

Mode Use it for
FAST Two-state functional runs with top-level port visibility.
DEBUG Selected internal VPI visibility and FST from time zero.
RECORD Runtime-controlled FST capture windows; no file opens until requested.
INSPECT Selected internal VPI visibility without waveform instrumentation.
COVERAGE RTL line/expression coverage, separate from functional coverage collected in Rust.

service_read_only() runs a synchronous operation after the design settles while holding simulation time. Runtime capture uses rustdv::sim::simulator_trace at that settled point; blocking service waits need a wall-clock deadline or cooperative cancellation. Simulation-time timeouts cannot release a blocked closure.

The simulator guide documents mode selection, control files, trace lifecycle, and coverage commands. These modes require the repository’s scripts and C++ host in addition to the Rust crates. DEBUG and RECORD need lz4 for FST; waveform and RTL-coverage instrumentation use separate builds.

Grow and verify the testbench

Use rust-analyzer for type and ownership diagnostics. Missing runtime connections are reported during elaboration. Grow the bench in this order:

  1. Transactions and a predictor, with ordinary #[test] unit tests and cargo test; no simulator is needed for this logic.
  2. The BFM’s reset and one operation, exercised by a directed simulator test.
  3. Monitors and a scoreboard, then sequences and random tests.

A stage is complete when its regression passes and its checks detect an intentionally corrupted DUT. A scoreboard that compares no transactions is not proof of correct hardware. For a hang, reproduce the printed RUSTDV_RANDOM_SEED and inspect the first failure in the log.

The verification skills provide a plan-to-testbench-to-mutation-check workflow. Use their SKILL.md files as human checklists or give them to an AI assistant. Keep the framework clone and your project accessible together, and start with a smoke test before asking the assistant to build a full bench. For example:

Build a Rust testbench for the SPI core in my-core/hdl/, using the verification skills and TinyALU testbench as references. First prove that clock and reset work, then grow the BFM and scoreboard. Show real run output and verify that a deliberately broken DUT fails.

Further reading