Chisel logo

Module 3.1: Generators: Parameters

Prev: FIR Filter
Next: Generators: Collections

Motivation

For Chisel modules to be code generators, there must be something that tells the generator how it should go about its job. In this section we discuss module parameterization, the various methodologies and Scala language features. The richness of the parameter passing implementation is directly proportional to the richness of the circuits generated. Parameters should provide useful default values, be easy to set, and protect against illegal or non-sensical values. For more complicated system it is very useful if they can be locally overriden in a way that does not inadvertantly affect other modules usages.

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}

Parameter Passing

Chisel provides powerful constructs for writing hardware generators. Generators are programs that take some circuit parameters and produce a circuit description. In this section, we'll start with discussing how Chisel generators get their parameters.

**Example: Parameterized Scala Object**
Every Chisel Modules is a Scala class just like any other. Recall that Scala classes can be parameterized like so:

In [ ]:
class ParameterizedScalaObject(param1: Int, param2: String) {
  println(s"I have parameters: param1 = $param1 and param2 = $param2")
}
val obj1 = new ParameterizedScalaObject(4,     "Hello")
val obj2 = new ParameterizedScalaObject(4 + 2, "World")

**Example: Parameterized Chisel Object**
Chisel modules can be parameterized the same way. The following module has parameters for the widths of all its inputs and outputs. Running the code block will print generated Verilog. Play with the parameters and check that the output changes to reflect new parameters.

In [ ]:
class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extends Module {
  require(in0Width >= 0)
  require(in1Width >= 0)
  require(sumWidth >= 0)
  val io = IO(new Bundle {
    val in0 = Input(UInt(in0Width.W))
    val in1 = Input(UInt(in1Width.W))
    val sum = Output(UInt(sumWidth.W))
  })
  // a +& b includes the carry, a + b does not
  io.sum := io.in0 +& io.in1
}

println(getVerilog(new ParameterizedWidthAdder(1, 4, 6)))

The above code block has some require(...) statements. These are pre-elaboration assertions, which are useful when your generator only works with certain parameterizations or when some parameterizations are mutually exclusive or nensensical. The above code block checks that widths are non-negative.

There is a separate construct for simulation-time assertions called assert(...).

Sorting with Parameterized Modules

The following code block is a parameterized sort similar to Sort4 from module 2.3. Unlike the previous example of an adder with parameterized width IOs, this example has a fixed IO. The parameter controls what hardware is generated inside the module. Sort4 **Example: Parameterized 4-Input Sort**
Unlike 2.3, this implementation is parameterized to be either a descending or an ascending sort.

In [ ]:
/** Sort4 sorts its 4 inputs to its 4 outputs */
class Sort4(ascending: Boolean) extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(16.W))
    val in1 = Input(UInt(16.W))
    val in2 = Input(UInt(16.W))
    val in3 = Input(UInt(16.W))
    val out0 = Output(UInt(16.W))
    val out1 = Output(UInt(16.W))
    val out2 = Output(UInt(16.W))
    val out3 = Output(UInt(16.W))
  })
    
  // this comparison funtion decides < or > based on the module's parameterization
  def comp(l: UInt, r: UInt): Bool = {
      if (ascending) {
        l < r
      } else {
        l > r
    }
  }

  val row10 = Wire(UInt(16.W))
  val row11 = Wire(UInt(16.W))
  val row12 = Wire(UInt(16.W))
  val row13 = Wire(UInt(16.W))

  when(comp(io.in0, io.in1)) {
    row10 := io.in0            // preserve first two elements
    row11 := io.in1
  }.otherwise {
    row10 := io.in1            // swap first two elements
    row11 := io.in0
  }

  when(comp(io.in2, io.in3)) {
    row12 := io.in2            // preserve last two elements
    row13 := io.in3
  }.otherwise {
    row12 := io.in3            // swap last two elements
    row13 := io.in2
  }

  val row21 = Wire(UInt(16.W))
  val row22 = Wire(UInt(16.W))

  when(comp(row11, row12)) {
    row21 := row11            // preserve middle 2 elements
    row22 := row12
  }.otherwise {
    row21 := row12            // swap middle two elements
    row22 := row11
  }

  val row31 = Wire(UInt(16.W))
  val row32 = Wire(UInt(16.W))
  when(comp(row10, row13)) {
    row31 := row10            // preserve middle 2 elements
    row32 := row13
  }.otherwise {
    row31 := row13            // swap middle two elements
    row32 := row10
  }

  when(comp(row10, row21)) {
    io.out0 := row31            // preserve first two elements
    io.out1 := row21
  }.otherwise {
    io.out0 := row21            // swap first two elements
    io.out1 := row31
  }

  when(comp(row22, row13)) {
    io.out2 := row22            // preserve first two elements
    io.out3 := row32
  }.otherwise {
    io.out2 := row32            // swap first two elements
    io.out3 := row22
  }
}

// verify the inputs are sorted
class Sort4AscendingTester(c: Sort4) extends PeekPokeTester(c) {
  poke(c.io.in0, 3)
  poke(c.io.in1, 6)
  poke(c.io.in2, 9)
  poke(c.io.in3, 12)
  expect(c.io.out0, 3)
  expect(c.io.out1, 6)
  expect(c.io.out2, 9)
  expect(c.io.out3, 12)

  poke(c.io.in0, 13)
  poke(c.io.in1, 4)
  poke(c.io.in2, 6)
  poke(c.io.in3, 1)
  expect(c.io.out0, 1)
  expect(c.io.out1, 4)
  expect(c.io.out2, 6)
  expect(c.io.out3, 13)

  poke(c.io.in0, 13)
  poke(c.io.in1, 6)
  poke(c.io.in2, 4)
  poke(c.io.in3, 1)
  expect(c.io.out0, 1)
  expect(c.io.out1, 4)
  expect(c.io.out2, 6)
  expect(c.io.out3, 13)

}
class Sort4DescendingTester(c: Sort4) extends PeekPokeTester(c) {
  poke(c.io.in0, 3)
  poke(c.io.in1, 6)
  poke(c.io.in2, 9)
  poke(c.io.in3, 12)
  expect(c.io.out0, 12)
  expect(c.io.out1, 9)
  expect(c.io.out2, 6)
  expect(c.io.out3, 3)

  poke(c.io.in0, 13)
  poke(c.io.in1, 4)
  poke(c.io.in2, 6)
  poke(c.io.in3, 1)
  expect(c.io.out0, 13)
  expect(c.io.out1, 6)
  expect(c.io.out2, 4)
  expect(c.io.out3, 1)
    
  poke(c.io.in0, 1)
  poke(c.io.in1, 6)
  poke(c.io.in2, 4)
  poke(c.io.in3, 13)
  expect(c.io.out0, 13)
  expect(c.io.out1, 6)
  expect(c.io.out2, 4)
  expect(c.io.out3, 1)

}

// Here are the testers
val worksAscending = iotesters.Driver(() => new Sort4(true)) { c => new Sort4AscendingTester(c) }
val worksDescending = iotesters.Driver(() => new Sort4(false)) { c => new Sort4DescendingTester(c) }
assert(worksAscending && worksDescending) // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

Option and Default Arguments

There are times when a function sometimes returns a value, and sometimes does not. Instead of erroring when it cannot return a value, Scala has a mechanism to encode this in the type system.

**Example: Erroneous Map Index Call**
In the following example, we have a map containing several key/value pairs. If we try to access a missing key/value pair, then we get a runtime error:

In [ ]:
val map = Map("a" -> 1)
val a = map("a")
println(a)
val b = map("b")
println(b)

**Example: Getting Uncertain Indices**
However, Map provides another way to access a key's value, through the get method. Using this returns a value of abstract class Option. Option has two subclasses, Some and None.

In [ ]:
val map = Map("a" -> 1)
val a = map.get("a")
println(a)
val b = map.get("b")
println(b)

As you'll see in later sections, Option is extremely important because it lets users use a match statement to check Scala types and values.

**Example: Get Or Else!**
Like Map, Option also has a get method, which errors if called on None. For these instances, we can provide a default using getOrElse.

In [ ]:
val some = Some(1)
val none = None
println(some.get)          // Returns 1
// println(none.get)       // Errors!
println(some.getOrElse(2)) // Returns 1
println(none.getOrElse(2)) // Returns 2

Options for Parameters with Defaults

When objects or functions have a lot of parameters, it can be tedious and error-prone to fully specify them all the time. In module 1, you were introduced to named arguments and parameter defaults. Sometimes, a parameter doesn't have a good default value. Option can be used with a default value of None in these situations.

**Example: Optional Reset**
The following shows a block that delays its input by one clock cycle. If resetValue = None, which is the default, the register will have no reset value and be initialized to garbage. This avoids the common but ugly case of using values outside the normal range to indicate "none", like using -1 as the reset value to indicate that this register is not reset.

In [ ]:
class DelayBy1(resetValue: Option[UInt] = None) extends Module {
    val io = IO(new Bundle {
        val in  = Input( UInt(16.W))
        val out = Output(UInt(16.W))
    })
    val reg = if (resetValue.isDefined) { // resetValue = Some(number)
        RegInit(resetValue.get)
    } else { //resetValue = None
        Reg(UInt())
    }
    reg := io.in
    io.out := reg
}

println(getVerilog(new DelayBy1))
println(getVerilog(new DelayBy1(Some(3.U))))

Match/Case Statements

The Scala matching concept is used throughout Chisel and needs to be part of any Chisel programmer's basic understanding. Scala provides the match operator which supports:

  • Simple testing for alternatives, something like a C switch statement
  • More complex testing of ad-hoc combinations of values
  • Taking actions based on the type of a variable when its type is unknown or underspecified, for example when
    • variable is taken from a heterogeneous list val mixedList = List(1, "string", false)
    • or variable is known to be a member of a super-class but not which specific sub-class it is.
  • Extraction of sub strings of a string that are specified with a regular expression

**Example: Value Matching**
The following example, depending on the value of the variable we match on, we execute a different case statement:

In [ ]:
// y is an integer variable defined somewhere else in the code
val y = 7
/// ...
val x = y match {
  case 0 => "zero" // One common syntax, preferred if fits in one line
  case 1 =>        // Another common syntax, preferred if does not fit in one line.
      "one"        // Note the code block continues until the next case
  case 2 => {      // Another syntax, but curly braces are not required
      "two"
  }
  case _ => "many" // _ is a wildcard that matches all values
}
println("y is " + x)

The match operator checks possible values and for each case returns a string. A couple of things to note:

  • Each code block that follows a => operator continues until it reaches either the ending brace of the match or the next case statement.
  • A match is searched in the order of the case statements, once a case statement has been matched, no other checks against other case statements are made.
  • The use of underscore as a wildcard, to handle any value not found.

**Example: Multiple Value Matching**
Also, multiple variables can be matched at the same time. Here's a simple example of a truth table implemented with a match statement and tuple of values:

In [ ]:
def animalType(biggerThanBreadBox: Boolean, meanAsCanBe: Boolean): String = {
  (biggerThanBreadBox, meanAsCanBe) match {
    case (true, true) => "wolverine"
    case (true, false) => "elephant"
    case (false, true) => "shrew"
    case (false, false) => "puppy"
  }
}
println(animalType(true, true))

**Example: Type Matching**
Scala is a strongly typed language, so the types of all objects are known during runtime. We can use match statements to use this type information to dictate control flow:

In [ ]:
val sequence = Seq("a", 1, 0.0)
sequence.foreach { x =>
  x match {
    case s: String => println(s"$x is a String")
    case s: Int    => println(s"$x is an Int")
    case s: Double => println(s"$x is a Double")
    case _ => println(s"$x is an unknown type!")
  }
}

**Example: Multiple Type Matching**
If you want to match on whether a value has one of many types, use the following syntax. Note that you must use an _ when matching.

In [ ]:
val sequence = Seq("a", 1, 0.0)
sequence.foreach { x =>
  x match {
    case _: Int | _: Double => println(s"$x is a number!")
    case _ => println(s"$x is an unknown type!")
  }
}

**Example: Type Matching and Erasure**
Type matching has some limitations. Because Scala runs on the JVM, and the JVM does not maintain polymorphic types, you cannot match on them at runtime (because they are all erased). Note that the following example always matches the first case statement, because the [String], [Int], and [Double] polymorphic types are erased, and the case statements are actually matching on just a Seq.

In [ ]:
val sequence = Seq(Seq("a"), Seq(1), Seq(0.0))
sequence.foreach { x =>
  x match {
    case s: Seq[String] => println(s"$x is a String")
    case s: Seq[Int]    => println(s"$x is an Int")
    case s: Seq[Double] => println(s"$x is a Double")
  }
}

Note that Scala compilers will usually give a warning if you implement code like the example above.

**Example: Optional Reset Matching**
The following code block shows the same DelayBy1 module with the match construct instead of if/else.

In [ ]:
class DelayBy1(resetValue: Option[UInt] = None) extends Module {
  val io = IO(new Bundle {
    val in  = Input( UInt(16.W))
    val out = Output(UInt(16.W))
  })
  val reg = resetValue match {
    case Some(r) => RegInit(r)
    case None    => Reg(UInt())
  }
  reg := io.in
  io.out := reg
}

println(getVerilog(new DelayBy1))
println(getVerilog(new DelayBy1(Some(3.U))))

IOs with Optional Fields

Sometimes we want IOs to be optionally included or excluded. Maybe there's some internal state that's nice to be able to look at for debugging, but you want to hide it when the generator is being used in a system. Maybe your generator has some inputs that don't need to be connected in every situation because there is a sensible default.

**Example: Optional IO with Option**
Optional bundle fields are one way to get this functionality. In the following example, we show a one-bit adder that optionally takes in a carry. If the carry is included, io.carryIn will have type Some[UInt] and be included in the IO bundle. If the carry is not included, io.carryIn will have type None and will be excluded from the IO bundle.

In [ ]:
class HalfFullAdder(val hasCarry: Boolean) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))
    val b = Input(UInt(1.W))
    val carryIn = if (hasCarry) Some(Input(UInt(1.W))) else None
    val s = Output(UInt(1.W))
    val carryOut = Output(UInt(1.W))
  })
  val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U)
  io.s := sum(0)
  io.carryOut := sum(1)
}

class HalfAdderTester(c: HalfFullAdder) extends PeekPokeTester(c) {
  require(!c.hasCarry, "DUT must be half adder")
  // 0 + 0 = 0
  poke(c.io.a, 0)
  poke(c.io.b, 0)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 0)
  // 0 + 1 = 1
  poke(c.io.b, 1)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 0)
  // 1 + 1 = 2
  poke(c.io.a, 1)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 1)
  // 1 + 0 = 1
  poke(c.io.b, 0)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 0)
}

class FullAdderTester(c: HalfFullAdder) extends PeekPokeTester(c) {
  require(c.hasCarry, "DUT must be half adder")
  poke(c.io.carryIn.get, 0)
  // 0 + 0 + 0 = 0
  poke(c.io.a, 0)
  poke(c.io.b, 0)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 0)
  // 0 + 0 + 1 = 1
  poke(c.io.b, 1)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 0)
  // 0 + 1 + 1 = 2
  poke(c.io.a, 1)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 1)
  // 0 + 1 + 0 = 1
  poke(c.io.b, 0)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 0)

  poke(c.io.carryIn.get, 1)
  // 1 + 0 + 0 = 1
  poke(c.io.a, 0)
  poke(c.io.b, 0)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 0)
  // 1 + 0 + 1 = 2
  poke(c.io.b, 1)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 1)
  // 1 + 1 + 1 = 3
  poke(c.io.a, 1)
  expect(c.io.s, 1)
  expect(c.io.carryOut, 1)
  // 1 + 1 + 0 = 2
  poke(c.io.b, 0)
  expect(c.io.s, 0)
  expect(c.io.carryOut, 1)
}

val worksHalf = iotesters.Driver(() => new HalfFullAdder(false)) { c => new HalfAdderTester(c) }
val worksFull = iotesters.Driver(() => new HalfFullAdder(true)) { c => new FullAdderTester(c) }
assert(worksHalf && worksFull) // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

**Example: Optional IO with Zero-Width Wires**
Another way to achieve similar functionality to Options is with zero-width wires. Chisel types are allowed to have widths of zero. An IO with width zero is pruned from the emitted Verilog, and anything that tries to use the value of a zero-width wire gets a constant zero. If zero is a sensible default value, zero-width wires can be nice because they obviate the need for matching on an option or calling getOrElse.

In [ ]:
class HalfFullAdder(val hasCarry: Boolean) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))
    val b = Input(UInt(1.W))
    val carryIn = Input(if (hasCarry) UInt(1.W) else UInt(0.W))
    val s = Output(UInt(1.W))
    val carryOut = Output(UInt(1.W))
  })
  val sum = io.a +& io.b +& io.carryIn
  io.s := sum(0)
  io.carryOut := sum(1)
}
println("Half Adder:")
println(getVerilog(new HalfFullAdder(false)))
println("\n\nFull Adder:")
println(getVerilog(new HalfFullAdder(true)))

Implicits

There are often times when you are programming that requires a lot of boilerplate code. To handle this use case, Scala introduced the notion of implicits, which allow the compiler to do some syntactic sugar for you. Because lots of things happen behind the scenes, implicits can appear very magical. This section breaks down some basic examples to explain what they are and where they are commonly used.

Implicit Arguments

At times, your code will require accessing a top-level variable of some sort from deep within a series of function calls. Instead of manually threading this variable through every function call, you can use implicit arguments to do it for you.

**Example: Implicit Cats**
In the following example, we can pass the number of cats implicitly or explicitly.

In [ ]:
object CatDog {
  implicit val numberOfCats: Int = 3
  //implicit val numberOfDogs: Int = 5

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

What's happening here? First, we define an implicit value numberOfCats. In a given scope, there can only be one implicit value of a given type. Then, we define a function that takes two argument lists; the first is any explicit parameters, and the second are any implicit parameters. When we call tooManyCats, we either omit the second implicit argument list (letting the compiler find it for us), or explicitly provide an argument (which can be different than the implicit value).

The following are ways implicit arguments can fail:

  • Two or more implicit values of a given type are defined in a scope
  • If the compiler cannot find an implicit value necessary for a function call

**Example: Implicit Logging**
The next code block shows how you might use implicit arguments to implement logging in a Chisel generator.

Note: there are better ways to do logging in Scala!

In [ ]:
sealed trait Verbosity
implicit case object Silent extends Verbosity
case object Verbose extends Verbosity

class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int)(implicit verbosity: Verbosity)
extends Module {
  def log(msg: => String): Unit = verbosity match {
    case Silent =>
    case Verbose => println(msg)
  }
  require(in0Width >= 0)
  log(s"in0Width of $in0Width OK")
  require(in1Width >= 0)
  log(s"in1Width of $in1Width OK")
  require(sumWidth >= 0)
  log(s"sumWidth of $sumWidth OK")
  val io = IO(new Bundle {
    val in0 = Input(UInt(in0Width.W))
    val in1 = Input(UInt(in1Width.W))
    val sum = Output(UInt(sumWidth.W))
  })
  log("Made IO")
  io.sum := io.in0 + io.in1
  log("Assigned output")
}

println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)))
println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))

Implicit Conversions

Like implicit arguments, implicit functions (also known as implicit conversions) are used to reduce boilerplate code. More specifically, they are used to automatically convert one Scala object into another.

**Example: Implicit Conversion**
In the following example, we have two classes, Animal and Human. Animal has a species field, but Human does not. However, by implementing an implicit conversion, we can call species on a Human.

In [ ]:
class Animal(val name: String, val species: String)
class Human(val name: String)
implicit def human2animal(h: Human): Animal = new Animal(h.name, "Homo sapiens")
val me = new Human("Adam")
println(me.species)

Generally, implicits can make your code confusing, so we recommend you use them as a last resort. First try inheritance, traits, or method overloading.


Generator Example

The following example shows a generator for a 1-bit input Mealy machine. It has a test based on the example from Wikipedia. Read through the code and try to follow what's going on.

**Example: Mealy Machine**
Try making your own parameterizations of the Mealy machine generator and writing your own tests in the code block below.

In [ ]:
// Mealy machine has
case class BinaryMealyParams(
  // number of states
  nStates: Int,
  // initial state
  s0: Int,
  // function describing state transition
  stateTransition: (Int, Boolean) => Int,
  // function describing output
  output: (Int, Boolean) => Int
) {
  require(nStates >= 0)
  require(s0 < nStates && s0 >= 0)
}

class BinaryMealy(val mp: BinaryMealyParams) extends Module {
  val io = IO(new Bundle {
    val in = Input(Bool())
    val out = Output(UInt())
  })

  val state = RegInit(UInt(), mp.s0.U)

  // output zero if no states
  io.out := 0.U
  for (i <- 0 until mp.nStates) {
    when (state === i.U) {
      when (io.in) {
        state  := mp.stateTransition(i, true).U
        io.out := mp.output(i, true).U
      }.otherwise {
        state  := mp.stateTransition(i, false).U
        io.out := mp.output(i, false).U
      }
    }
  }
}

// example from https://en.wikipedia.org/wiki/Mealy_machine
val nStates = 3
val s0 = 2
def stateTransition(state: Int, in: Boolean): Int = {
  if (in) {
    1
  } else {
    0
  }
}
def output(state: Int, in: Boolean): Int = {
  if (state == 2) {
    return 0
  }
  if ((state == 1 && !in) || (state == 0 && in)) {
    return 1
  } else {
    return 0
  }
}

val testParams = BinaryMealyParams(nStates, s0, stateTransition, output)

class BinaryMealyTester(c: BinaryMealy) extends PeekPokeTester(c) {
  poke(c.io.in, false)
  expect(c.io.out, 0)
  step(1)
  poke(c.io.in, false)
  expect(c.io.out, 0)
  step(1)
  poke(c.io.in, false)
  expect(c.io.out, 0)
  step(1)
  poke(c.io.in, true)
  expect(c.io.out, 1)
  step(1)
  poke(c.io.in, true)
  expect(c.io.out, 0)
  step(1)
  poke(c.io.in, false)
  expect(c.io.out, 1)
  step(1)
  poke(c.io.in, true)
  expect(c.io.out, 1)
  step(1)
  poke(c.io.in, false)
  expect(c.io.out, 1)
  step(1)
  poke(c.io.in, true)
  expect(c.io.out, 1)
}
val works = iotesters.Driver(() => new BinaryMealy(testParams)) { c => new BinaryMealyTester(c) }
assert(works) // Scala Code: if works == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

You're done!

Return to the top.