1. A register updated under when holds in every other cycle
Outside reset, put a register's connection inside a when and it captures a new value only at edges
where the condition holds. In every other cycle the register receives no connection and keeps its current
value. That is the enable from the fundamentals, and it is the one place where a missing path is not
an error: a wire with no driver on some path is rejected before simulation, but a register simply
holds, so it never needs a default or an .otherwise. The unconnected-wire check that
Mux, when, Wire, and a default for every path applied
to wires and outputs is not triggered by a register connected only inside when.
Form
val r = RegInit(0.U(4.W))
when (io.en) {
r := io.in
}| Cycle | en | What the edge does to r |
|---|---|---|
| any | 1 | captures io.in |
| any | 0 | keeps its current value |
2. Priority is written as source order
When two connections to a register could both apply in one cycle, the rules of
the when chapter decide: a when/.elsewhen chain
uses its first true branch, and between separate statements the later connection wins. A load that
must beat counting is written first in a chain; a clear that must beat everything is written either
first in the chain or as the last separate when.
Form
when (io.load) {
count := io.cycles
} .elsewhen (count =/= 0.U) {
count := count - 1.U
}With load and a nonzero count both true, the register loads; the decrement branch is never reached.
With both false, nothing connects and the register holds.
3. Reset changes only registers that declare a reset value
Reset is not a global clear. It belongs to each register's declaration, and it applies only to registers that declare a reset value.
| Created with | Reset value | While reset is high, at the edge |
|---|---|---|
RegInit(v) | v | captures v instead of its next value |
RegNext(x, v) | v | the same |
Reg(t) | none | has no reset connection; keeps following its next-value logic |
RegNext(x) | none | the same |
A register without a reset value also has no defined starting value. The treadle simulator happens to start such registers at 0, but hardware promises nothing. Control state such as flags, valid bits, and counters needs a reset value because correctness depends on where it starts.
The implicit reset of a top-level module is synchronous: it takes effect at a clock edge, not the
moment it goes high. chiseltest pulses it for one cycle before your code runs, so at the first peek
every register with a reset value already holds it. A testbench can also drive it like an ordinary
input with dut.reset.poke(true.B), and then nothing changes until a step.
4. Four registers through one reset
ResetDemo holds four registers created the four ways of section 3.
| Register | Created with | Next value |
|---|---|---|
withInit | RegInit(5.U(4.W)) | in while en is high |
noInit | Reg(UInt(4.W)) | in while en is high |
next | RegNext(io.in) | in, every cycle |
nextInit | RegNext(io.in, 0.U(4.W)) | in, every cycle |
import chisel3._
class ResetDemo extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(UInt(4.W))
val withInit = Output(UInt(4.W))
val noInit = Output(UInt(4.W))
val next = Output(UInt(4.W))
val nextInit = Output(UInt(4.W))
})
// Reset loads 5 into this register.
val withInit = RegInit(5.U(4.W))
// No reset value: reset does not touch this register.
val noInit = Reg(UInt(4.W))
when(io.en) {
withInit := io.in
noInit := io.in
}
io.withInit := withInit
io.noInit := noInit
io.next := RegNext(io.in)
io.nextInit := RegNext(io.in, 0.U(4.W))
}The testbench first prints only withInit, so it never depends on the undefined start of the
registers without a reset value. It then loads 9 into every register, sets en low and in to 6,
pokes reset high, prints before the edge, steps once, prints again, and finally releases reset and
steps once more.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new ResetDemo) { dut =>
def show(phase: String): Unit = {
val v = Seq(dut.io.withInit, dut.io.noInit, dut.io.next, dut.io.nextInit).map(_.peek().litValue)
println(s"$phase withInit=${v(0)} noInit=${v(1)} next=${v(2)} nextInit=${v(3)}")
}
println(s"start withInit=${dut.io.withInit.peek().litValue}")
// Load 9 into every register.
dut.io.en.poke(true.B)
dut.io.in.poke(9.U)
dut.clock.step()
show("loaded")
// Hold reset for one edge while in changes to 6 and en is low.
dut.io.en.poke(false.B)
dut.io.in.poke(6.U)
dut.reset.poke(true.B)
show("reset-before-edge")
dut.clock.step()
show("reset-after-edge")
dut.reset.poke(false.B)
dut.clock.step()
show("released")
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run. What does the start line show for withInit? Then predict the
reset-before-edge line, which is printed right after reset goes high and before any edge, and the
four values on the reset-after-edge line.
Check
reset has just been poked high and no edge has happened. What does the reset-before-edge line show?
Check
After the edge with reset high and en low, what does noInit show?
Check
On the same reset-after-edge line, what do next and nextInit show?
Check the result. Press Run and compare the five lines.
| Output line | What it shows |
|---|---|
start | 5, because chiseltest pulsed reset before the test body |
loaded | All four hold 9 after one edge with en high |
reset-before-edge | Still 9 everywhere: poking reset alone moves nothing |
reset-after-edge | withInit is its reset value 5; noInit holds 9 because en is low; next tracks in regardless of reset and shows 6; nextInit is its reset value 0 |
released | withInit and noInit hold because en is still low; nextInit resumes tracking in and shows 6 |
Nothing changed on the reset-before-edge line, which proves this module's reset is
synchronous: it takes effect only at a clock edge.
5. Take away one reset value
In the lab, change withInit from RegInit(5.U(4.W)) to Reg(UInt(4.W)) and predict the
reset-after-edge line before running again.
Check
With withInit created by Reg(UInt(4.W)), what does it show on the reset-after-edge line?
6. Reset types
6.1 Three reset types
Chisel has three reset types, all implementing the trait Reset.
| Type | Constructed with | Produces |
|---|---|---|
Bool | Bool() | Synchronous-reset registers |
AsyncReset | AsyncReset() | Asynchronous-reset registers |
Reset | Reset() | Abstract reset; the concrete type is inferred during FIRRTL compilation |
A Module's implicit reset is abstract, and inference resolves it from what drives it.
| What drives the abstract reset | Inferred type |
|---|---|
Only AsyncReset, abstract Reset, or DontCare | AsyncReset |
Both Bool and AsyncReset | Error |
| Anything else | Bool |
Nothing drives a top-level module's reset, so it defaults to Bool. That is why ResetDemo, used
as the top, has a synchronous reset, and its CHIRRTL declares input reset : UInt<1>. Abstract
reset lets one block be reused under either reset discipline without edits.
6.2 Forcing the reset type
Mixing a trait into a module fixes the type of its implicit reset.
Form
class ModuleName extends Module with RequireSyncReset // reset is Bool
class ModuleName extends Module with RequireAsyncReset // reset is AsyncResetChange the header to class ResetDemo extends Module with RequireAsyncReset and emit CHIRRTL: the
port becomes input reset : AsyncReset while the registers are declared exactly as before. The
testbench then stops at dut.reset.poke(true.B) with chiseltest.LiteralTypeException: don't know how to poke an IO[AsyncReset] with a Bool, because the port's type really changed.
6.3 Changing the reset for a region with withReset
Registers created inside a withReset block use the given signal as their reset instead of the
implicit one.
Form
withReset(resetSignal) {
// registers created here reset on resetSignal
}val r = withReset(io.clear) { RegInit(0.U(8.W)) }
r := r + 1.Uio.clear is a Bool, so r is a synchronous-reset register: raising clear does not zero r
immediately, only after the next edge.
Caution. The casts reset.asBool and reset.asAsyncReset are not checked by the compiler. Use
them only when you know the reset's actual discipline.
7. Common mistakes
7.1 Expecting reset to act the moment you poke it
// Wrong expectation: synchronous reset has not happened yet
dut.reset.poke(true.B)
dut.io.withInit.peek() // still the old value
// Right: step one edge, then look
dut.reset.poke(true.B)
dut.clock.step()
dut.io.withInit.peek() // the reset valueSynchronous reset acts only at a clock edge. To see its effect in a testbench, hold reset high
through one clock.step().
8. Apply it: a flag that survives reset
The graded Practice problem below has an error flag seen next to an error counter errors. After a
reset the counter is 0 and the flag is still on, and before the first error the flag has no defined
value. Compare the two declarations against the table in section 3; only one of them has to change.
Write the cycle-by-cycle table for the five printed phases before you press Run.
9. Summary
| Rule | Consequence |
|---|---|
a register connected only inside when | holds in every cycle where the condition is false; no default needed |
| priority | first true branch of a chain; between separate statements the later one wins |
RegInit(v), RegNext(x, v) | reset to v at an edge while reset is high |
Reg(t), RegNext(x) | untouched by reset; undefined start |
| the implicit reset at the top | synchronous: poke it, then step, to see its effect |
| reset type | the implicit reset is abstract and resolves to Bool at the top; RequireSyncReset and RequireAsyncReset force it; withReset changes it for a region |
10. Check your understanding
-
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.