I have many modules with multiple parameters. Take as a toy example a modified version of the GCD in the template:
class GCD (len: Int = 16, validHigh: Boolean = true) extends Module {
val io = IO(new Bundle {
val value1 = Input(UInt(len.W))
val value2 = Input(UInt(len.W))
val loadingValues = Input(Bool())
val outputGCD = Output(UInt(len.W))
val outputValid = Output(Bool())
})
val x = Reg(UInt())
val y = Reg(UInt())
when(x > y) { x := x - y }
.otherwise { y := y - x }
when(io.loadingValues) {
x := io.value1
y := io.value2
}
io.outputGCD := x
if (validHigh) {
io.outputValid := (y === 0.U)
} else {
io.outputValid := (y =/= 0.U)
}
}
To test or synthesize many different designs, I want to change the values from the command line when I call the tester or the generator apps. Preferably, like this:
[generation or test command] --len 12 --validHigh false
but this or something similar would also be okay
[generation or test command] --param "len=12" --param "validHigh=false"
After some trial and error, I came up with a solution that looks like this:
gcd.scala
package gcd
import firrtl._
import chisel3._
case class GCDConfig(
len: Int = 16,
validHigh: Boolean = true
)
class GCD (val conf: GCDConfig = GCDConfig()) extends Module {
val io = IO(new Bundle {
val value1 = Input(UInt(conf.len.W))
val value2 = Input(UInt(conf.len.W))
val loadingValues = Input(Bool())
val outputGCD = Output(UInt(conf.len.W))
val outputValid = Output(Bool())
})
val x = Reg(UInt())
val y = Reg(UInt())
when(x > y) { x := x - y }
.otherwise { y := y - x }
when(io.loadingValues) {
x := io.value1
y := io.value2
}
io.outputGCD := x
if (conf.validHigh) {
io.outputValid := y === 0.U
} else {
io.outputValid := y =/= 0.U
}
}
trait HasParams {
self: ExecutionOptionsManager =>
var params: Map[String, String] = Map()
parser.note("Design Parameters")
parser.opt[Map[String, String]]('p', "params")
.valueName("k1=v1,k2=v2")
.foreach { v => params = v }
.text("Parameters of Design")
}
object GCD {
def apply(params: Map[String, String]): GCD = {
new GCD(params2conf(params))
}
def params2conf(params: Map[String, String]): GCDConfig = {
var conf = new GCDConfig
for ((k, v) <- params) {
(k, v) match {
case ("len", _) => conf = conf.copy(len = v.toInt)
case ("validHigh", _) => conf = conf.copy(validHigh = v.toBoolean)
case _ =>
}
}
conf
}
}
object GCDGen extends App {
val optionsManager = new ExecutionOptionsManager("gcdgen")
with HasChiselExecutionOptions with HasFirrtlOptions with HasParams
optionsManager.parse(args) match {
case true =>
chisel3.Driver.execute(optionsManager, () => GCD(optionsManager.params))
case _ =>
ChiselExecutionFailure("could not parse results")
}
}
and for tests
GCDSpec.scala
package gcd
import chisel3._
import firrtl._
import chisel3.tester._
import org.scalatest.FreeSpec
import chisel3.experimental.BundleLiterals._
import chiseltest.internal._
import chiseltest.experimental.TestOptionBuilder._
object GCDTest extends App {
val optionsManager = new ExecutionOptionsManager("gcdtest") with HasParams
optionsManager.parse(args) match {
case true =>
//println(optionsManager.commonOptions.programArgs)
(new GCDSpec(optionsManager.params)).execute()
case _ =>
ChiselExecutionFailure("could not parse results")
}
}
class GCDSpec(params: Map[String, String] = Map()) extends FreeSpec with ChiselScalatestTester {
"Gcd should calculate proper greatest common denominator" in {
test(GCD(params)) { dut =>
dut.io.value1.poke(95.U)
dut.io.value2.poke(10.U)
dut.io.loadingValues.poke(true.B)
dut.clock.step(1)
dut.io.loadingValues.poke(false.B)
while (dut.io.outputValid.peek().litToBoolean != dut.conf.validHigh) {
dut.clock.step(1)
}
dut.io.outputGCD.expect(5.U)
}
}
}
This way, I can generate different designs and test them with
sbt 'runMain gcd.GCDGen --params "len=12,validHigh=false"'
sbt 'test:runMain gcd.GCDTest --params "len=12,validHigh=false"'
But there are a couple of problems or annoyances with this solution:
- It uses deprecated features (ExecutionOptionsManager and HasFirrtlOptions). I'm not sure if this solution is portable to the new FirrtlStage Infrastructure.
- There is a lot of boilerplate involved. It becomes tedious to write new case classes and
params2conffunctions for every module and rewrite both when a parameter is added or removed. - Using
conf.xinstead ofxall the time. But I guess, this is unavoidable because there is nothing like python'skwargsin Scala.
Is there a better way or one that is at least not deprecated?