Skip to content
code0.HDL

RegNext, delays, and waveforms

Delay a signal by one edge with RegNext and by two with a second one, build a rising-edge detector from the delayed copy, then read reset, pokes, and register changes off a recorded waveform.

In this chapter

  • Delay a signal by one or two edges with RegNext and give the delay a reset value
  • Build rising- and falling-edge detectors from a delayed copy and predict their pulses cycle by cycle
  • Record a VCD with WriteVcdAnnotation, hand it to the lab as dump.vcd, and say which signals it contains
  • Read reset, clock edges, pokes, and register changes off the recorded timeline
Concepts in this chapter
  • RegNext, one- and two-stage delay
Prerequisite concepts

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 ago

Fill 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.

State table
Cycleininputs sampled at the edges1after the edges2after the edge
01
10
21
31
40
50

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 && !previous

The 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:

SignalDefinitionMeaning
lastRegNext(io.in, false.B)Last cycle's input; false during reset
delayedlastThe input delayed by one cycle
riseio.in && !lastTrue only when the input is 1 now and was 0 last cycle
Edge.scala
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.

Tb.scala
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.

Cycleinlastdelayedrise
000 (reset value)00
11001
211 (captured from cycle 1)10

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.

ColumnWhat it shows
delayedThe in column shifted down one line: one register, exactly one cycle of delay
riseHigh only in cycles 1 and 5, the two cycles where a 0 is followed by a 1
Cycles 2 and 3in 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?

Your answer

EditWhat happens
Change rise to !io.in && lastIt 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!
LineMeaning
$timescale 1ns $endEach time unit is one nanosecond
$var wire 1 ! clk $endDeclares 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:

RecordedExample in this chapter's Blinker
Top-level ports, with . flattened to _io_en, io_led, io_count
The implicit clock and resetclock, reset
Registers bound to a valcount, led
Combinational values bound to a valwrap

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
PartEffect
Seq(WriteVcdAnnotation)Turns VCD output on for this test
"run1"Fixes the working directory as test_run_dir/run1/
CounterThe 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.

Blinker.scala
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.

Tb.scala
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.

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.

TimeChangeWhat it shows
0reset = 1chiseltest's reset cycle begins
1clock risesThe only rising edge while reset is high
6clock falls, reset = 0, io_en = 1Reset ends and your first poke lands between edges
11, 21, 31, 41clock rises, count goes 1 to 4Registers change only at rising edges
51count = 5, wrap = 1A combinational val changes at the same time as the register it reads
61count = 0, wrap = 0, led = 1, io_led = 1One edge updates several signals at once
86io_en = 0The 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.

  1. Remove val wrap and write count === 5.U directly in both places where wrap was used. The circuit behaves exactly as before, but wrap disappears from the VCD's signal list. The comparison still exists, under a generated name that the VCD omits.
  2. Pass Seq() instead of Seq(WriteVcdAnnotation). The test prints the same eight lines, but no VCD is written, and the existence check fails the run with java.lang.RuntimeException: VCD not found: test_run_dir/tb/Blinker.vcd.
  3. Restore the annotation but drop the "tb" argument. The VCD is written under a directory such as test_run_dir/chisel_test_1792139124602/, the fixed path no longer matches, and the same VCD not found error 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 edge

A 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

ElementMeaning
RegNext(x, init)x one edge later; init after reset. Without init, no reset and no defined start
two RegNext in a rowtwo 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 outputone more cycle of latency; decide it per interface and document it
VCDA header of signal declarations followed by a log of value changes over time
WriteVcdAnnotationPassed in test's annotation list; writes <TopModule>.vcd
Locationtest_run_dir/<name>/ with a test name; a timestamped directory without one
Lab viewerDisplays dump.vcd from the working directory; copy after test returns
Recorded signalsPorts, clock, reset, and val-named registers and wires; unnamed temporaries are omitted
TimelineOne rising edge in reset, rising edges every 10 units, pokes between edges

11. Check your understanding

  1. Why does rise stay low in cycle 2 of the edge example even though in is still 1?

    Show answer

    The edge at the end of cycle 1 stored 1 in last. In cycle 2 both in and last are 1, so io.in && !last is false.

  2. Why does the testbench copy the VCD after test returns 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.

  3. 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 reset is high. chiseltest performs that cycle before the test body runs, and the CYCLE counter counts only the body's steps.

  4. Why does io_en change 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.

  5. What would you change so that the condition io.en && wrap appears in the waveform under the name toggle?

    Show answer

    Bind it to a val, as in val toggle = io.en && wrap, and use toggle in the when. The value gets the name toggle, and the VCD records it.

  6. A run succeeds, prints all its lines, and shows no waveform. What is the first thing to check?

    Show answer

    Whether anything created dump.vcd in the working directory. The testbench must copy the VCD there after test returns; an existence check before the copy turns a missing file into a clear failure.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Sources

Licences and notices for the sources this site follows