a * b + c
Prev: Object Oriented Programming
Next: Introduction to FIRRTL
Scala is a strongly-typed programming language. This is a two-edged sword; on one hand, many programs that would compile and execute in Python (a dynamically-typed language) would fail at compile time in Scala. On the other hand, programs that compile in Scala will contain many fewer runtime errors than a similar Python program.
In this section, our goal is to familiarize you with types as a first class citizen in Scala. While initially you may feel you have limited productivity, you will soon learn to understand compile-time error messages and how to architect your programs with the type system in mind to catch more errors for you.
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
println(10.getClass)
println(10.0.getClass)
println("ten".getClass)
When you declare your own class, it has an associated type.
class MyClass {
def myMethod = ???
}
println(new MyClass().getClass)
While not required, it is HIGHLY recommended that you define input and output types for all function declarations. This will let the Scala compiler catch improper use of a function.
def double(s: String): String = s + s
// Uncomment the code below to test it
// double("hi") // Proper use of double
// double(10) // Bad input argument!
// double("hi") / 10 // Inproper use of double's output!
Functions that don't return anything return type Unit
.
var counter = 0
def increment(): Unit = {
counter += 1
}
increment()
Recap: Module 2.2 discussed the difference between Chisel types and Scala types, for example the fact that
val a = Wire(UInt(4.W))
a := 0.U
is legal because 0.U
is of type UInt
(a Chisel type), whereas
val a = Wire(UInt(4.W))
a := 0
is illegal because 0 is type Int
(a Scala type).
This is also true of Bool
, a Chisel type which is distinct from Boolean
.
val bool = Wire(Bool())
val boolean: Boolean = false
// legal
when (bool) { ... }
if (boolean) { ... }
// illegal
if (bool) { ... }
when (boolean) { ... }
If you make a mistake and mix up UInt
and Int
or Bool
and Boolean
, the Scala compiler will generally catch it for you.
This is because of Scala's static typing.
At compile time, the compiler is able to distinguish between Chisel and Scala types and also able to understand that if ()
expects a Boolean
and when ()
expects a Bool
.
x.asInstanceOf[T]
casts the object x
to the type T
. It throws an exception if the given object cannot be cast to type T
.
val x: UInt = 3.U
try {
println(x.asInstanceOf[Int])
} catch {
case e: java.lang.ClassCastException => println("As expected, we can't cast UInt to Int")
}
// But we can cast UInt to Data since UInt inherits from Data.
println(x.asInstanceOf[Data])
The code below will give an error if you try to run it without removing the comment.
What's the problem?
It is trying to assign a UInt
to an SInt
, which is illegal.
Chisel has a set of type casting functions.
The most general is asTypeOf()
, which is shown below.
Some chisel objects also define asUInt()
and asSInt()
as well as some others.
If you remove the //
from the code block below, the example should work for you.
class TypeConvertDemo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(SInt(4.W))
})
io.out := io.in//.asTypeOf(io.out)
}
test(new TypeConvertDemo) { c =>
c.io.in.poke(3.U)
c.io.out.expect(3.S)
c.io.in.poke(15.U)
c.io.out.expect(-1.S)
}
Recall that in 3.1 the match operator was introduced.
Type matching is especially useful when trying to write type-generic generators.
The following example shows an example of a "generator" that can add two literals of type UInt
or SInt
.
Later sections will talk more about writing type-generic generators.
Note: there are much better and safer ways to write type-generic generators in Scala.
class ConstantSum(in1: Data, in2: Data) extends Module {
val io = IO(new Bundle {
val out = Output(chiselTypeOf(in1)) // in case in1 is literal then just get its type
})
(in1, in2) match {
case (x: UInt, y: UInt) => io.out := x + y
case (x: SInt, y: SInt) => io.out := x + y
case _ => throw new Exception("I give up!")
}
}
println(getVerilog(dut = new ConstantSum(3.U, 4.U)))
println(getVerilog(dut = new ConstantSum(-3.S, 4.S)))
println(getVerilog(dut = new ConstantSum(3.U, 4.S)))
It is good to remember that Chisel types generally should not be value matched. Scala's match executes during circuit elaboration, but what you probably want is a post-elaboration comparison. The following gives a syntax error:
class InputIsZero extends Module {
val io = IO(new Bundle {
val in = Input(UInt(16.W))
val out = Output(Bool())
})
io.out := (io.in match {
// note that case 0.U is an error
case (0.U) => true.B
case _ => false.B
})
}
println(getVerilog(new InputIsZero))
What's actually going on when you do a match? How does Scala let you do fancy value matching with case classes like this:
case class Something(a: String, b: Int)
val a = Something("A", 3)
a match {
case Something("A", value) => value
case Something(str, 3) => 0
}
As it turns out, the companion object that is created for every case class also contains an unapply method, in addition to an apply method. What is an unapply method?
Scala unapply methods are another form of syntactic sugar that give match statements the ability to both match on types and extract values from those types during the matching.
Let's look at the following example.
For some reason, let's say that if the generator is being pipelined, the delay is 3*totalWidth
, otherwise the delay is 2*someOtherWidth
.
Because case classes have unapply defined, we can match values inside the case class, like so:
case class SomeGeneratorParameters(
someWidth: Int,
someOtherWidth: Int = 10,
pipelineMe: Boolean = false
) {
require(someWidth >= 0)
require(someOtherWidth >= 0)
val totalWidth = someWidth + someOtherWidth
}
def delay(p: SomeGeneratorParameters): Int = p match {
case SomeGeneratorParameters(_, sw, false) => sw * 2
case sg @SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
}
println(delay(SomeGeneratorParameters(10, 10)))
println(delay(SomeGeneratorParameters(10, 10, true)))
If you look at the delay
function, you should note that in addition to matching on the type of each character, we are also:
These are possible due to the compiler implementing an unapply
method. Note that unapplying the case is just syntactic sugar; e.g. the following two cases examples are equivalent:
case p: SomeGeneratorParameters => p.sw * 2
case SomeGeneratorParameters(_, sw, _) => sw * 2
In addition, there are more syntaxes and styles of matching. The following two cases are also equivalent, but the second allows you to match on internal values while still referencing the parent value:
case SomeGeneratorParameters(_, sw, true) => sw
case sg @SomeGeneratorParameters(_, sw, true) => sw
Finally, you can directly embed condition checking into match statements, as demonstrated by the third of these equivalent examples:
case SomeGeneratorParameters(_, sw, false) => sw * 2
case s @SomeGeneratorParameters(_, sw, false) => s.sw * 2
case s: SomeGeneratorParameters if s.pipelineMe => s.sw * 2
All these syntaxes are enabled by a Scala unapply method contained in a class's companion object. If you want to unapply a class but do not want to make it a case class, you can manually implement the unapply method. The following example demonstrates how one can manually implement a class's apply and unapply methods:
class Boat(val name: String, val length: Int)
object Boat {
def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
def apply(name: String, length: Int): Boat = new Boat(name, length)
}
def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
b match {
case Boat(_, length) if length < 60 => true
case Boat(_, _) => false
}
}
val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")
This is a brief overview; this guide has a more detailed overview.
Partial functions are functions that are only defined on a subset of their inputs.
Like an option, a partial function may not have a value for a particular input.
This can be tested with isDefinedAt(...)
.
Partial functions can be chained together with orElse
.
Note that calling a PartialFunction
with an undefined input will result in a runtime error. This can happen, for example, if the input to the PartialFunction
is user-defined. To be more type-safe, we recommend writing functions that return an Option
instead.
// Helper function to make this cell a bit less tedious.
def printAndAssert(cmd: String, result: Boolean, expected: Boolean): Unit = {
println(s"$cmd = $result")
assert(result == expected)
}
// Defined for -1, 2, 5, etc.
val partialFunc1: PartialFunction[Int, String] = {
case i if (i + 1) % 3 == 0 => "Something"
}
printAndAssert("partialFunc1.isDefinedAt(2)", partialFunc1.isDefinedAt(2), true)
printAndAssert("partialFunc1.isDefinedAt(5)", partialFunc1.isDefinedAt(5), true)
printAndAssert("partialFunc1.isDefinedAt(1)", partialFunc1.isDefinedAt(1), false)
printAndAssert("partialFunc1.isDefinedAt(0)", partialFunc1.isDefinedAt(0), false)
println(s"partialFunc1(2) = ${partialFunc1(2)}")
try {
println(partialFunc1(0))
} catch {
case e: scala.MatchError => println("partialFunc1(0) = can't apply PartialFunctions where they are not defined")
}
// Defined for 1, 4, 7, etc.
val partialFunc2: PartialFunction[Int, String] = {
case i if (i + 2) % 3 == 0 => "Something else"
}
printAndAssert("partialFunc2.isDefinedAt(1)", partialFunc2.isDefinedAt(1), true)
printAndAssert("partialFunc2.isDefinedAt(0)", partialFunc2.isDefinedAt(0), false)
println(s"partialFunc2(1) = ${partialFunc2(1)}")
try {
println(partialFunc2(0))
} catch {
case e: scala.MatchError => println("partialFunc2(0) = can't apply PartialFunctions where they are not defined")
}
val partialFunc3 = partialFunc1 orElse partialFunc2
printAndAssert("partialFunc3.isDefinedAt(0)", partialFunc3.isDefinedAt(0), false)
printAndAssert("partialFunc3.isDefinedAt(1)", partialFunc3.isDefinedAt(1), true)
printAndAssert("partialFunc3.isDefinedAt(2)", partialFunc3.isDefinedAt(2), true)
printAndAssert("partialFunc3.isDefinedAt(3)", partialFunc3.isDefinedAt(3), false)
println(s"partialFunc3(1) = ${partialFunc3(1)}")
println(s"partialFunc3(2) = ${partialFunc3(2)}")
Chisel can check the type for many connections, including:
For other types, Chisel will let you connect them, but may truncate/pad bits as appropriate.
class Bundle1 extends Bundle {
val a = UInt(8.W)
}
class Bundle2 extends Bundle1 {
val b = UInt(16.W)
}
class BadTypeModule extends Module {
val io = IO(new Bundle {
val c = Input(Clock())
val in = Input(UInt(2.W))
val out = Output(Bool())
val bundleIn = Input(new Bundle2)
val bundleOut = Output(new Bundle1)
})
//io.out := io.c // won't work due to different types
// Okay, but Chisel will truncate the input width to 1 to match the output.
// io.out := io.in
// // Compiles; Chisel will connect the common subelements of the two Bundles (in this case, 'a').
// io.bundleOut := io.bundleIn
}
println(getVerilog(new BadTypeModule))
Scala's generic types (also known as polymorphism) is very complicated, especially when coupling it with inheritance.
This section will just get your toes wet; to understand more, check out this tutorial.
Classes can be polymorphic in their types. One good example is sequences, which require knowing their contained type.
val seq1 = Seq("1", "2", "3") // Type is Seq[String]
val seq2 = Seq(1, 2, 3) // Type is Seq[Int]
val seq3 = Seq(1, "2", true) // Type is Seq[Any]
Sometimes, the Scala compiler needs help determining a polymorphic type, which requires the user to explicitly put the type:
//val default = Seq() // Error!
val default = Seq[String]() // User must tell compiler that default is of type Seq[String]
Seq(1, "2", true).foldLeft(default){ (strings, next) =>
next match {
case s: String => strings ++ Seq(s)
case _ => strings
}
}
Functions can also be polymorphic in their input or output types. The following example defines a function that times how long it takes to run a block of code. It is parameterized based on the return type of the block of code. Note that the => T
syntax encodes an anonymous function that does not have an argument list, e.g. { ... }
versus { x => ... }
.
def time[T](block: => T): T = {
val t0 = System.nanoTime()
val result = block
val t1 = System.nanoTime()
val timeMillis = (t1 - t0) / 1000000.0
println(s"Block took $timeMillis milliseconds!")
result
}
// Adds 1 through a million
val int = time { (1 to 1000000).reduce(_ + _) }
println(s"Add 1 through a million is $int")
// Finds the largest number under a million that, in hex, contains "beef"
val string = time {
(1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
}
println(s"The largest number under a million that has beef: $string")
To write type generic code with Chisel, it is helpful to know a bit about the type hierarchy of Chisel.
chisel3.Data
is the base class for Chisel hardware types.
UInt
, SInt
, Vec
, Bundle
, etc. are all instances of Data
.
Data
can be used in IOs and supports :=
, wires, regs, etc.
Registers are a good example of polymorphic code in Chisel.
Look at the implementation of RegEnable
(a register with a Bool
enable signal) here.
The apply function is templated for [T <: Data]
, which means RegEnable
will work for all Chisel hardware types.
Some operations are only defined on subtypes of Bits
, for example +
.
This is why you can add UInt
s or SInt
s but not Bundle
s or Vec
s.
Example: Type Generic ShiftRegister
In Scala, objects and functions aren't the only things we can treat as parameters.
We can also treat types as parameters.
We usually need to provide a type constraint. In this case, we want to be able to put objects in a bundle, connect (:=) them, and create registers with them (RegNext). These operations cannot be done on arbitrary objects; for example wire := 3 is illegal because 3 is a Scala Int, not a Chisel UInt. If we use a type constraint to say that type T is a subclass of Data, then we can use := on any objects of type T because := is defined for all Data.
Here is an implementation of a shift register that take types as a parameter. gen is an argument of type T that tells what width to use, for example new ShiftRegister(UInt(4.W)) is a shift register for 4-bit UInts. gen also allows the Scala compiler to infer the type T- you can write new ShiftRegisterUInt if you want to to be more specific, but the Scala compiler is smart enough to figure it out if you leave out the [UInt].
class ShiftRegisterIO[T <: Data](gen: T, n: Int) extends Bundle {
require (n >= 0, "Shift register must have non-negative shift")
val in = Input(gen)
val out = Output(Vec(n + 1, gen)) // + 1 because in is included in out
override def cloneType: this.type = (new ShiftRegisterIO(gen, n)).asInstanceOf[this.type]
}
class ShiftRegister[T <: Data](gen: T, n: Int) extends Module {
val io = IO(new ShiftRegisterIO(gen, n))
io.out.foldLeft(io.in) { case (in, out) =>
out := in
RegNext(in)
}
}
visualize(() => new ShiftRegister(SInt(6.W), 3))
test(new ShiftRegister(SInt(6.W), 3)) { c =>
println(s"Testing ShiftRegister of type ${c.io.in} and depth ${c.io.out.length}")
for (i <- 0 until 10) {
c.io.in.poke(i.S) // magic literal creation
println(s"$i: ${c.io.out.indices.map { index => c.io.out(index).peek().litValue} }")
c.clock.step(1)
}}
We generally recommend avoiding to use inheritance with type generics. It can be very tricky to do properly and can get frustrating quickly.
The example above was limited to simple operations that could be performed on any instance of Data
such as :=
or RegNext()
.
When generating DSP circuits, we would like to do mathematical operations like addition and multiplication.
The dsptools
library provides tools for writing type parameterized DSP generators.
Here is an example of writing a multiply-accumulate module.
It can be used to generate a multiply-accumulate (MAC) for FixedPoint
, SInt
, or even DspComplex[T]
(the complex number type provided by dsptools
).
The syntax of the type bound is a little different because dsptools
uses typeclasses.
They are beyond the scope of this notebook.
Read the dsptools
readme and documentation for more information on using typeclasses.
T <: Data : Ring
means that T
is a subtype of Data
and is also a Ring
.
Ring
is defined in dsptools
as a number with +
and *
(among other operations).
An alternative to Ring
would be Real
, but that would not allow us to make a MAC for DspComplex()
because complex numbers are not Real
.
import chisel3.experimental._
import dsptools.numbers._
class Mac[T <: Data : Ring](genIn : T, genOut: T) extends Module {
val io = IO(new Bundle {
val a = Input(genIn)
val b = Input(genIn)
val c = Input(genIn)
val out = Output(genOut)
})
io.out := io.a * io.b + io.c
}
println(getVerilog(new Mac(UInt(4.W), UInt(6.W)) ))
println(getVerilog(new Mac(SInt(4.W), SInt(6.W)) ))
println(getVerilog(new Mac(FixedPoint(4.W, 3.BP), FixedPoint(6.W, 4.BP))))
Exercise: Mac as Object
The Mac Module
has a small number of inputs and just one output.
It might be convenient for other Chisel generators to write code like
val out = Mac(a, b, c)
Implement an apply
method in the Mac
companion object below that implements the Mac
functionality.
object Mac {
def apply[T <: Data : Ring](a: T, b: T, c: T): T = {
??? // your code
}
}
class MacTestModule extends Module {
val io = IO(new Bundle {
val uin = Input(UInt(4.W))
val uout = Output(UInt())
val sin = Input(SInt(4.W))
val sout = Output(SInt())
//val fin = Input(FixedPoint(16.W, 12.BP))
//val fout = Output(FixedPoint())
})
// for each IO pair, do out = in * in + in
io.uout := Mac(io.uin, io.uin, io.uin)
io.sout := Mac(io.sin, io.sin, io.sin)
//io.fout := Mac(io.fin, io.fin, io.fin)
}
println(getVerilog(new MacTestModule))
a * b + c
Exercise: Integrator
Implement an integrator as pictured below. $n_1$ is the width of genReg
and $n_2$ is the width of genIn
.
Don't forget that Reg
, RegInit
, RegNext
, RegEnable
, etc. are templated for types T <: Data
.
class Integrator[T <: Data : Ring](genIn: T, genReg: T) extends Module {
val io = IO(new Bundle {
val in = Input(genIn)
val out = Output(genReg)
})
??? // your code
}
test(new Integrator(SInt(4.W), SInt(8.W))) { c =>
c.io.in.poke(3.S)
c.io.out.expect(0.S)
c.clock.step(1)
c.io.in.poke(-4.S)
c.io.out.expect(3.S)
c.clock.step(1)
c.io.in.poke(6.S)
c.io.out.expect(-1.S)
c.clock.step(1)
c.io.out.expect(5.S)
}
class Integrator[T <: Data : Ring](genIn: T, genReg: T) extends Module { val io = IO(new Bundle { val in = Input(genIn.cloneType) val out = Output(genReg.cloneType) })
val reg = RegInit(genReg, Ring[T].zero) // init to zero reg := reg + io.in io.out := reg
}
One of the things that makes Chisel powerful is its extensibility. You can add your own types that have their own operations and representations that are tailored to your application. This section will introduce ways to make custom types.
Example: DspComplex
DspComplex
is a custom data type defined in dsptools here.
The key line to understand is this:
class DspComplex[T <: Data:Ring](val real: T, val imag: T) extends Bundle { ... }
DspComplex
is a type-generic container.
That means the real and imaginary parts of a complex number can be any type as long as they satisfy the type constraints, given by T <: Data : Ring
.
T <: Data
means T
is a subtype of chisel3.Data
, the base type for Chisel objects.
This means that DspComplex
only works for objects that are Chisel types and not arbitrary Scala types.
T : Ring
means that a Ring typeclass implementation for T
exists.
Ring
typeclasses define +
and *
operators as well as additive and multiplicative identities (see this Wikipedia article for details about rings).
dsptools defines typeclasses for commonly used Chisel types here.
dsptools also defines a Ring
typeclass for DspComplex
, so we can reuse our MAC generator with complex numbers:
println(getVerilog(new Mac(DspComplex(SInt(4.W), SInt(4.W)), DspComplex(SInt(6.W), SInt(6.W))) ))
Exercise: Sign-magnitude Numbers
Suppose you wanted to use a sign-magnitude representation and want to reuse all of your DSP generators.
Typeclasses enable this kind of ad-hoc polymorphism.
The following example gives the beggining of an implementation of a SignMagnitude type as well as an implementation of a Ring
typeclass that will allow the type to be used with the Mac generator.
Fill in implementations for +
and *
.
You should pattern them after the implementation for unary_-()
.
The next block contains a test that checks the correctness of a Mac
that uses SignMagnitude
.
class SignMagnitude(val magnitudeWidth: Option[Int] = None) extends Bundle {
val sign = Bool()
val magnitude = magnitudeWidth match {
case Some(w) => UInt(w.W)
case None => UInt()
}
def +(that: SignMagnitude): SignMagnitude = {
// Implement this!
}
def -(that: SignMagnitude): SignMagnitude = {
this.+(-that)
}
def unary_-(): SignMagnitude = {
val result = Wire(new SignMagnitude())
result.sign := !this.sign
result.magnitude := this.magnitude
result
}
def *(that: SignMagnitude): SignMagnitude = {
// Implement this!
}
}
trait SignMagnitudeRing extends Ring[SignMagnitude] {
def plus(f: SignMagnitude, g: SignMagnitude): SignMagnitude = {
f + g
}
def times(f: SignMagnitude, g: SignMagnitude): SignMagnitude = {
f * g
}
def one: SignMagnitude = {
val one = Wire(new SignMagnitude(Some(1)))
one.sign := false.B
one.magnitude := 1.U
one
}
def zero: SignMagnitude = {
val zero = Wire(new SignMagnitude(Some(0)))
zero.sign := false.B
zero.magnitude := 0.U
zero
}
def negate(f: SignMagnitude): SignMagnitude = {
-f
}
// Leave unimplemented for this example
def minusContext(f: SignMagnitude, g: SignMagnitude): SignMagnitude = ???
def negateContext(f: SignMagnitude): SignMagnitude = ???
def plusContext(f: SignMagnitude,g: SignMagnitude): SignMagnitude = ???
def timesContext(f: SignMagnitude,g: SignMagnitude): SignMagnitude = ???
}
implicit object SignMagnitudeRingImpl extends SignMagnitudeRing
import chisel3.experimental.BundleLiterals._
test(new Mac(new SignMagnitude(Some(4)), new SignMagnitude(Some(5)))) { c =>
c.io.a.poke(chiselTypeOf(c.io.a).Lit(_.sign -> false.B, _.magnitude -> 3.U))
c.io.b.poke(chiselTypeOf(c.io.b).Lit(_.sign -> false.B, _.magnitude -> 3.U))
c.io.c.poke(chiselTypeOf(c.io.c).Lit(_.sign -> false.B, _.magnitude -> 2.U))
c.io.out.expect(chiselTypeOf(c.io.out).Lit(_.sign -> false.B, _.magnitude -> 11.U))
c.io.c.sign.poke(true.B)
c.io.out.expect(chiselTypeOf(c.io.out).Lit(_.sign -> false.B, _.magnitude -> 7.U))
c.io.b.sign.poke(true.B)
c.io.out.expect(chiselTypeOf(c.io.out).Lit(_.sign -> true.B, _.magnitude -> 11.U))
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
Look at the verilog to see if the output looks reasonable:
println(getVerilog(new Mac(new SignMagnitude(Some(4)), new SignMagnitude(Some(5)))))
SignMagnitude
even works with DspComplex
!
println(getVerilog(new Mac(DspComplex(new SignMagnitude(Some(4)), new SignMagnitude(Some(4))), DspComplex(new SignMagnitude(Some(5)), new SignMagnitude(Some(5))))))
// implementations for class SignMagnitudedef +(that: SignMagnitude): SignMagnitude = { val result = Wire(new SignMagnitude()) val signsTheSame = this.sign === that.sign when (signsTheSame) { result.sign := this.sign result.magnitude := this.magnitude + that.magnitude } .otherwise { when (this.magnitude > that.magnitude) { result.sign := this.sign result.magnitude := this.magnitude - that.magnitude } .otherwise { result.sign := that.sign result.magnitude := that.magnitude - this.magnitude } } result } def *(that: SignMagnitude): SignMagnitude = { val result = Wire(new SignMagnitude()) result.sign := this.sign ^ that.sign result.magnitude := this.magnitude * that.magnitude result }