Step testbenches
Coroutine testbenches where every await is one clock edge.
The testbench API is built on async functions, and the rule that makes it work is that .await means one clock edge. tb.step().await advances time; tb.set(..).await applies an input and takes no edge.
That rule was learned the hard way. An earlier version of set took an edge, which meant every testbench acquired a spurious leading edge โ invisible in CRC-3, whose all-zero init is a fixed point, and visible in CRC-32, whose all-ones init is not.
Step by step
- 01
Run a closure against a design
Testbench::newinspects the design's declared ports andrunexecutes the async closure, checking at the end that the testbench drove only ports the design declares.use ferrite_lithic_tb::Testbench; let tb = Testbench::new(&design)?; tb.run(|tb| async move { tb.pulse(ferrite_lithic_corpus::RESET).await; tb.set("count", 4).await; tb.step().await; })?; - 02
set takes no edge, step takes the edge
This is the whole discipline.
setwrites a value into the input and returns;stepadvances the clock. Combining them into one call is what made the leading-edge bug invisible in one design and obvious in another.tb.set("in_bit", 1).await; // no edge tb.step().await; // one edge // Read what the design settled on, before the next edge. let ready = tb.peek("in_ready"); - 03
Drive buses by value, not by bit
set_bitsanddrive_bitstake aBits, so a testbench never has to shift a bit into a position by hand โ which is where a wrong bit order would otherwise live.tb.set_bits("in_byte", Bits::constant(0x5a, 8)?).await; tb.drive_bits("state", &snapshot)?; - 04
Collect the run and assert on it
seriesreturns the cycle-indexed values the testbench recorded, so a test can assert on a whole run.waveshands back the same data in the wave crate's model, and a failure captures both.let out = tb.series("symbol"); assert_eq!(out.first(), Some(&Bits::constant(72, 9)?));
What bites
peek reads the settled value
peekandvalueread what the design presents on the current cycle, which is what the design latches on the edge that follows. Reading after the edge gets the next cycle's answer, and the loss shows up several symbols later.Only declared ports
runfails if the testbench drives a name the design does not declare. A typo in a port name is an error rather than a stimulus that silently goes nowhere.
Notes
- decision
async, because a clock is a suspension point
A testbench that reads like a list of clock edges in the order they happen is easier to check against a design than one built from nested callbacks. The
.awaitis the clock edge and nothing else is.