Testbench Generation
This page describes how to generate simulation testbenches from a single Rust-side test pattern.
Goal
You can describe one test pattern in Rust and generate simulator input for each backend:
- logical Verilog for Icarus Verilog
- rsfqlib Verilog for Icarus Verilog
- rsfqlib SPICE for JoSIM
Circuit module generation remains separate from testbench generation.
Existing backends such as LogicalVerilog, RsfqlibVerilog, and RsfqlibSpice generate the circuit modules.
The testbench generator creates the top-level simulation wrapper, input patterns, dump settings, and observed outputs.
Example
#![allow(unused)]
fn main() {
let test = Testbench::new(&fa)
.cycles(11)
.signals(["cin", "b", "a"], 0..8, 0.5)
.constant("clk", 1, 0.0)
.observe(["cout", "s"])
.period_ps(100.0);
}
Generate a testbench by choosing a testbench backend:
#![allow(unused)]
fn main() {
test.print(LogicalVerilogTestbench);
test.print(RsfqlibVerilogTestbench);
test.print(RsfqlibSpiceTestbench);
}
This describes an 11-cycle simulation.
The first 8 cycles enumerate the full-adder input truth table, and the remaining 3 cycles are automatically filled with zeros for cin, b, and a.
The clk signal is not a special testbench concept.
It is defined as a normal input signal, here using the constant helper because many SFQ examples need a clock pulse in every cycle.
Stimulus API
Each input port must be specified by exactly one of these methods:
#![allow(unused)]
fn main() {
.signal(name, values, phase)
.signals(names, values, phase)
.constant(name, value, phase)
.pulse(name, cycles, phase)
.toggle(name, cycles, phase)
}
The same signal must not be specified more than once.
signal
signal defines one 1-bit input signal by an integer sequence.
#![allow(unused)]
fn main() {
.signal("a", [0, 1, 0, 1], 0.5)
}
The values must be 0 or 1.
signals
signals defines multiple 1-bit input signals from an integer sequence.
#![allow(unused)]
fn main() {
.signals(["cin", "b", "a"], 0..8, 0.5)
}
Signal names are MSB first.
For example, with ["cin", "b", "a"], the value 4, written as 0b100, expands to:
cin = 1
b = 0
a = 0
Each integer value must fit in the width given by the number of signal names.
constant
constant defines a signal that has the same value for every simulation cycle.
#![allow(unused)]
fn main() {
.constant("clk", 1, 0.0)
}
The value must be 0 or 1.
pulse
pulse defines a signal that is 1 only at the listed cycle indices and 0 otherwise.
#![allow(unused)]
fn main() {
.pulse("trigger", [3, 7, 10], 0.5)
}
The cycle indices must be within the simulation length specified by cycles.
toggle
toggle defines a signal with initial value 0.
At each listed cycle index, the signal value is inverted and keeps that new value until the next toggle.
#![allow(unused)]
fn main() {
.toggle("mode", [2, 6], 0.4)
}
In this example, mode is 0 before cycle 2, 1 from cycle 2 through cycle 5, and 0 again from cycle 6 onward.
Cycles And Padding
cycles(n) sets the total simulation length.
This includes both active input patterns and any quiet cycles at the end.
#![allow(unused)]
fn main() {
.cycles(11)
}
Length rules:
- if
signalhas more thannvalues, it is an error - if
signalhas fewer thannvalues, zeros are appended - if
signalshas more thannvalues, it is an error - if
signalshas fewer thannvalues, zeros are appended for every signal constantexpands toncyclespulseandtogglecycle indices must be less thann
There is no separate flush_cycles setting.
Quiet time at the end of a simulation is represented by choosing a larger cycles value.
Phase And Period
Each stimulus definition carries its own phase.
#![allow(unused)]
fn main() {
.signal("a", [0, 1, 0, 1], 0.5)
.constant("clk", 1, 0.0)
}
The phase is a real value in the range 0.0 <= phase < 1.0.
It represents the position of the pulse within a simulation cycle.
period_ps sets the cycle period in picoseconds and is required:
#![allow(unused)]
fn main() {
.period_ps(100.0)
}
For physical backends, the event time is:
cycle_index * period_ps + phase * period_ps
For phase = 0.0, the pulse is placed on the next cycle boundary instead of time zero.
This keeps the first physical pulse away from simulator startup time and matches the existing hand-written samples.
The default SPICE pulse shape is the same as the existing hand-written samples: the pulse rises from zero, reaches 827.13u, and returns to zero over a short picosecond-scale interval.
SPICE input pulses are generated as current sources and passed through THmitll_DCSFQ and THmitll_JTL before reaching the circuit input, matching the timing samples.
For rsfqlib Verilog, pulses are represented by signal edges.
The generated testbench toggles an input with ^= 1 at each pulse time instead of driving a short 0/1 level pulse.
Observed Signals
Input signals are automatically observed.
observe lists only additional signals, such as outputs or internal nodes.
#![allow(unused)]
fn main() {
.observe(["cout", "s"])
}
For the full-adder example above, the final observed signal list is:
cin, b, a, clk, cout, s
SPICE generation uses the observed list for .print v(...) statements.
Verilog generation uses $dumpvars(0, top) to write the waveform.
SPICE internal hierarchical names, such as a1.XTOP, are accepted by observe without strict validation against the circuit port list.
Generated Outputs
The circuit name determines backend output names.
For a circuit named FullAdder:
logical Verilog: FullAdder.vcd
rsfqlib Verilog: FullAdder.vcd
rsfqlib SPICE: FullAdder.csv
The generated top-level testbench or SPICE file is separate from the generated circuit module file.
The generated SPICE testbench includes the rsfqlib SPICE library as:
.include /path/to/all.cir
Replace /path/to/all.cir with the path to your local all.cir file before running JoSIM.
Validation Rules
The generator validates the testbench before emitting backend code:
cycles(n)must be specifiedperiod_ps(...)must be specified- phase must satisfy
0.0 <= phase < 1.0 - 1-bit values must be
0or1 signalsvalues must fit in the given bit width- the same signal must not be specified more than once
- every circuit input port must be specified
- stimulus names must refer to circuit input ports
observenames are allowed to include backend-specific hierarchical names