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| Position | Which value | Meaning |
|---|---|---|
acc on the right of :=, or anywhere in an expression | current value | what the register holds during this cycle |
acc on the left of acc := ... | next value | what 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.
| Call | Effect |
|---|---|
poke | Changes an input immediately |
peek | Reads 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
| Constructor | Reset value | Typical use |
|---|---|---|
RegNext(x) | none | A copy of x delayed by one cycle; type and width inferred from x |
RegNext(x, init) | init | The same delay, with a defined value after reset |
RegInit(init) | init | State updated by explicit :=, starting from init |
Reg(t) | none | State 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 agoPoke 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 := nextValueval cnt = RegInit(0.U(4.W))
cnt := cnt + 1.U| Code | Meaning |
|---|---|
RegInit(0.U(4.W)) | A 4-bit register that resets to 0 |
cnt := cnt + 1.U | Its 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.
| Call | Effect |
|---|---|
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.
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 =>
// 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?
Check
After one step(), what are next and sum?
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)
}
}| Part | Meaning |
|---|---|
import chiseltest._ | Brings poke, peek, expect, and step into scope |
chiseltest.RawTester.test | The form of test that works outside the ScalaTest framework, so a plain object Main extends App can call it |
new Inverter | The module to elaborate and simulate |
dut => | Your code receives the running circuit as dut; dut.io.in is its io.in port |
annotations | Optional 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| Rule | What happens otherwise |
|---|---|
| Poke only inputs | Poking an output throws chiseltest.UnpokeableException: Can only poke inputs |
| The value must fit the port | Poking 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 portsval sum = dut.io.sum.peek().litValue
println(s"sum is $sum")
if (dut.io.busy.peek().litToBoolean) dut.clock.step()| Call | Returns | Typical use |
|---|---|---|
peek() | A Chisel literal | Passing to other chiseltest calls |
.litValue | BigInt | Printing, arithmetic, comparing with Scala numbers |
.litToBoolean | Boolean | if 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 message | Meaning |
|---|---|
In step N | Number of clock steps your code had taken; the reset cycle is not counted |
io_sum | The 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 |
: message | Your 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 edgesPokes, 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 output | Responds to a poke | Responds to a step |
|---|---|---|
Combinational, such as a + b | Immediately, before any step | Also when registers it reads change |
Register, such as RegInit(...) | No | Yes, 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.
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)
}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?
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.
max | log2Ceil(max + 1) | Range it can hold |
|---|---|---|
| 2 | 2 | 0–3 |
| 4 | 3 | 0–7 |
| 7 | 3 | 0–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?
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.inr := 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
| Element | Meaning |
|---|---|
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, expect | between 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
-
In
x := x + 1.U, whichxis read in the current cycle, and which one changes after the edge?Show answer
The
xon the right is the current value read this cycle. Thexon the left is the next value, captured at the next rising edge. -
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 is the square wave's period twice the counter's, and why does
wavechange 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.
-
Why does a counter built with
RegInit(0.U)only ever show 0 and 1?Show answer
The reset value
0.Uis 1 bit wide, andx + 1.Uis never wider thanx. Width inference therefore makes the register 1 bit, and the value after 1 is truncated to 0. -
If the reset were asynchronous, which registers should change on the
reset-before-edgeline, 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,
withInitandnextInit, should jump to 5 and 0 at once. But forcing an asynchronous reset withRequireAsyncResetmakesdut.reset.poke(true.B)throwLiteralTypeException, so the testbench never reaches that line.