val nextState = (state << 1) | io.in state := nextState io.out := state
Prev: Control Flow
Next: FIR Filter
You can't write any meaningful digital logic without state. You can't write any meaningful digital logic without state. You can't write any meaningful digital logic....
Get it? Because without storing intermediate results, you can't get anywhere.
Ok, that bad joke aside, this module will describe how to express common sequential patterns in Chisel. By the end of the module, you should be able to implement and test a shift register in Chisel.
It's important to emphasize that this section will probably not dramatically impress you. Chisel's power is not in new sequential logic patterns, but in the parameterization of a design. Before we demonstrate that capability, we have to learn what these sequential patterns are. Thus, this section will show you that Chisel can do pretty much what Verilog can do - you just need to learn the Chisel syntax.
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
The basic stateful element in Chisel is the register, denoted Reg
.
A Reg
holds its output value until the rising edge of its clock, at which time it takes on the value of its input.
By default, every Chisel Module
has an implicit clock that is used by every register in the design.
This saves you from always specifying the same clock all over your code.
Example: Using a Register
The following code block implements a module that takes the input, adds 1 to it, and connects it as the input of a register.
Note: The implicit clock can be overridden for multi-clock designs. See the appendix for an example.
class RegisterModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
val register = Reg(UInt(12.W))
register := io.in + 1.U
io.out := register
}
class RegisterModuleTester(c: RegisterModule) extends PeekPokeTester(c) {
for (i <- 0 until 100) {
poke(c.io.in, i)
step(1)
expect(c.io.out, i+1)
}
}
assert(chisel3.iotesters.Driver(() => new RegisterModule) { c => new RegisterModuleTester(c) })
println("SUCCESS!!")
The register is created by calling Reg(tpe)
, where tpe
is a variable that encodes the type of register we want.
In this example, tpe
is a 12-bit UInt
.
Look at what the tester above is doing.
Between calls to poke()
and expect
, there is a call to step(1)
.
This tells the test harness to tick the clock once, which will cause the register to pass its input to its output.
Calling step(n)
will tick the clock n
times.
The astute observer will notice that previous testers testing combinational logic did not call step()
. This is because calling poke()
on an input immediately propagates the updated values through combinational logic. Calling step()
is only needed to update state elements in sequential logic.
The code block below will show the verilog generated by RegisterModule
.
Note:
register
shows up as reg [11:0]
, as expected
ifdef Randomize` that initialized the register to some random variable before simulation startsregister
is updated on posedge clock
println(getVerilog(new RegisterModule))
One important note is that Chisel distinguishes between types (like UInt
) and hardware nodes (like the literal 2.U
, or the output of myReg
). While
val myReg = Reg(UInt(2.W))
is legal because a Reg needs a data type as a model,
val myReg = Reg(2.U)
is an error because 2.U
is already a hardware node and can't be used as a model.
Example: RegNext
Chisel has a convenience register object for registers with simple input connections. The previous Module
can be shortened to the following Module
. Notice how we didn't need to specify the register bitwidth this time. It gets inferred from the register's output connection, in this case io.out
.
class RegNextModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
// register bitwidth is inferred from io.out
io.out := RegNext(io.in + 1.U)
}
class RegNextModuleTester(c: RegNextModule) extends PeekPokeTester(c) {
for (i <- 0 until 100) {
poke(c.io.in, i)
step(1)
expect(c.io.out, i+1)
}
}
assert(chisel3.iotesters.Driver(() => new RegNextModule) { c => new RegNextModuleTester(c) })
println("SUCCESS!!")
The Verilog looks almost the same as before, though the register name is generated instead of explicity defined.
println(getVerilog(new RegNextModule))
RegInit
¶The register in RegisterModule
was initialized to random data for simulation.
Unless otherwised specified, registers do not have a reset value (or a reset).
The way to create a register that resets to a given value is with RegInit
.
For instance, a 12-bit register initialized to zero can be created with the following. Both versions below are valid and do the same thing:
val myReg = RegInit(UInt(12.W), 0.U)
val myReg = RegInit(0.U(12.W))
The first version has two arguments. The first argument is a type node that specified the datatype and its width. The second argument is a hardware node that specified the reset value, in this case 0.
The second version has one argument.
It is a hardware node that specifies the reset value, but normally 0.U
.
Example: Initialized Register
The following demonstrates using RegInit()
, initialized to zero.
class RegInitModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
val register = RegInit(0.U(12.W))
register := io.in + 1.U
io.out := register
}
println(getVerilog(new RegInitModule))
Note that the generated verilog now has a block that checks if (reset)
to reset the register to 0.
Also note that this is inside the always @(posedge clock)
block.
Chisel's implicit reset is active high and synchronous.
The register is still initialized to random junk before reset is called.
The PeekPokeTesters
always call reset before running your test, but you can manually call reset as well using the reset(n)
function, where reset is high for n
cycles.
Registers are very similar to wires in terms of control flow.
They have last connect semantics and can be assigned to conditionally with when
, elsewhen
, and otherwise
.
Example: Register Control Flow
The following example finds the maximum value in a sequence of inputs using conditional register assignments.
class FindMax extends Module {
val io = IO(new Bundle {
val in = Input(UInt(10.W))
val max = Output(UInt(10.W))
})
val max = RegInit(0.U(10.W))
when (io.in > max) {
max := io.in
}
io.max := max
}
assert(chisel3.iotesters.Driver(() => new FindMax) {
c => new PeekPokeTester(c) {
expect(c.io.max, 0)
poke(c.io.in, 1)
step(1)
expect(c.io.max, 1)
poke(c.io.in, 3)
step(1)
expect(c.io.max, 3)
poke(c.io.in, 2)
step(1)
expect(c.io.max, 3)
poke(c.io.in, 24)
step(1)
expect(c.io.max, 24)
}
})
println("SUCCESS!!")
Operations called on a register are performed on the output of the register, and the kind of operations depend on the register's type. That means that you can write
val reg: UInt = Reg(UInt(4.W))```
which means the value `reg` is of type `UInt` and you can do things you can normally do with `UInt`s, like `+`, `-`, etc.
You aren't restricted to using `UInt`s with registers, you can use any subclass of the base type `chisel3.Data`. This includes `SInt` for signed integers and a lot of other things.
<span style="color:blue">**Example: Comb Filter**</span><br>
The following example shows a comb filter.
class Comb extends Module {
val io = IO(new Bundle {
val in = Input(SInt(12.W))
val out = Output(SInt(12.W))
})
val delay: SInt = Reg(SInt(12.W))
delay := io.in
io.out := io.in - delay
}
println(getVerilog(new Comb))
Exercise: Shift Register
Given your new-found registering knowledge, build a module that implements a shift register for a LFSR. Specifically:
Cat
may come in handy.b0001
.out(0) := in
will not work.A basic Module skeleton, testvector, and Driver invocation is provided below. The first register has been provided for you.
class MyShiftRegister(val init: Int = 1) extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out = Output(UInt(4.W))
})
val state = RegInit(UInt(4.W), init.U)
???
}
class MyShiftRegisterTester(c: MyShiftRegister) extends PeekPokeTester(c) {
var state = c.init
for (i <- 0 until 10) {
// poke in LSB of i (i % 2)
poke(c.io.in, i % 2)
// update expected state
state = ((state * 2) + (i % 2)) & 0xf
step(1)
expect(c.io.out, state)
}
}
assert(chisel3.iotesters.Driver(() => new MyShiftRegister()) {
c => new MyShiftRegisterTester(c)
})
println("SUCCESS!!")
val nextState = (state << 1) | io.in state := nextState io.out := state
Exercise: Parameterized Shift Register (Optional)
Write a shift register that is parameterized by its delay (n
), its initial value (init
), and also has an enable input signal (en
).
// n is the output width (number of delays - 1)
// init state to init
class MyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(Bool())
val out = Output(UInt(n.W))
})
val state = RegInit(init.U(n.W))
???
}
class MyOptionalShiftRegisterTester(c: MyOptionalShiftRegister) extends PeekPokeTester(c) {
val inSeq = Seq(0, 1, 1, 1, 0, 1, 1, 0, 0, 1)
var state = c.init
var i = 0
poke(c.io.en, 1)
while (i < 10 * c.n) {
// poke in repeated inSeq
val toPoke = inSeq(i % inSeq.length)
poke(c.io.in, toPoke)
// update expected state
state = ((state * 2) + toPoke) & BigInt("1"*c.n, 2)
step(1)
expect(c.io.out, state)
i += 1
}
}
// test different depths
for (i <- Seq(3, 4, 8, 24, 65)) {
println(s"Testing n=$i")
assert(chisel3.iotesters.Driver(() => new MyOptionalShiftRegister(n = i)) {
c => new MyOptionalShiftRegisterTester(c)
})
}
println("SUCCESS!!")
val nextState = (state << 1) | io.in when (io.en) { state := nextState } io.out := state
Chisel modules have a default clock and reset that are implicitly used by every register created inside them. There are times where you want to be able to override this default behavior; perhaps you have a black box that generates a clock or reset signal, or you have a multi-clock design.
Chisel provides constructs for dealing with these cases.
Clocks and resets can be overridden separately or together with withClock() {}
, withReset() {}
, and withClockAndReset() {}
.
The following code blocks will give examples of using these functions.
One thing to be aware of is that reset
(as of this tutorial's writing) is always synchronous and of type Bool
.
Clocks have their own type in Chisel (Clock
) and should be declared as such.
Bool
s can be converted to Clock
s by calling asClock()
on them, but you should be careful that you aren't doing something silly.
Also note that chisel-testers
do not currently have complete support for multi-clock designs.
Example: Multi-Clock Module
A module with multiple clocks and reset signals.
// we need to import multi-clock features
import chisel3.experimental.{withClock, withReset, withClockAndReset}
class ClockExamples extends Module {
val io = IO(new Bundle {
val in = Input(UInt(10.W))
val alternateReset = Input(Bool())
val alternateClock = Input(Clock())
val outImplicit = Output(UInt())
val outAlternateReset = Output(UInt())
val outAlternateClock = Output(UInt())
val outAlternateBoth = Output(UInt())
})
val imp = RegInit(0.U(10.W))
imp := io.in
io.outImplicit := imp
withReset(io.alternateReset) {
// everything in this scope with have alternateReset as the reset
val altRst = RegInit(0.U(10.W))
altRst := io.in
io.outAlternateReset := altRst
}
withClock(io.alternateClock) {
val altClk = RegInit(0.U(10.W))
altClk := io.in
io.outAlternateClock := altClk
}
withClockAndReset(io.alternateClock, io.alternateReset) {
val alt = RegInit(0.U(10.W))
alt := io.in
io.outAlternateBoth := alt
}
}
println(getVerilog(new ClockExamples))
Great job completing this section!! You've now learned how to create registers and write sequential logic in Chisel, which means you have enough basic building blocks to write real circuits.
The next section will combine everything we've learned into one example! If you need a little more encouragement, just remember these words from an expert Chisel user: