1. RegNext is a register whose next value is its argument
RegNext(x, init) creates a register, connects x as its next value, and returns the register's
current value, which is x one edge later. The second argument is the reset value; without it the
register has no reset and no defined start. Two in a row delay by two edges: the second register's
next value is the first register's current value, not x. Each RegNext in a chain adds one cycle.
Form
val d1 = RegNext(io.in, 0.U) // in, one edge ago
val d2 = RegNext(d1, 0.U) // in, two edges agoFill in the two-stage delay before touching Chisel. The in column is the input during each cycle;
write what s1 and s2 hold after the edge that ends it.
Predict, then compare · delay2
Fill in each cell before comparing. Empty cells count as a mismatch.
| Cycle | ininputs sampled at the edge | s1after the edge | s2after the edge |
|---|---|---|---|
| 0 | 1 | ||
| 1 | 0 | ||
| 2 | 1 | ||
| 3 | 1 | ||
| 4 | 0 | ||
| 5 | 0 |
2. A rising edge is "1 now, 0 one edge ago"
A combinational expression sees only the current cycle. To detect a change over time, keep the
previous input in a register and compare: a rising-edge detector compares the input now with the
input one cycle ago, and RegNext supplies the latter. A cycle where the old value is 0 and the new
one is 1 is a rising edge.
Form
val previous = RegNext(signal, resetValue)
val rising = signal && !previousThe reset value says what the circuit assumes about the cycle before reset ended.
3. Example: a delayed copy and a rising edge
Edge has three signals:
| Signal | Definition | Meaning |
|---|---|---|
last | RegNext(io.in, false.B) | Last cycle's input; false during reset |
delayed | last | The input delayed by one cycle |
rise | io.in && !last | True only when the input is 1 now and was 0 last cycle |
import chisel3._
class Edge extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val delayed = Output(Bool())
val rise = Output(Bool())
})
// last holds the previous cycle's input; false.B is its reset value.
val last = RegNext(io.in, false.B)
io.delayed := last
// High only when in is 1 now and was 0 last cycle.
io.rise := io.in && !last
}In every cycle the testbench pokes one value from the pattern 0 1 1 1 0 1 0 0, prints delayed and
rise before stepping, then steps the clock. So cycle n shows the register as it was captured at
the edge that ended cycle n − 1.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Edge) { dut =>
val pattern = Seq(0, 1, 1, 1, 0, 1, 0, 0)
for ((v, cycle) <- pattern.zipWithIndex) {
dut.io.in.poke((v == 1).B)
val d = dut.io.delayed.peek().litValue
val r = dut.io.rise.peek().litValue
println(s"cycle=$cycle in=$v delayed=$d rise=$r")
// The register samples in at this clock edge.
dut.clock.step()
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run. Extend this table to cycle 7; the first three rows are filled in.
| Cycle | in | last | delayed | rise |
|---|---|---|---|---|
| 0 | 0 | 0 (reset value) | 0 | 0 |
| 1 | 1 | 0 | 0 | 1 |
| 2 | 1 | 1 (captured from cycle 1) | 1 | 0 |
in stays high through cycles 1 to 3. How many times does rise go high over the whole pattern, and
in which cycle does it fire again?
Check
Over the eight cycles, in how many cycles is rise 1?
Check the result. Press Run. Read delayed as the in column shifted down by one line, and
check that rise is 1 exactly where a 0 line is followed by a 1 line.
| Column | What it shows |
|---|---|
delayed | The in column shifted down one line: one register, exactly one cycle of delay |
rise | High only in cycles 1 and 5, the two cycles where a 0 is followed by a 1 |
| Cycles 2 and 3 | in is 1 but so is last, so rise stays 0; a long high stretch produces one pulse |
That is the observable contract of an edge detector: one cycle of output per 0-to-1 transition.
4. Change the comparison
In the lab, change rise to !io.in && last and run again.
Check
With rise = !io.in && last, in which cycles is the output 1?
| Edit | What happens |
|---|---|
Change rise to !io.in && last | It becomes a falling-edge detector, firing in cycles 4 and 6 where a 1 is followed by a 0. Same register, different logic around it |
Change it to io.rise := RegNext(io.in && !last, false.B) | The output gains a register, so the pulses move one cycle later, to cycles 2 and 6 |
Note. A registered output, as in the second edit, decouples a module's output timing from the logic behind it but costs one cycle of latency. Decide it per interface and document the latency where callers will see it.
5. What a VCD file records
A println or printf shows the values you chose to print, and an expect checks the values you
chose to check. A waveform records every named signal on every cycle, so you can look for the
problem you did not anticipate. chiseltest writes a waveform as a VCD (Value Change Dump) file, and a
testbench hands that file to the lab's waveform viewer.
5.1 Declarations and a change log
A VCD file is plain text in two parts. The header declares each signal once, with its width, its
name, and a short identifier such as ! or #. The body is a time-ordered log. A line #time
sets the current time, and the lines after it record only the signals whose value changed at that
time. A viewer rebuilds each trace by holding a signal's last value until its next change.
Form
$timescale <unit> $end $var wire <width> <id> <name> $end #<time> <0 or 1><id> one-bit change b<binary value> <id> multi-bit change
Here is a hand-written file with a one-bit clk and a 2-bit q:
$timescale 1ns $end $var wire 1 ! clk $end $var wire 2 " q $end #0 0! b0 " #5 1! b1 " #10 0!
| Line | Meaning |
|---|---|
$timescale 1ns $end | Each time unit is one nanosecond |
$var wire 1 ! clk $end | Declares clk, 1 bit wide, identified by ! |
#0, then 0! and b0 " | At time 0, clk is 0 and q is 0 |
#5, then 1! and b1 " | At time 5, clk rises and q becomes 1 |
#10, then 0! | At time 10, only clk changes; q holds 1 |
5.2 Which signals are recorded
chiseltest's VCD contains the signals that have names in the simulated circuit:
| Recorded | Example in this chapter's Blinker |
|---|---|
Top-level ports, with . flattened to _ | io_en, io_led, io_count |
| The implicit clock and reset | clock, reset |
Registers bound to a val | count, led |
Combinational values bound to a val | wrap |
A value bound to a val keeps its name, while an unnamed expression gets a generated one. Unnamed
temporaries, which Printing from hardware shows with names such as
_count_T, exist in the simulated circuit but are left out of the VCD.
Note. If you want to see a value in a waveform, bind it to a val. Naming costs nothing in the
generated hardware.
6. Recording a VCD with chiseltest
6.1 WriteVcdAnnotation
chiseltest writes a VCD when the annotation list passed to test contains WriteVcdAnnotation. The
annotation lives in chiseltest.simulator.
Form
import chiseltest.simulator.WriteVcdAnnotation
test(new TopModule, Seq(WriteVcdAnnotation), "name") { dut => ... }test(new Counter, Seq(WriteVcdAnnotation), "run1") { dut =>
dut.io.en.poke(true.B)
dut.clock.step(5)
}
// writes test_run_dir/run1/Counter.vcd| Part | Effect |
|---|---|
Seq(WriteVcdAnnotation) | Turns VCD output on for this test |
"run1" | Fixes the working directory as test_run_dir/run1/ |
Counter | The top module's name becomes the file name: Counter.vcd |
Without a name argument, the directory is test_run_dir/chisel_test_<milliseconds>/, which changes
on every run. Passing a fixed name is what makes the path predictable.
6.2 Handing the file to the lab
The lab shows a waveform only for a file called dump.vcd in the run's working directory. After the
command finishes, the runtime sends that file to the browser. chiseltest does not know about this
convention, so the testbench copies the file itself.
Form
import java.nio.file.{Files, Paths, StandardCopyOption}
Files.copy(sourcePath, Paths.get("dump.vcd"), StandardCopyOption.REPLACE_EXISTING)The copy must happen after test returns. The VCD file does not exist yet while your test body
is running; the simulator writes it when the test ends. A copy placed inside the body finds nothing
to copy.
Caution. Check that the file exists before copying, and throw an exception if it does not.
Files.copy also throws if the source is missing; the explicit check supplies a clearer message
with the expected VCD path when the annotation is missing or the test name is wrong.
7. Example: the same story as a waveform
Blinker counts with a 3-bit register count from 0 to 5 and wraps, but only while io.en is high.
wrap is a named combinational signal that is true when count is 5, so it appears in the waveform
too. led toggles at each wrap. Both registers also drive output ports.
import chisel3._
class Blinker extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val led = Output(Bool())
val count = Output(UInt(3.W))
})
val count = RegInit(0.U(3.W))
// A named val becomes a signal in the waveform.
val wrap = count === 5.U
when (io.en) {
count := Mux(wrap, 0.U, count + 1.U)
}
val led = RegInit(false.B)
when (io.en && wrap) {
led := !led
}
io.led := led
io.count := count
}Tb.scala passes Seq(WriteVcdAnnotation) and the name "tb". It pokes en high, steps eight
times while printing count and led, then pokes en low and steps twice more. After test
returns, it checks that test_run_dir/tb/Blinker.vcd exists, throws an exception if it does not, and
copies it to dump.vcd.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
import chiseltest.simulator.WriteVcdAnnotation
import java.nio.file.{Files, Paths, StandardCopyOption}
object Main extends App {
// The third argument fixes the directory: test_run_dir/tb/.
test(new Blinker, Seq(WriteVcdAnnotation), "tb") { dut =>
dut.io.en.poke(true.B)
for (cycle <- 1 to 8) {
dut.clock.step()
println(s"CYCLE $cycle count=${dut.io.count.peek().litValue} led=${dut.io.led.peek().litValue}")
}
dut.io.en.poke(false.B)
dut.clock.step(2)
}
// The VCD is named after the top module; copy it where the lab looks.
val vcd = Paths.get("test_run_dir", "tb", "Blinker.vcd")
if (!Files.exists(vcd)) throw new RuntimeException(s"VCD not found: $vcd")
Files.copy(vcd, Paths.get("dump.vcd"), StandardCopyOption.REPLACE_EXISTING)
println(s"COPIED $vcd to dump.vcd")
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Latch a PWM duty at the period boundary
Load the duty register only at the edge that ends a period, and confirm the timing against count and level in the waveform.
PracticeKeep a tick divider's phase across pauses
Ticks after a pause land in the wrong cycles; find in the waveform what phase does while en is low.
Practice
Trace the first eight cycles. count goes 1, 2, 3, 4, 5. While count is 5, wrap is true, so at
the next edge count returns to 0 and led flips to 1. Then count continues 1, 2. The two final
steps with en low change nothing.
Predict before you run.
Check
On which CYCLE line does led first read 1?
Check
In the waveform, how many rising edges of clock happen while reset is high?
Then decide two more things from the timeline: does io_en change exactly at a rising edge, or
somewhere between edges? And at which edge do count, wrap, led, and io_led all change
together?
Check the result. The text output matches the trace: led reads 1 from CYCLE 6, together with
count returning to 0, and the run ends with a COPIED line. The waveform appears below the output.
The clock period is 10 units of the 1ns timescale, with rising edges at 11, 21, 31, and so on and
falling edges at 16, 26, and so on.
| Time | Change | What it shows |
|---|---|---|
| 0 | reset = 1 | chiseltest's reset cycle begins |
| 1 | clock rises | The only rising edge while reset is high |
| 6 | clock falls, reset = 0, io_en = 1 | Reset ends and your first poke lands between edges |
| 11, 21, 31, 41 | clock rises, count goes 1 to 4 | Registers change only at rising edges |
| 51 | count = 5, wrap = 1 | A combinational val changes at the same time as the register it reads |
| 61 | count = 0, wrap = 0, led = 1, io_led = 1 | One edge updates several signals at once |
| 86 | io_en = 0 | The poke after eight steps, again at a falling-edge time |
A poke happens between steps, and treadle records it at the current time, which is halfway between rising edges. The viewer places time 0 at the left edge because the first recorded change is at time 0; it computes the start from the change log, not from a header field.
The waveform shows the same simulation the testbench printed, for every named signal, and it also shows timing that the text hid: the reset cycle and the half-cycle offset between pokes and edges. Read input changes where they are drawn, and read register changes at rising edges; activity before your first poke is the reset cycle.
Try it yourself.
- Remove
val wrapand writecount === 5.Udirectly in both places wherewrapwas used. The circuit behaves exactly as before, butwrapdisappears from the VCD's signal list. The comparison still exists, under a generated name that the VCD omits. - Pass
Seq()instead ofSeq(WriteVcdAnnotation). The test prints the same eight lines, but no VCD is written, and the existence check fails the run withjava.lang.RuntimeException: VCD not found: test_run_dir/tb/Blinker.vcd. - Restore the annotation but drop the
"tb"argument. The VCD is written under a directory such astest_run_dir/chisel_test_1792139124602/, the fixed path no longer matches, and the sameVCD not founderror appears.
8. Common mistakes
8.1 Copying the VCD inside the test body
// Wrong: the file does not exist yet while the body runs
test(new Blinker, Seq(WriteVcdAnnotation), "tb") { dut =>
dut.clock.step(8)
Files.copy(vcd, Paths.get("dump.vcd"))
}
// Right: copy after test returns
test(new Blinker, Seq(WriteVcdAnnotation), "tb") { dut =>
dut.clock.step(8)
}
Files.copy(vcd, Paths.get("dump.vcd"), StandardCopyOption.REPLACE_EXISTING)The simulator writes the VCD when the test finishes. During the body, the path
test_run_dir/tb/Blinker.vcd has no file behind it.
8.2 Leaving out the test name
// Wrong: the directory name changes on every run
test(new Blinker, Seq(WriteVcdAnnotation)) { dut => ... }
// Right: a fixed directory
test(new Blinker, Seq(WriteVcdAnnotation), "tb") { dut => ... }Without a name, the VCD lands in test_run_dir/chisel_test_<milliseconds>/, and a fixed copy path
cannot find it.
8.3 Expecting an unnamed expression in the waveform
// Wrong: the comparison has no name, so the VCD omits it
when (io.en && count === 5.U) { led := !led }
// Right: bind it to a val
val toggle = io.en && count === 5.U
when (toggle) { led := !led }Only named values are recorded. With the val, toggle appears in the VCD next to count and
led.
8.4 Reading a poke as a register change
dut.io.en.poke(true.B) // io_en changes now, between edges
dut.clock.step() // count changes at this rising edgeA poke is recorded at the moment it happens, half a cycle away from the rising edges. Read inputs where they change, and read register changes at rising edges. Activity before your first poke is chiseltest's reset cycle.
8.5 Copying without checking
// Wrong: a missing file throws NoSuchFileException without saying why
Files.copy(vcd, Paths.get("dump.vcd"))
// Right: say what is missing
if (!Files.exists(vcd)) throw new RuntimeException(s"VCD not found: $vcd")
Files.copy(vcd, Paths.get("dump.vcd"), StandardCopyOption.REPLACE_EXISTING)An explicit check produces a message that names the expected path, which points straight at a missing annotation or a wrong test name.
9. Apply it: count edges, then read two waveforms
The graded Practice problem below has a counter that adds one in every cycle where in is high. Make
it add one per 0-to-1 transition: rise needs the previous input, which is a RegNext with a reset
value of false.B, exactly as in section 3. An input that is already high when reset ends counts as
one edge, because the register says the previous input was 0. Write the cycle-by-cycle table before
you press Run.
The two problems below the vcd example are solved by reading waveforms. In the first, a PWM
generator's output never rises; load the duty register only at the edge that ends a period, and
confirm the timing against count and level in the waveform. In the second, a tick divider's ticks
arrive late after a pause; look at what phase does while en is low.
10. Summary
| Element | Meaning |
|---|---|
RegNext(x, init) | x one edge later; init after reset. Without init, no reset and no defined start |
two RegNext in a row | two edges of delay |
x && !RegNext(x, false.B) | a rising-edge pulse, one cycle per 0-to-1 transition; !x && previous is the falling edge |
| registered output | one more cycle of latency; decide it per interface and document it |
| VCD | A header of signal declarations followed by a log of value changes over time |
WriteVcdAnnotation | Passed in test's annotation list; writes <TopModule>.vcd |
| Location | test_run_dir/<name>/ with a test name; a timestamped directory without one |
| Lab viewer | Displays dump.vcd from the working directory; copy after test returns |
| Recorded signals | Ports, clock, reset, and val-named registers and wires; unnamed temporaries are omitted |
| Timeline | One rising edge in reset, rising edges every 10 units, pokes between edges |
11. Check your understanding
-
Why does
risestay low in cycle 2 of the edge example even thoughinis still 1?Show answer
The edge at the end of cycle 1 stored 1 in
last. In cycle 2 bothinandlastare 1, soio.in && !lastis false. -
Why does the testbench copy the VCD after
testreturns rather than inside the test body?Show answer
The simulator writes the VCD file when the test finishes. While the body is running, the file does not exist yet.
-
Which rising edge in the waveform belongs to the reset cycle, and why is it not counted as
CYCLE 1?Show answer
The edge at time 1, while
resetis high. chiseltest performs that cycle before the test body runs, and theCYCLEcounter counts only the body's steps. -
Why does
io_enchange at time 6 instead of at a rising edge?Show answer
A poke happens between steps, and treadle records it at the current time. After the reset cycle, the current time is 6, halfway between the rising edges at 1 and 11.
-
What would you change so that the condition
io.en && wrapappears in the waveform under the nametoggle?Show answer
Bind it to a
val, as inval toggle = io.en && wrap, and usetogglein thewhen. The value gets the nametoggle, and the VCD records it. -
A run succeeds, prints all its lines, and shows no waveform. What is the first thing to check?
Show answer
Whether anything created
dump.vcdin the working directory. The testbench must copy the VCD there aftertestreturns; an existence check before the copy turns a missing file into a clear failure.