Skip to content
code0.HDL

Testbench techniques

Read a failed expectation down to the step that produced it, wait for an event without hanging the test, and use the idle-cycle timeout to tell a slow design from a stuck one.

In this chapter

  • Locate the cycle a failed expect reports and the design state behind it
  • Write a wait loop that advances the clock and ends on the event it waits for
  • Choose an idle-cycle timeout that catches a hang without failing a slow but correct run
Concepts in this chapter
  • Testbench techniques
Prerequisite concepts

Registers, RegInit, and stepping the clock wrote testbenches with test, poke, peek, expect and clock.step. This chapter adds the techniques that sit on top of them: what happens before your code runs, how to read a failed expectation, and how a wait loop ends.

1. What happens before your code runs

test(...) does four things before calling your code:

StepAction
1Elaborates the module and lowers the circuit for the simulator
2Creates a working directory: test_run_dir/<name>/ when you pass a name, otherwise test_run_dir/chisel_test_<milliseconds>/
3Holds reset high for one clock cycle, then releases it, so every RegInit starts at its initial value
4Calls your code with dut

Because of step 3, you do not need to reset the design yourself at the start of a test. The cycle that chiseltest spends in reset is not counted in the step numbers that failure messages report.

Check

In Acc, next is acc + in and sum is the register acc. At the start of the body you poke en high and in = 3, peek both outputs, call clock.step(), and peek both again. What do the two peeks show?

Your answer

2. Reading a failed expectation

An expect that does not hold is the most useful thing a testbench can produce: it says which step, which port, and which two values disagreed. The message is worth reading once on purpose.

This example fails on purpose. It uses the same Acc. The testbench adds 3 and then 4 with en high, and then expects the running total to be 8.

Acc.scala
import chisel3._

class Acc extends Module {
  val io = IO(new Bundle {
    val en   = Input(Bool())
    val in   = Input(UInt(8.W))
    val next = Output(UInt(8.W))
    val sum  = Output(UInt(8.W))
  })
  val acc = RegInit(0.U(8.W))
  // next is combinational: it follows a poke without a clock step.
  io.next := acc + io.in
  when (io.en) {
    acc := io.next
  }
  io.sum := acc
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Acc) { dut =>
    dut.io.en.poke(true.B)
    dut.io.in.poke(3.U)
    dut.clock.step()
    dut.io.in.poke(4.U)
    dut.clock.step()
    // The wrong expectation: 3 + 4 is 7, not 8.
    dut.io.sum.expect(8.U, "running total after 3 + 4")
    println("not reached")
  }
}

Practice this concept

Use this concept in a short Chisel problem as your next step.

Predict before you run. What step number will the message report, and which value will appear as the actual one? Will not reached be printed?

Check the result.

ItemDetail
Wrong codedut.io.sum.expect(8.U, ...), while 3 + 4 is 7
Failing stageSimulation, at the expect after the second step; compilation and elaboration succeed
Error messagechiseltest.FailedExpectException: In step 2: io_sum=7 (0x7) did not equal expected=8 (0x8): running total after 3 + 4
FixThe design is right and the expectation is wrong here: expect(7.U, ...) passes

In step 2 counts the two clock.step() calls in the body; the reset cycle before the body is not included. The println after the expect never runs, so the run prints nothing on standard output. The stack trace's Tb.scala line points at the failing expect.

Check

Move the expect(8.U, ...) line up so it runs right after the first clock.step(). What does the failure message start with?

Your answer

In real work a failure is just as often a design bug. Decide which side is wrong first. The step number tells you which cycle to inspect, for example with a printf or a waveform.

3. Waiting for an event

3.1 Wait loops

Many designs take a variable number of cycles and signal completion on an output. A testbench waits for that signal with a loop that peeks and steps.

Syntax

while (!dut.io.done.peek().litToBoolean) {
  dut.clock.step()
}
var cycles = 0
while (!dut.io.ready.peek().litToBoolean) {
  dut.clock.step()
  cycles += 1
}
println(s"ready after $cycles cycles")

If the design never raises the signal, the loop never ends on its own.

3.2 The idle-cycle timeout

chiseltest guards each clock with an idle-cycle timeout. Every step counts one idle cycle. A poke that changes an input value sets the count back to 0; poking the same value does not. peek and expect do not change it. When the count reaches the limit, the test fails with a TimeoutException.

Syntax

dut.clock.setTimeout(cycles)   // default limit: 1000
dut.clock.setTimeout(0)        // disables the check
Call between stepsEffect on the idle count
step()Adds one
poke that changes an input valueResets to 0; the same value leaves it unchanged
peek, expectNone

A wait loop only peeks, so its steps keep adding up until the limit stops it. That is the behavior you want: polling a signal is not progress.

3.3 Example: a wait loop that never ends

Waiter counts while start is high and raises done when the count reaches 10. The testbench sets a timeout of 20, pokes start high for one cycle, pokes it low, and then waits for done.

This example fails on purpose. Because start drops after one cycle, the count stops at 1 and done never rises. Before running, decide how many steps the loop takes before the timeout stops it.

Waiter.scala
import chisel3._

class Waiter extends Module {
  val io = IO(new Bundle {
    val start = Input(Bool())
    val done  = Output(Bool())
  })
  val count = RegInit(0.U(8.W))
  // The count advances only while start is held high.
  when (io.start) {
    count := count + 1.U
  }
  io.done := count === 10.U
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Waiter) { dut =>
    dut.clock.setTimeout(20)
    dut.io.start.poke(true.B)
    dut.clock.step()
    // The bug: start drops after one cycle, so done never rises.
    dut.io.start.poke(false.B)
    var cycles = 1
    while (!dut.io.done.peek().litToBoolean) {
      dut.clock.step()
      cycles += 1
    }
    println(s"DONE after $cycles cycles")
  }
}

Practice this concept

Use this concept in a short Chisel problem as your next step.

Check the result.

ItemDetail
Wrong codedut.io.start.poke(false.B) after one step, while Waiter counts only while start is high
Failing stageSimulation, inside the wait loop
Error messagechiseltest.TimeoutException: timeout on Waiter.clock ... at 20 idle cycles
FixKeep start high until the count reaches 10, or change the design so that one pulse starts the count

The message says that 20 steps passed after the last poke, which was start going low. The timeout does not know why done stayed low; it only bounds how long the testbench waits. The DONE line never prints.

Check

Which single change makes this test print DONE?

Your answer

Try it yourself.

  1. Delete dut.io.start.poke(false.B). Now start stays high, the count reaches 10 after ten steps, and the test prints DONE after 10 cycles. Ten steps since the last poke are below the limit of 20.
  2. Keep that fix and change setTimeout(20) to setTimeout(5). The design is now correct, but the test fails again, this time at 5 idle cycles. A timeout shorter than the design's legitimate latency is a testbench bug.
  3. Restore the original code and use setTimeout(0). The check is off, and the original bug turns back into a test that never finishes on its own.

Set the limit above the longest wait you expect and well below the point where a hang would waste time.

4. Common mistakes

4.1 A wait loop without step

// Wrong: time never advances, and no timeout can stop it
while (!dut.io.done.peek().litToBoolean) { }

// Right
while (!dut.io.done.peek().litToBoolean) { dut.clock.step() }

The idle-cycle timeout counts steps. A loop that only peeks never advances the clock, so done cannot change and the timeout never triggers.

5. Summary

TopicKey point
Before your codetest(...) elaborates the module, starts treadle, pulses reset once, and releases it
expect failureReports the step number, the port, the actual and expected values, and your message
Step numberingCounts the steps your code took; the reset cycle belongs to the setup
Wait loopwhile (peek(...)) clock.step(1) — the step is what lets the design move
setTimeout(n)Fails after n steps without a poke; default 1000; 0 disables it

6. Check your understanding

  1. In In step 2: io_sum=7 ..., what does "step 2" count, and why is the reset cycle not included?

    Show answer

    It counts the clock steps taken by your code. The reset cycle belongs to chiseltest's setup before your code runs, so it is not part of the count.

  2. Why does polling done with peek not reset the idle-cycle count, and why is that useful?

    Show answer

    Only pokes that change an input value reset the count; reading a signal does not change the design's inputs. If polling reset the count, a wait loop for a signal that never arrives would never time out.

  3. A test needs about 40 cycles for a legitimate result and has setTimeout(30). What happens, and what should you change?

    Show answer

    If no input is poked during the wait, the test fails with a TimeoutException at 30 idle cycles even though the design is correct. Raise the limit above 40, for example to 60, so that it still catches a real hang.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows