Chisel logo

Module 2.4: Sequential Logic

Prev: Control Flow
Next: FIR Filter

Motivation

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.

Setup

In [ ]:
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
In [ ]:
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

Registers

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.

In [ ]:
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:

  • The module has an input for clock (and reset) that you didn't add- this is the implicit clock
  • The variable register shows up as reg [11:0], as expected
  • There is a block sectioned off by `ifdef Randomize that initialized the register to some random variable before simulation starts
  • register is updated on posedge clock
In [ ]:
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.

In [ ]:
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.

In [ ]:
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.

In [ ]:
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.


Control Flow

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.

In [ ]:
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!!")

Other Register Examples

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 UInts, like +, -, etc.

You aren't restricted to using UInts 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.

**Example: Comb Filter**
The following example shows a comb filter.

In [ ]:
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))

Exercises

**Exercise: Shift Register**
Given your new-found registering knowledge, build a module that implements a shift register for a LFSR. Specifically:

  • Each element is a single bit wide.
  • Has a 4-bit output signal.
  • Takes a single input bit, which is the next value into the shift register.
  • Outputs the parallel output of the shift register, with the most significant bit being the last element of the shift register and the least significant bit being the first element of the shift register. Cat may come in handy.
  • The output initializes at b0001.
  • Shifts each clock cycle (no enable signal).
  • Note in Chisel, subword assignment IS ILLEGAL; something like out(0) := in will not work.

shift register figure

A basic Module skeleton, testvector, and Driver invocation is provided below. The first register has been provided for you.

In [ ]:
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).

In [ ]:
// 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

Appendix: Explicit clock and reset

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. Bools can be converted to Clocks 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.

In [ ]:
// 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))

Wrap Up

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:

BobRoss


You're done!

Return to the top.