ferrite-lithic811 tests ยท 21 designs

ferrite-lithic-tbphase A317 tests

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

  1. 01

    Run a closure against a design

    Testbench::new inspects the design's declared ports and run executes 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;
    })?;
  2. 02

    set takes no edge, step takes the edge

    This is the whole discipline. set writes a value into the input and returns; step advances 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");
  3. 03

    Drive buses by value, not by bit

    set_bits and drive_bits take a Bits, 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)?;
  4. 04

    Collect the run and assert on it

    series returns the cycle-indexed values the testbench recorded, so a test can assert on a whole run. waves hands 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

    peek and value read 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

    run fails 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 .await is the clock edge and nothing else is.