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:
| Step | Action |
|---|---|
| 1 | Elaborates the module and lowers the circuit for the simulator |
| 2 | Creates a working directory: test_run_dir/<name>/ when you pass a name, otherwise test_run_dir/chisel_test_<milliseconds>/ |
| 3 | Holds reset high for one clock cycle, then releases it, so every RegInit starts at its initial value |
| 4 | Calls 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?
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.
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
}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.
| Item | Detail |
|---|---|
| Wrong code | dut.io.sum.expect(8.U, ...), while 3 + 4 is 7 |
| Failing stage | Simulation, at the expect after the second step; compilation and elaboration succeed |
| Error message | chiseltest.FailedExpectException: In step 2: io_sum=7 (0x7) did not equal expected=8 (0x8): running total after 3 + 4 |
| Fix | The 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?
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 steps | Effect on the idle count |
|---|---|
step() | Adds one |
poke that changes an input value | Resets to 0; the same value leaves it unchanged |
peek, expect | None |
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.
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
}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.
| Item | Detail |
|---|---|
| Wrong code | dut.io.start.poke(false.B) after one step, while Waiter counts only while start is high |
| Failing stage | Simulation, inside the wait loop |
| Error message | chiseltest.TimeoutException: timeout on Waiter.clock ... at 20 idle cycles |
| Fix | Keep 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?
Try it yourself.
- Delete
dut.io.start.poke(false.B). Nowstartstays high, the count reaches 10 after ten steps, and the test printsDONE after 10 cycles. Ten steps since the last poke are below the limit of 20. - Keep that fix and change
setTimeout(20)tosetTimeout(5). The design is now correct, but the test fails again, this timeat 5 idle cycles. A timeout shorter than the design's legitimate latency is a testbench bug. - 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
| Topic | Key point |
|---|---|
| Before your code | test(...) elaborates the module, starts treadle, pulses reset once, and releases it |
expect failure | Reports the step number, the port, the actual and expected values, and your message |
| Step numbering | Counts the steps your code took; the reset cycle belongs to the setup |
| Wait loop | while (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
-
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.
-
Why does polling
donewithpeeknot 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.
-
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
TimeoutExceptionat 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.