-
Notifications
You must be signed in to change notification settings - Fork 197
Scripting Hardware Generation
Often times parametrization requires instantiating multiple components which are connected in a very regular structure.
A revisit to the parametrized Adder
component definition shows the for
loop construct in action:
// A n-bit adder with carry in and carry out
class Adder(n: Int) extends Module {
val io = IO(new Bundle {
val A = Input(UInt(n.W))
val B = Input(UInt(n.W))
val Cin = Input(UInt(1.W))
val Sum = Output(UInt(n.W))
val Cout = Output(UInt(1.W))
})
// create a vector of FullAdders
val FAs = Vec(Seq.fill(n) { Module(new FullAdder()).io })
val carry = Vec(Seq.fill(n + 1) { UInt(1.W) })
val sum = Vec(Seq.fill(n) { Bool() })
// first carry is the top level carry in
carry(0) := io.Cin
// wire up the ports of the full adders
for (i <- 0 until n) {
FAs(i).a := io.A(i)
FAs(i).b := io.B(i)
FAs(i).cin := carry(i)
carry(i + 1) := FAs(i).cout
sum(i) := FAs(i).sum.toBool()
}
io.Sum := sum.asUInt
io.Cout := carry(n)
}
Notice that a Scala integer i
value is used in the for
loop definition as the index variable.
This indexing variable is specified to take values from 0 until
n, which means it takes values 0, 1, 2..., n-1.
If we wanted it to take values from 0 to n inclusive, we would use for (i <- 0 to n)
.
It is also important to note, that the indexing variable i
does not actually manifest itself in the generated hardware.
It exclusively belongs to Scala and is only used in declaring how the connections are specified in the Chisel component definition.
The for loop construct is also very useful for assigning to arbitrarily long Vec
s
As previously mentioned, the if, elseif,
and else
keywords are reserved for Scala control structures.
What this means for Chisel is that these constructs allow you to selectively generate different structures depending on parameters that are supplied.
This is particularly useful when you want to turn certain features of your implementation "on" or "off", or if you want to use a different variant of some component.
For instance, suppose we have several simple counters that we would like to package up into a general purpose counter module: UpCounter, DownCounter, and OneHotCounter. From the definitions below, we notice that for these simple counters, the I/O interfaces and parameters are identical:
// Simple up counter that increments from 0 and wraps around
class UpCounter(CounterWidth: Int) extends Module {
val io = IO(new Bundle {
val output = Output(UInt(CounterWidth.W))
val ce = Input(Bool())
})...
}
// Simple down counter that decrements from
// 2^CounterWidth-1 then wraps around
class DownCounter(CounterWidth: Int) extends Module {
val io = IO(new Bundle {
val output = Output(UInt(CounterWidth.W))
val ce = Input(Bool())
})...
}
// Simple one hot counter that increments from one hot 0
// to CounterWidth-1 then wraps around
class OneHotCounter(CounterWidth:Int) extends Module {
val io = IO(new Bundle {
val output = Output(UInt(CounterWidth.W))
val ce = Input(Bool())
})...
}
We could just instantiate all three of these counters and multiplex between them but if we needed one at any given time this would be a waste of hardware.
In order to choose between which of these three counters we want to instantiate, we can use Scala's if, else if, else
statements to tell Chisel how to pick which component to instantiate based on a CounterType
parameter:
class Counter(CounterWidth: Int, CounterType: String)
extends Module {
val io = IO(new Bundle {
val output = Output(UInt(CounterWidth.W))
val ce = Input(Bool())
})
if (CounterType == "UpCounter") {
val upcounter = new UpCounter(CounterWidth)
upcounter.io.ce := io.ce
io.output := upcounter.io.output
} else if (CounterType == "DownCounter") {
val downcounter = new DownCounter(CounterWidth)
downcounter.io.ce := io.ce
io.output := downcounter.io.output
} else if (CounterType == "OneHotCounter") {
val onehotcounter = new OneHotCounter(CounterWidth)
onehotcounter.io.ce := io.ce
io.output := onehotcounter.io.output
} else {
// default output 1
io.output := 1.U
}
}
By consolidating these three counter components into a single Counter
module, we can instantiate a different counter by simply changing the parameter CounterType
.
For instance:
// instantiate a down counter of width 16
val downcounter =
Module(new Counter(16, "DownCounter"))
// instantiate an up counter of width 16
val upcounter =
Module(new Counter(16, "UpCounter"))
// instantiate a one hot counter of width 16
val onehotcounter =
Module(new Counter(16, "OneHotCounter"))
This allows seamless alternation between them.
Chisel also allows the usage of the Scala def
statement to define Chisel code that may be used frequently.
These def
statements can be packaged into a Scala Object and then called inside a Module.
The following Chisel code shows an alternate implementation of an counter using def
that increments by amt
if the inc
signal is asserted.
object Counter {
def wrapAround(n: UInt, max: UInt) =
Mux(n > max, 0.U, n)
def counter(max: UInt, en: Bool, amt: UInt) = {
val x = RegInit(0.U(max.getWidth.W))
x := wrapAround(x + amt, max)
x
}
}
class Counter extends Module {
val io = IO(new Bundle {
val inc = Input(Bool())
val amt = Input(UInt(4.W))
val tot = Output(UInt(8.W))
})
io.tot := counter(255.U, io.inc, io.amt)
}
In this example, we use calls to subroutines defined in the Counter
object in order to perform the appropriate logic.
The next assignment is to construct a bit shift register with delay parameter.
The following is the template from $TUT_DIR/src/main/scala/problems/VecShiftRegisterParam.scala
:
class VecShiftRegisterParam(val n: Int, val w: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(w.W))
val out = Output(UInt(w.W))
})
...
io.out := 0.U
}
where out
is a n
cycle delayed copy of values on in
.
Also notice how val
is added to each parameter value to
allow those values to be accessible from the tester.
Edit $TUT_DIR/src/main/scala/problems/VecShiftRegisterParam.scala
and run:
./run-problem.sh VecShiftRegisterParam
until your circuit passes the tests.
The next assignment is to write a 16x16 multiplication table using Vec
.
The following is the template from $TUT_DIR/src/main/scala/problems/Mul.scala
:
class Mul extends Module {
val io = IO(new Bundle {
val x = Input(UInt(4.W))
val y = Input(UInt(4.W))
val z = Output(UInt(8.W))
})
val muls = new ArrayBuffer[UInt]()
// flush this out ...
io.z := 0.U
}
As a hint build the lookup table using a rom constructed from the tab
lookup table represented as a Scala ArrayBuffer with incrementally added elements (using +=
):
val tab = Vec(muls)
and lookup the result using an address formed from the x
and y
inputs as follows:
io.z := tab(Cat(io.x, io.y))
Edit $TUT_DIR/src/main/scala/problems/Mul.scala
and run:
./run-problem.sh Mul
until your circuit passes the tests.