Prev: ChiselTest (was chisel-testers2)
Next: Generators: Collections
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.
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.tester._
import chisel3.tester.RawTester.test
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 Module
s is a Scala class just like any other.
Recall that Scala classes can be parameterized like so:
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.
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 nonsensical.
The above code block checks that widths are non-negative.
There is a separate construct for simulation-time assertions called assert(...)
.
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.
Example: Parameterized 4-Input Sort
Unlike 2.3, this implementation is parameterized to be either a descending or an ascending sort.
/** 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 row20 = Wire(UInt(16.W))
val row23 = Wire(UInt(16.W))
when(comp(row10, row13)) {
row20 := row10 // preserve the first and the forth elements
row23 := row13
}.otherwise {
row20 := row13 // swap the first and the forth elements
row23 := row10
}
when(comp(row20, row21)) {
io.out0 := row20 // preserve first two elements
io.out1 := row21
}.otherwise {
io.out0 := row21 // swap first two elements
io.out1 := row20
}
when(comp(row22, row23)) {
io.out2 := row22 // preserve first two elements
io.out3 := row23
}.otherwise {
io.out2 := row23 // swap first two elements
io.out3 := row22
}
}
// Here are the testers
test(new Sort4(true)) { c =>
c.io.in0.poke(3.U)
c.io.in1.poke(6.U)
c.io.in2.poke(9.U)
c.io.in3.poke(12.U)
c.io.out0.expect(3.U)
c.io.out1.expect(6.U)
c.io.out2.expect(9.U)
c.io.out3.expect(12.U)
c.io.in0.poke(13.U)
c.io.in1.poke(4.U)
c.io.in2.poke(6.U)
c.io.in3.poke(1.U)
c.io.out0.expect(1.U)
c.io.out1.expect(4.U)
c.io.out2.expect(6.U)
c.io.out3.expect(13.U)
c.io.in0.poke(13.U)
c.io.in1.poke(6.U)
c.io.in2.poke(4.U)
c.io.in3.poke(1.U)
c.io.out0.expect(1.U)
c.io.out1.expect(4.U)
c.io.out2.expect(6.U)
c.io.out3.expect(13.U)
}
test(new Sort4(false)) { c =>
c.io.in0.poke(3.U)
c.io.in1.poke(6.U)
c.io.in2.poke(9.U)
c.io.in3.poke(12.U)
c.io.out0.expect(12.U)
c.io.out1.expect(9.U)
c.io.out2.expect(6.U)
c.io.out3.expect(3.U)
c.io.in0.poke(13.U)
c.io.in1.poke(4.U)
c.io.in2.poke(6.U)
c.io.in3.poke(1.U)
c.io.out0.expect(13.U)
c.io.out1.expect(6.U)
c.io.out2.expect(4.U)
c.io.out3.expect(1.U)
c.io.in0.poke(1.U)
c.io.in1.poke(6.U)
c.io.in2.poke(4.U)
c.io.in3.poke(13.U)
c.io.out0.expect(13.U)
c.io.out1.expect(6.U)
c.io.out2.expect(4.U)
c.io.out3.expect(1.U)
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
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:
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
.
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
.
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
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.
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))))
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:
val mixedList = List(1, "string", false)
Example: Value Matching
The following example, depending on the value of the variable we match on, we execute a different case statement:
// 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:
=>
operator continues until it reaches either the ending brace of the match or the next case statement.checks against other case statements are made.
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:
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:
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.*
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
.
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
.
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))))
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.
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)
}
test(new HalfFullAdder(false)) { c =>
require(!c.hasCarry, "DUT must be half adder")
// 0 + 0 = 0
c.io.a.poke(0.U)
c.io.b.poke(0.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(0.U)
// 0 + 1 = 1
c.io.b.poke(1.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(0.U)
// 1 + 1 = 2
c.io.a.poke(1.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(1.U)
// 1 + 0 = 1
c.io.b.poke(0.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(0.U)
}
test(new HalfFullAdder(true)) { c =>
require(c.hasCarry, "DUT must be half adder")
c.io.carryIn.get.poke(0.U)
// 0 + 0 + 0 = 0
c.io.a.poke(0.U)
c.io.b.poke(0.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(0.U)
// 0 + 0 + 1 = 1
c.io.b.poke(1.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(0.U)
// 0 + 1 + 1 = 2
c.io.a.poke(1.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(1.U)
// 0 + 1 + 0 = 1
c.io.b.poke(0.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(0.U)
c.io.carryIn.get.poke(1.U)
// 1 + 0 + 0 = 1
c.io.a.poke(0.U)
c.io.b.poke(0.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(0.U)
// 1 + 0 + 1 = 2
c.io.b.poke(1.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(1.U)
// 1 + 1 + 1 = 3
c.io.a.poke(1.U)
c.io.s.expect(1.U)
c.io.carryOut.expect(1.U)
// 1 + 1 + 0 = 2
c.io.b.poke(0.U)
c.io.s.expect(0.U)
c.io.carryOut.expect(1.U)
}
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 Option
s 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
.
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)))
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.
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.
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:
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!*
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)))
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
.
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.
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.
// 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)
test(new BinaryMealy(testParams)) { c =>
c.io.in.poke(false.B)
c.io.out.expect(0.U)
c.clock.step(1)
c.io.in.poke(false.B)
c.io.out.expect(0.U)
c.clock.step(1)
c.io.in.poke(false.B)
c.io.out.expect(0.U)
c.clock.step(1)
c.io.in.poke(true.B)
c.io.out.expect(1.U)
c.clock.step(1)
c.io.in.poke(true.B)
c.io.out.expect(0.U)
c.clock.step(1)
c.io.in.poke(false.B)
c.io.out.expect(1.U)
c.clock.step(1)
c.io.in.poke(true.B)
c.io.out.expect(1.U)
c.clock.step(1)
c.io.in.poke(false.B)
c.io.out.expect(1.U)
c.clock.step(1)
c.io.in.poke(true.B)
c.io.out.expect(1.U)
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!