Chisel参数化生成器(从Scala讲起)
动机
要想使得Chisel当模块成为代码生成器时,必须有一些东西来告诉生成器如何执行这项工作。本节将介绍模块的参数化,涉及多种方法和Scala语言特性。丰富的参数传递将直接传递到生成的电路。参数应提供有用的默认值,易于设置,不会设置为非法或无意义的值。对于更复杂的系统来说,在局部重载而不影响其他模块的使用是非常有用的。
参数传递
Chisel它为写硬件生成器提供了强大的结构。生成器是个接一些电路参数并生成电路描述的程序。本节将讨论Chisel如何获取生成器的参数。
例子:参数化Scala对象
每个Chisel中的Mudule
都也是Scala的类,Scala所有类都可以以下参数化:
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")
执行结果如下:
I have parameters: param1 = 4 and param2 = Hello I have parameters: param1 = 6 and param2 = World
例子:参数化Chisel对象
Chisel模块也可以以同样的方式进行参数化。下面的模块有其所有的输入和输出宽度作为参数。可以打印并生成以下代码Verilog代码。输出的变化可以通过修改参数来看到。
import chisel3._ import chisel3.util._ import chisel3.tester._ import chisel3.tester.RawTester.test 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
}
object MyModule extends App {
println(getVerilogString(new ParameterizedWidthAdder(1, 4, 6)))
}
输出如下:
module ParameterizedWidthAdder(
input clock,
input reset,
input io_in0,
input [3:0] io_in1,
output [5:0] io_sum
);
wire [3:0] _GEN_0 = {
{3'd0}, io_in0}; // @[MyModule.scala 16:20]
wire [4:0] _io_sum_T = _GEN_0 + io_in1; // @[MyModule.scala 16:20]
assign io_sum = {
{1'd0}, _io_sum_T}; // @[MyModule.scala 16:10]
endmodule
前面的代码中有一些require(...)
语句。这些是预展开的断言,在希望生成器只在一定的参数化或一些参数化互斥或无意义时很有用。上面的代码就检查了宽度应该是非负数。
在仿真时有单独的断言构造,为assert(...)
。
使用参数化模块排序
下面的代码块是个参数化排序,和前面的Sort4
类似。和上面参数化加法器的例子不同,这个例子的输入输出是固定的。这里的参数会控制模块内部的硬件生成。
例子:参数化的4输入排序
和前面的Sort4
不同,这里的实现会参数化为降序或升序排序。
import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test
/** 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
}
}
object MyModule extends App {
// 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!
}
测试通过。
选项和默认参数
有时候函数会返回值,有时候则不会。除了不能返回值时报错以外,Scala还有个机制来将其编码到类型系统中来。
例子:错误的映射索引调用
在下面的例子中有个映射(Map),包含几个键值对(Key-Value Pairs)。如果我们尝试访问一个丢失的键值对,就会得到一个运行时错误。
val map = Map("a" -> 1)
val a = map("a")
println(a)
val b = map("b")
println(b)
输出:
java.util.NoSuchElementException: key not found: b
例子:得到不确定的切片
然而,Map
提供了另一种方式来访问一个键值对,是通过get
方法。使用get
方法会返回一个抽象类Option
的值。Option
有两个子类,Some
和None
。
val map = Map("a" -> 1)
val a = map.get("a")
println(a)
val b = map.get("b")
println(b)
输出为:
Some(1)
None
类型分别为:
map: Map[String, Int] = Map("a" -> 1)
a: Option[Int] = Some(1)
b: Option[Int] = None
后面的章节中会看到,Option
极其重要,因为它可以让用户用匹配语句来检查Scala类型或值。
例子:getOrElse
和Map
一样,Option
也有一个get
方法,如果在None
上调用的话就会报错。对于这种实例,我们可以用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
输出:
1
1
2
其中类型分别为:
some: Some[Int] = Some(1)
none: None.type = None
具备默认值的参数的Option
当对象或函数有很多参数的时候,每次都指定全部显然是又麻烦又易错。在很前面,我们介绍了命名参数和参数默认值。有的时候,参数并没有一个好的默认值。Option
可以和默认值None
一起使用在这些场合。
例子:可选的Reset
下面展示了一个代码块,延迟它的输入一个时钟周期。如果resetValue = None
,即默认值,寄存器将会无法得到复位值并被初始化为辣鸡值。这避免了普遍但丑陋的情况,即用常规范围之外的值来指示None
,比如使用-1
作为复位值来指示某个寄存器没有复位。
import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test
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
}
object MyModule extends App {
println(getVerilogString(new DelayBy1))
println(getVerilogString(new DelayBy1(Some(3.U))))
}
输出如下:
module DelayBy1(
input clock,
input reset,
input [15:0] io_in,
output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
reg [15:0] reg_; // @[MyModule.scala 15:8]
assign io_out = reg_; // @[MyModule.scala 18:10]
always @(posedge clock) begin
reg_ <= io_in; // @[MyModule.scala 17:7]
end
// Register and memory initialization
...
endmodule
module DelayBy1(
input clock,
input reset,
input [15:0] io_in,
output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
reg [15:0] reg_; // @[MyModule.scala 13:12]
assign io_out = reg_; // @[MyModule.scala 18:10]
always @(posedge clock) begin
if (reset) begin // @[MyModule.scala 13:12]
reg_ <= 16'h3; // @[MyModule.scala 13:12]
end else begin
reg_ <= io_in; // @[MyModule.scala 17:7]
end
end
// Register and memory initialization
...
endmodule
两者的区别在于,前者未对reset
信号进行相应,而后者在时钟上升沿收到reset
信号后,将寄存器的值复位为16'h3
。
match/case
语句
Scala的匹配概念贯穿整个Chisel语言,应该作为每个Chisel程序员的基本理解内容。Scala提供了match
运算符,支持以下功能:
- 对于候选项的简单测试,类似于C语言的
swtich
语句; - 对值的ad-hoc组合的更复杂的测试;
- 当一个变量的类型未知或待指定时,基于该变量的类型采取行为,例如:
- 变量来自一个异构的列表:
val mixedList = List(1, "string", false)
; - 或已知变量是一个超类的成员,但不知道是指定的哪个子类;
- 变量来自一个异构的列表:
- 根据一个正则表达式从一个字符串中提取子串;
例子:值匹配
下面的例子,基于匹配到的变量的值,我们可以执行不同的case
语句:
// 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)
输出为:
y is many
例子中,match
操作符检查可能的值,对于每种情况都会返回一个字符串。还有几点需要注意的是:
- 每个
=>
操作符之后的代码块都会延续到右大括号或者下一个case
语句之前; match
语句会按顺序进行搜索,一旦匹配就不会检查后续的情况了;_
下划线是通配符,用于匹配前面没有匹配上的任何其他情况;
例子:多值匹配
多个值可以同时进行匹配。这里有个简单的例子,是一个由match
语句和几个值构造的真值表:
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))
输出如下:
wolverine
例子:类型匹配
Scala是一种强类型的语言,所以每个对象在执行的时候都是知道类型的。我们可以使用match
语句来利用这个信息指示控制流:
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!")
}
}
输出为:
a is a String
1 is an Int
0.0 is a Double
代码中case s
中的s
是待输入的对象,写成a
也行,x
也行。
例子:类型匹配和擦除
类型匹配有一些限制。因为Scala是在JVM上运行的,而JVM不会维护复合(polymorphic)类型,所以不能在运行时匹配,因为会被擦除。下面的例子会总是匹配第一个条件语句,因为[String]
、[Int]
和[Double]
复合类型被擦除了,case
语句只会匹配上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")
}
}
输出为(编译或运行会提示警告信息):
<console>:15: warning: non-variable type argument String in type pattern Seq[String] (the underlying of Seq[String]) is unchecked since it is eliminated by erasure
case s: Seq[String] => println(s"$x is a String")
^
<console>:16: warning: non-variable type argument Int in type pattern Seq[Int] (the underlying of Seq[Int]) is unchecked since it is eliminated by erasure
case s: Seq[Int] => println(s"$x is an Int")
^
<console>:17: warning: non-variable type argument Double in type pattern Seq[Double] (the underlying of Seq[Double]) is unchecked since it is eliminated by erasure
case s: Seq[Double] => println(s"$x is a Double")
^
<console>:16: warning: unreachable code
case s: Seq[Int] => println(s"$x is an Int")
List(a) is a String
List(1) is a String
List(0.0) is a String
例子:可选的复位匹配
下面这个例子展示了相同的DelayBy1
模块,但是用match
语句替换了if/else
:
import chisel3._ import chisel3.util._ import chisel3.tester._ import chisel3.tester.RawTester.test 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