Skip to content
code0.HDL

Registers, RegInit, and stepping the clock

Create a register with a reset value, read its current value on the right of := and its next value on the left, then drive it from a testbench with poke, peek, and step and watch which outputs move before an edge and which only after.

In this chapter

  • Create registers with Reg, RegInit and RegNext and read the current value on the right of := and the next value on the left
  • Drive a design from chiseltest with poke, peek and step and predict which outputs change before an edge and which only after it
  • Write a counter as a register and one connection
  • Diagnose a countdown that never finishes from a missing poke or step
Concepts in this chapter
  • Reg, RegInit, step
Prerequisite concepts

1. A register has a current value and a next value

Everything so far was combinational. A register remembers: it holds a value until the next rising edge of the clock, and only then takes a new one. In Chisel, RegInit(0.U(8.W)) creates an 8-bit register whose value after reset is 0. Every Module has an implicit clock and reset that you never write in the port list; registers use both automatically. This course assumes one clock and positive-edge registers throughout.

Form

val acc = RegInit(0.U(8.W))
acc := acc + io.in
PositionWhich valueMeaning
acc on the right of :=, or anywhere in an expressioncurrent valuewhat the register holds during this cycle
acc on the left of acc := ...next valuewhat it will hold after the next rising edge

The logic around the register only ever sees the current value. A connection to a register does not change what anything reads in the same cycle; it decides what the register captures at the next edge. The val acc is a Scala name for the register node; the state is what the register holds in simulation, and that changes only at edges.

Caution. A reset value with a stated width fixes the register's width. With RegInit(0.U) and no width, the width is inferred from what is connected to the register. For a counter whose only connection is itself plus a 1-bit literal, inference settles on 1 bit and the counter runs 0, 1, 0, 1. Give reset values a width.

2. Stepping cycles in chiseltest

In a testbench, three calls make up one cycle.

CallEffect
pokeChanges an input immediately
peekReads an output computed from the current register values and the inputs just poked
clock.step()Performs one rising edge; afterwards every register holds what its next value was

Note. chiseltest's test(...) pulses reset once before running the test body, so at the first peek every register with a reset value already holds it.

3. The register constructors

ConstructorReset valueTypical use
RegNext(x)noneA copy of x delayed by one cycle; type and width inferred from x
RegNext(x, init)initThe same delay, with a defined value after reset
RegInit(init)initState updated by explicit :=, starting from init
Reg(t)noneState of type t with no reset; updated by :=

3.1 RegNext: a one-cycle delay

RegNext creates a register whose next value is its argument and returns the register's current value.

Syntax

val name = RegNext(nextValue)
val name = RegNext(nextValue, resetValue)
val d1 = RegNext(io.in, 0.U)   // in, one cycle ago
val d2 = RegNext(d1, 0.U)      // in, two cycles ago

Poke 5, 6, 7, 8 into io.in on successive cycles and d1 reads 0, 5, 6, 7 while d2 reads 0, 0, 5, 6. Each RegNext in the chain adds one cycle.

3.2 RegInit: state with a reset value

RegInit creates a register with a reset value but no next value yet. You connect the next value afterwards with :=.

Syntax

val name = RegInit(resetValue)
name := nextValue
val cnt = RegInit(0.U(4.W))
cnt := cnt + 1.U
CodeMeaning
RegInit(0.U(4.W))A 4-bit register that resets to 0
cnt := cnt + 1.UIts next value is its current value plus one

cnt counts 0 to 15, and because 16 does not fit in 4 bits, it wraps to 0.

Caution. A reset value with a stated width fixes the register's width. Write RegInit(0.U) with no width and the register's width is left to inference from what is connected to it; with cnt + 1.U as the only connection it settles on 1 bit, so the counter above would run 0, 1, 0, 1. Always give a register's reset value a width.

3.3 Reg: state with no reset value

Reg takes only a type and creates a register with no reset value.

Syntax

val name = Reg(type)
val r = Reg(UInt(8.W))
when(io.en) {
  r := io.in
}

In cycles where en is high, r captures in; in the others it receives no connection and keeps its value.

Note. A register that receives no connection in a cycle keeps its current value. Unlike a wire, it never needs a default, which is why connecting it only inside when does not trigger the unconnected-wire check.

4. poke sets, peek reads now, step advances one edge

A chiseltest testbench is built from four calls.

CallEffect
dut.io.x.poke(v)drives an input with v at once; the value stays until the next poke of that port
dut.io.y.peek()reads a port now, from the current register values and the inputs just poked
dut.clock.step()performs one rising edge; afterwards every register holds its next value
dut.io.y.expect(v)reads now and fails the run if the value differs from v

Combinational outputs follow a poke immediately. Register outputs change only during a step. Before your code starts, test(...) holds reset high for one cycle and releases it, so every RegInit already holds its reset value at the first peek.

Acc.scala keeps a running total. acc is the register; io.next is combinational, the register plus the current input; io.sum shows the register. acc takes io.next only on edges where en is high.

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 =>
    // chiseltest has already pulsed reset once before this line runs.
    println(s"START sum=${dut.io.sum.peek().litValue} reset=${dut.reset.peek().litValue}")
    dut.io.en.poke(true.B)
    dut.io.in.poke(3.U)
    println(s"POKE next=${dut.io.next.peek().litValue} sum=${dut.io.sum.peek().litValue}")
    dut.clock.step()
    println(s"STEP next=${dut.io.next.peek().litValue} sum=${dut.io.sum.peek().litValue}")
    // A poked value stays until the testbench pokes again.
    dut.io.en.poke(false.B)
    dut.clock.step(2)
    dut.io.sum.expect(3.U)
    println(s"HOLD sum=${dut.io.sum.peek().litValue}")
    dut.reset.poke(true.B)
    dut.clock.step()
    dut.reset.poke(false.B)
    dut.io.sum.expect(0.U)
    println("RESET sum=0 checked")
  }
}

Predict the POKE and STEP lines. On the POKE line, en and in = 3 have been poked and no edge has happened yet.

Check

On the POKE line, what are next and sum?

Your answer

Check

After one step(), what are next and sum?

Your answer

Press Run. The HOLD line follows two edges with en low, and the final lines drive reset like any input: high for one edge, then low again.

5. The testbench API in detail

5.1 test and the dut handle

A testbench starts by handing a module to test. chiseltest elaborates the module, starts a simulator, and calls your code with a handle to the running circuit. By convention the handle is named dut, for "device under test".

Syntax

test(new ModuleName) { dut =>
  // drive and check dut's ports
}
test(new ModuleName, annotations, "name") { dut => ... }
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Inverter) { dut =>
    dut.io.in.poke(true.B)
    dut.io.out.expect(false.B)
  }
}
PartMeaning
import chiseltest._Brings poke, peek, expect, and step into scope
chiseltest.RawTester.testThe form of test that works outside the ScalaTest framework, so a plain object Main extends App can call it
new InverterThe module to elaborate and simulate
dut =>Your code receives the running circuit as dut; dut.io.in is its io.in port
annotationsOptional options, such as waveform output in the next chapter
"name"Optional working directory name

chiseltest is a testing library, not a simulator. It passes the circuit to a simulator backend. This lab uses the default backend, treadle, a FIRRTL interpreter that runs inside the same JVM as your test, so no Verilog is generated and no external simulator is launched.

5.2 poke: driving an input

poke sets an input port to a value. The value is a Chisel literal of the port's type. It takes effect immediately and stays until you poke the same port again.

Syntax

dut.io.port.poke(value)
dut.io.en.poke(true.B)      // Bool
dut.io.in.poke(3.U)         // UInt
dut.io.offset.poke(-3.S)    // SInt
dut.reset.poke(true.B)      // reset is an input too
RuleWhat happens otherwise
Poke only inputsPoking an output throws chiseltest.UnpokeableException: Can only poke inputs
The value must fit the portPoking 300.U into an 8-bit port throws a ChiselException saying the value does not fit the range 0 ... 255

5.3 peek: reading a port

peek returns the current value of any port as a Chisel literal. To use the value in Scala, convert it.

Syntax

dut.io.port.peek()                 // a Chisel literal, such as UInt<8>(3)
dut.io.port.peek().litValue        // BigInt
dut.io.flag.peek().litToBoolean    // Boolean, for Bool ports
val sum = dut.io.sum.peek().litValue
println(s"sum is $sum")
if (dut.io.busy.peek().litToBoolean) dut.clock.step()
CallReturnsTypical use
peek()A Chisel literalPassing to other chiseltest calls
.litValueBigIntPrinting, arithmetic, comparing with Scala numbers
.litToBooleanBooleanif and while conditions

Caution. Compare a peeked value with a Scala number only after .litValue. dut.io.sum.peek() == 3 compiles but is always false, because it compares a Chisel literal with an Int.

5.4 expect: checking a port

expect compares a port with a value and fails the test if they differ. An optional second argument adds your own message to the failure.

Syntax

dut.io.port.expect(value)
dut.io.port.expect(value, "message")
dut.io.sum.expect(7.U, "sum after 3 + 4")

When the check fails, chiseltest throws a FailedExpectException whose message has a fixed shape:

Part of the messageMeaning
In step NNumber of clock steps your code had taken; the reset cycle is not counted
io_sumThe port's name in the lowered circuit; dots become underscores
=7 (0x7)The value the port actually carried
expected=8 (0x8)The value you passed to expect
: messageYour second argument, if you gave one

The exception ends the test unless you catch it, so lines after a failing expect do not run.

5.5 clock.step: advancing time

clock.step advances the clock by one or more rising edges. At each rising edge, every register takes its next value at once.

Syntax

dut.clock.step()     // one rising edge
dut.clock.step(n)    // n rising edges

Pokes, peeks, and expects all happen between clock edges. Combinational logic settles as soon as an input changes, so a peek right after a poke already sees the new combinational result. Register outputs change only during a step.

Kind of outputResponds to a pokeResponds to a step
Combinational, such as a + bImmediately, before any stepAlso when registers it reads change
Register, such as RegInit(...)NoYes, at each rising edge

6. A counter is two lines

Blinker.scala has two Scala functions. def wrapCounter(max: Int): UInt = { ... } declares a function that takes a Scala Int and returns a hardware UInt; its body creates a register and returns it, so each call builds one new register. log2Ceil(max + 1) computes, as a Scala number, how many bits hold 0 to max. The Mux sends the counter back to 0 at max; without it the register would wrap at a power of two. toggle(p) flips a Bool register in every cycle where p is true.

Blinker.scala
import chisel3._
import chisel3.util.log2Ceil

class Blinker(n: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(4.W))
    val pulse = Output(Bool())
    val wave  = Output(Bool())
  })

  // Counts 0 to max, then wraps back to 0.
  def wrapCounter(max: Int): UInt = {
    val x = RegInit(0.U(log2Ceil(max + 1).W))
    x := Mux(x === max.U, 0.U, x + 1.U)
    x
  }

  // Flips its state in every cycle where p is true.
  def toggle(p: Bool): Bool = {
    val x = RegInit(false.B)
    x := Mux(p, !x, x)
    x
  }

  val count = wrapCounter(n - 1)
  val pulse = count === 0.U
  io.count := count
  io.pulse := pulse
  io.wave  := toggle(pulse)
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Blinker(3)) { dut =>
    for (cycle <- 0 until 9) {
      val c = dut.io.count.peek().litValue
      val p = dut.io.pulse.peek().litValue
      val w = dut.io.wave.peek().litValue
      println(s"cycle=$cycle count=$c pulse=$p wave=$w")
      dut.clock.step()
    }
  }
}

The testbench prints count, pulse, and wave before each of nine edges. pulse is combinational, count === 0.U. wave is a register that captures its flipped value at the edge that ends a pulse cycle.

Check

pulse is high in cycle 0. What is wave in cycle 0, and in cycle 1?

Your answer

Run and read the three columns together: pulse every three cycles, wave changing one cycle after each pulse, so its period is six.

6.1 Scala functions that return registers

A Scala function that creates a register and returns it gives you a reusable block with state. Each call creates one new register, and the caller receives its current value.

Syntax

def functionName(arg: Type): ReturnType = {
  val r = RegInit(resetValue)
  r := nextValue
  r
}

6.2 Sizing a counter with log2Ceil

chisel3.util.log2Ceil(n) computes, as a Scala integer, how many bits it takes to tell n values apart. A counter running 0 to max needs log2Ceil(max + 1) bits.

maxlog2Ceil(max + 1)Range it can hold
220–3
430–7
730–7

7. Delete one poke

In the Acc testbench, delete the line dut.io.en.poke(false.B) and run again.

Check

Without en being poked low, what happens at the expect before the HOLD line?

Your answer

8. Apply it: a countdown that never finishes

The graded Practice problem below is a countdown whose done never rises; a wait loop in its testbench steps until chiseltest's idle-cycle timeout stops it. Read the WAIT lines it prints, find the value at which left stops changing, and compare the condition guarding the decrement with the rule "count down until 0".

9. Common mistakes

9.1 Expecting a register to show its new value in the same cycle

// Wrong expectation: out equals in this cycle
val r = RegInit(0.U(8.W))
r := io.in
io.out := r          // shows the previous value this cycle

// Right: if you need the same cycle, skip the register
io.out := io.in

r := io.in only decides what is captured at the next edge. Poke 9 into in and out still shows the old value until one edge has passed. When an output reacts one cycle later than you expected, count the registers its signal passes through.

9.2 Giving a reset value no width

// Wrong: width inference settles on 1 bit
val cnt = RegInit(0.U)
cnt := cnt + 1.U     // 0, 1, 0, 1, ...

// Right
val cnt = RegInit(0.U(8.W))
cnt := cnt + 1.U     // 0, 1, 2, 3, ...

0.U is a 1-bit literal, and cnt + 1.U is never wider than cnt, so inference makes the register 1 bit wide. Even with an 8-bit output port, the counter only ever shows 0 and 1.

9.3 Expecting a counter to stop at max without a wrap

// Wrong: a 2-bit register counts 0 to 3
val x = RegInit(0.U(log2Ceil(3).W))
x := x + 1.U

// Right: return to 0 at max
val x = RegInit(0.U(log2Ceil(3).W))
x := Mux(x === 2.U, 0.U, x + 1.U)

A register's width only limits what it can hold. The Mux is the only thing that sends the counter back to 0 at max; without it, the counter wraps at a power of two.

9.4 Comparing a peeked literal with a Scala number

// Wrong: always false
if (dut.io.sum.peek() == 3) println("three")

// Right
if (dut.io.sum.peek().litValue == 3) println("three")

peek() returns a Chisel literal such as UInt<8>(3), which is never equal to the Int 3. Convert with .litValue or .litToBoolean first, or use expect when you know the right value.

9.5 Checking a register before stepping

dut.io.in.poke(3.U)
// Wrong: the register has not seen an edge yet
dut.io.sum.expect(3.U)

// Right
dut.io.in.poke(3.U)
dut.clock.step()
dut.io.sum.expect(3.U)

Registers change only during step. A combinational output may be checked right after a poke; a register output needs at least one step.

9.6 Forgetting that pokes persist

dut.io.en.poke(true.B)
dut.clock.step()
// Wrong: en is still high, so these steps keep accumulating
dut.clock.step(2)

// Right: turn en off explicitly
dut.io.en.poke(false.B)
dut.clock.step(2)

An input keeps its last poked value until you poke it again. A value that changes "on its own" in a later step usually comes from an earlier poke that is still in effect.

9.7 Poking an output

// Wrong: throws UnpokeableException
dut.io.sum.poke(1.U)

// Right: drive the inputs that determine sum, then check it
dut.io.in.poke(1.U)
dut.clock.step()
dut.io.sum.expect(1.U)

Outputs are driven by the circuit. To get a particular output value, drive the inputs and let the design produce it.

10. Summary

ElementMeaning
RegInit(v)a register with reset value v; a stated width on v fixes the register's width
right of :=current value, read this cycle
left of :=next value, captured at the next rising edge
poke, peek, expectbetween edges: drive, read now, check now
clock.step()one rising edge; registers move only here
def f(x: Int): UInt = { ... }a Scala function; each call builds its hardware again

Reg without a reset value, RegNext, and reset types are in the Sequential Circuits chapter; the testbench API and the idle-cycle timeout are in the Testbench chapter. The next lesson updates a register under a condition and looks at what reset does and does not touch.

11. Check your understanding

  1. In x := x + 1.U, which x is read in the current cycle, and which one changes after the edge?

    Show answer

    The x on the right is the current value read this cycle. The x on the left is the next value, captured at the next rising edge.

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

  3. Why is the square wave's period twice the counter's, and why does wave change one cycle after each pulse?

    Show answer

    The toggle flips once per pulse, so returning to the same level takes two pulses. And the toggle register captures its flipped value at the edge that ends the pulse cycle, so the change shows up in the following cycle.

  4. Why does a counter built with RegInit(0.U) only ever show 0 and 1?

    Show answer

    The reset value 0.U is 1 bit wide, and x + 1.U is never wider than x. Width inference therefore makes the register 1 bit, and the value after 1 is truncated to 0.

  5. If the reset were asynchronous, which registers should change on the reset-before-edge line, and why can the shipped testbench not show that directly?

    Show answer

    An asynchronous reset does not wait for an edge, so the registers with a reset value, withInit and nextInit, should jump to 5 and 0 at once. But forcing an asynchronous reset with RequireAsyncReset makes dut.reset.poke(true.B) throw LiteralTypeException, so the testbench never reaches that line.

Checkpoint

Run the example and compare it with your prediction