这是这个系列的最后一篇文章了,官方的Chisel-Bootcamp中后面还有FIRRTL相关的内容,但设计一个RISC-V CPU这样的目标,靠本系列文章讲述的内容已经足够了。由于这个系列的学习偏向于在实战中学习,所以难免有很多遗漏的地方。因此,后续还会更新一个系列,力争对Chisel语言进行全面详实的讲解。
另外,除了Chisel语言的基础学习,还会像之前承诺的那样,针对RISC-V体系结构的实现做一个系列。该系列将会从只有几个指令的单周期CPU开始,逐步实现支持RV64GC指令集、可运行操作系统的复杂CPU。敬请期待!
目前国内研究体系结构的较少,从事或打算相关工作的不多,各大知识分享平台上相关文章热度也不高,尤其是CSDN上,甚至还没有给计算机体系结构一个专门的分类,毕竟冷门研究方向啊。本人为某计科被评为A+学科的院校的计科硕士,硕士期间研究课题与计算机体系结构交集不多,但本人对体系结构有较浓厚的兴趣,因此在毕业之后一直考虑继续在体系结构方向学习,于是就有了这个系列。
虽然到目前为止这些文章还很少被浏览,但它们总归会是被需要的。既然如此,那我会继续坚持把这些事情做下去。
言归正传,Scala是一个强类型的编程语言,这是把双刃剑:不好的方面,很多可以在Python上解释并执行的程序,在Scala中连编译都通过不了,因为Python是个动态类型语言;好的方面,Scala中编译通过的程序在运行时就会比Python产生更少的错误。
这一篇的目的是让我们更了解作为Scala中一等公民的数据类型(types)。刚开始我们可能觉得自己写代码效率很低,但很快啊,我们就会学会去理解编译时的错误信息,进一步理解如何利用类型系统在大脑里构建我们的程序时就找到错误。
Scala中所有的对象都有一个类型,通常就是这个对象的类,比如:
println(10.getClass)
println(10.0.getClass)
println("ten".getClass)
输出为:
int
double
class java.lang.String
当我们声明自己的类时,它也有相应的类型:
class MyClass {
val a = 1
}
println(new MyClass().getClass)
输出为:
defined class MyClass
如果没有要求,强烈建议在声明函数的时候定义输入和输出的类型。这会让Scala编译器捕获到不恰当的函数使用。
def double(s: String): String = s + s
// 可以试试运行这几句
double("hi") // 正确用法,会返回"hihi"
double(10) // 错误用法,编译器会报参数类型不匹配错误
double("hi") / 10 // 错误用法,编译器会报返回值的使用不对(/ 不是String的成员)
如果函数不返回任何值,那返回类型就是Unit
。
var counter = 0
def increment(): Unit = {
counter += 1
}
increment()
前面我们讨论过Chisel类型和Scala类型的区别,举例来说:
val a = Wire(UInt(4.W))
a := 0.U
这个是正确用法,因为0.U
是UInt
类型(一个Chisel类型)的,而:
val a = Wire(UInt(4.W))
a := 0
就不对,因为0
是Int
类型(一个Scala类型)的。
还有布尔值,在Scala里面是Boolean
,而在Chisel里面是Bool
:
val bool = Wire(Bool())
val boolean: Boolean = false
// 正确用法
when (bool) { ... }
if (boolean) { ... }
// 错误用法
if (bool) { ... }
when (boolean) { ... }
如果我们不小心弄混了Scala类型和Chisel类型,Scala编译器会为我们生成捕获到的错误。这是因为Scala是静态类型的,在编译的时候,编译器能够区分Chisel和Scala类型,还能理解if ()
里面应该是Boolean
而when ()
里面应该是Bool
。
asInstanceOf
)x.asInstanceOf[T]
可以将对象x
转换为类型T
,如果不能够完成到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")
}
// 但是我们可以将x强制转换为Data类型,因为UInt就是从Data继承的
println(x.asInstanceOf[Data])
输出为:
As expected, we can't cast UInt to Int
UInt<2>(3)
下面的代码运行的话Scala内核会直接挂掉(为什么不是报错呢?不知道),因为将UInt
类型的值赋值给了SInt
,这是非法的:
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
}
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)
}
Chisel有一组类型转换函数,其中最常用的强制类型转换用的是x.asTypeOf(T)
,如果把第6行代码改成:
io.out := io.in.asTypeOf(io.out)
问题就解决了,测试通过。有一些Chisel对象还定义了asUInt()
和asSInt()
。
匹配操作符match
在前面就已经讲过。类型匹配在写泛型生成器(Type-generic Generator)的时候尤其有用。下面是一个生成器的例子,它可以把两个UInt
或SInt
的字面值相加。后面还会继续对泛型生成器进行阐述。
下面的例子中,chiselTypeOf
可以用于获取字面值的Chisel类型。注意:Scala中其实有更好、更安全的办法来写泛型生成器的。
class ConstantSum(in1: Data, in2: Data) extends Module {
val io = IO(new Bundle {
val out = Output(chiselTypeOf(in1)) // 获取in1的类型
})
(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(getVerilogString(new ConstantSum(3.U, 4.U)))
println(getVerilogString(new ConstantSum(-3.S, 4.S)))
println(getVerilogString(new ConstantSum(3.U, 4.S))) // 会触发异常
输出如下:
module ConstantSum(
input clock,
input reset,
output [1:0] io_out
);
assign io_out = 2'h3; // @[MyModule.scala 15:43]
endmodule
module ConstantSum(
input clock,
input reset,
output [2:0] io_out
);
assign io_out = 3'sh1; // @[MyModule.scala 16:43]
endmodule
[error] java.lang.Exception: I give up!
需要记住的是,Chisel类型通常不应该进行值匹配。Scala的match
会在电路展开时进行,但是你可能想要的是展开后的比较。下面的例子就会报一个符号错误:
class InputIsZero extends Module {
val io = IO(new Bundle {
val in = Input(UInt(16.W))
val out = Output(Bool())
})
io.out := (io.in match {
// 这里会触发编译报语法错误
case (0.U) => true.B
case _ => false.B
})
}
println(getVerilogString(new InputIsZero))
Unapply
当进行匹配的时候原理是什么呢?为什么我们可以完成下面这样有意思的匹配呢:
case class Something(a: String, b: Int)
val a = Something("A", 3)
a match {
case Something("A", value) => value
case Something(str, 3) => 0
}
这是因为每个case类都会有个伴生对象被创建,其中包含了一个unapply
方法,作为apply
方法的补充。那什么是unapply
方法呢?
Scala中的unapply
方法是另一种语法糖的形式,它可以给匹配语句在匹配中既能匹配类型,也能从类型中提取值的能力。
来看下面这个例子。如果我们给参数说这个生成器流水线化了,那延迟就是3*totalWidth
,否则就是2*someOtherWidth
。因为case类有定义了的unapply
,所以我们可以在case类里进行值匹配,就像这样:
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 sg @ SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
case SomeGeneratorParameters(_, sw, false) => sw * 2
}
println(delay(SomeGeneratorParameters(10, 10)))
println(delay(SomeGeneratorParameters(10, 10, true)))
输出为:
20
60
如果仔细看delay
函数的话,应该可以注意到除了进行类型匹配以外,我们还:
这是编译器实现了unapply
方法给予的可能性。要注意的是case类的unapply
只是个语法糖,下面这两条语句是等价的:
case p: SomeGeneratorParameters => p.sw * 2
case SomeGeneratorParameters(_, sw, _) => sw * 2
此外,匹配还有其他的语法和风格。下面这两条语句也是等价的,但是第二条语句允许在内部值上进行匹配的同时还引用父值:
case SomeGeneratorParameters(_, sw, true) => sw
case sg@SomeGeneratorParameters(_, sw, true) => sw
我们还可以直接在检查语句中嵌入条件检查,就像下面三条等价的语句演示的一样:
case SomeGeneratorParameters(_, sw, false) => sw * 2
case s@SomeGeneratorParameters(_, sw, false) => s.sw * 2
case s: SomeGeneratorParameters if s.pipelineMe => s.sw * 2
上面所有的语法能用都是因为Scala类的伴生对象中的unapply
方法。如果想unapply
一个类但又不想把它写成case类,那可以手动实现一个unapply
方法。下面的例子就演示了如何手动实现一个类的apply
和unapply
方法:
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!")
输出为:
Pinta and Nina are small boats!
这里只是简单介绍一下,详细的可以自己去找Scala的文档。
偏函数(PartialFunction
)是一种只定义在它输入的一个子集上的函数。就跟一个选项一样,偏函数也许对于特定输入不会返回值,这个可以通过isDefinedAt(...)
来测试。
偏函数之间可以通过orElse
串起来。注意,用未定义的输入调用偏函数会导致运行时错误。比如在给偏函数的输入是用户定义的时候就会发生。所以为了更具类型安全,建议写函数的时候返回一个Option
。下面看例子,最后一段的partialFunc3
通过or
把两个偏函数的定义域取并集了:
// 用来打印的助手函数,不然代码太冗长了
def printAndAssert(cmd: String, result: Boolean, expected: Boolean): Unit = {
println(s"$cmd = $result")
assert(result == expected)
}
// 这个偏函数是为..., -1, 2, 5, ...这些数定义的
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")
}
// 这个偏函数是为..., 1, 4, 7, ...这些数定义的
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)}")
输出如下:
partialFunc1.isDefinedAt(2) = true
partialFunc1.isDefinedAt(5) = true
partialFunc1.isDefinedAt(1) = false
partialFunc1.isDefinedAt(0) = false
partialFunc1(2) = Something
partialFunc1(0) = can't apply PartialFunctions where they are not defined
partialFunc2.isDefinedAt(1) = true
partialFunc2.isDefinedAt(0) = false
partialFunc2(1) = Something else
partialFunc2(0) = can't apply PartialFunctions where they are not defined
partialFunc3.isDefinedAt(0) = false
partialFunc3.isDefinedAt(1) = true
partialFunc3.isDefinedAt(2) = true
partialFunc3.isDefinedAt(3) = false
partialFunc3(1) = Something else
partialFunc3(2) = Something
Chisel可以检查不同类型的连接,下面这种连接就会报错:
Bool
/UInt
到Clock
对于其他类型,Chisel会允许我们连接,但是可能会酌情截断或者扩展数据,比如:
Bool
/UInt
到Bool
/UInt
Bundle
到Bundle
看例子:
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)
})
// 报错,failed @: Sink (Bool) and Source (Clock) have different types.
// io.out := io.c
// 可以,但是Chisel会把io.in截断成1位来匹配io.out
io.out := io.in
// 可以,但是Chisel只会把他俩的共有部分连接起来
io.bundleOut := io.bundleIn
}
println(getVerilogString(new BadTypeModule))
输出如下:
module BadTypeModule(
input clock,
input reset,
input io_c,
input [1:0] io_in,
output io_out,
input [7:0] io_bundleIn_a,
input [15:0] io_bundleIn_b,
output [7:0] io_bundleOut_a
);
assign io_out = io_in[0]; // @[MyModule.scala 31:10]
assign io_bundleOut_a = io_bundleIn_a; // @[MyModule.scala 34:16]
endmodule
Scala的泛型(也叫做polymorphism,多型,多态)很复杂,尤其是和继承耦合到一起的时候。这一小节就简单感受一下,更多内容也还是自己去找文档看。
类的类型可以是多态的,一个很好的例子就是序列,需要知道它们元素的类型:
val seq1 = Seq("1", "2", "3") // 类型为Seq[String]
val seq2 = Seq(1, 2, 3) // 类型为Seq[Int]
val seq3 = Seq(1, "2", true) // 类型为Seq[Any]
有时候Scala编译器需要确定一个多态类型,这就需要我们显式指定类型了:
//val default = Seq() // 官方说这里会报错,但实际上测试并没有报错
val default = Seq[String]() // 用户必须告诉编译器这个序列的具体类型
println(Seq(1, "2", "3", true).foldLeft(default){ (strings, next) =>
next match {
case s: String => strings ++ Seq(s)
case _ => strings
}
})
输出为:
List(2, 3)
函数的输入和输出类型也可以是多态的。下面的例子定义了一个函数,它会对一个代码块运行消耗的时间进行测试。它基于代码块的返回值类型来参数化。注意,=> T
语法编码了一个匿名的函数,这个匿名函数没有参数列表。
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
}
// 从1加到一百万
val int = time { (1 to 1000000).reduce(_ + _) }
println(s"Add 1 through a million is $int")
// 从1到一百万,找到其中对应的16进制字符串包含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")
输出如下:
Block took 23.394011 milliseconds!
Add 1 through a million is 1784293664
Block took 98.548134 milliseconds!
The largest number under a million that has beef: ebeef
为了写好Chisel中的泛型代码,了解一下Chisel的类型层级很有必要。
Chisel3.Data
是Chisel硬件类型的基类。UInt
、SInt
、Vec
和Bundle
等类型都是Data
的实例。Data
可以被用于IO接口,还可以支持:=
运算、wire、reg等。
寄存器就是Chisel中一个很好的多态的例子。我们可以看看Chisel源代码中RegEnable
(具备一个Bool
使能信号的寄存器)的实现:
object RegEnable {
/** Returns a register with the specified next, update enable gate, and no reset initialization.
*
* @example {{{
* val regWithEnable = RegEnable(nextVal, ena)
* }}}
*/
def apply[T <: Data](next: T, enable: Bool): T = {
val r = Reg(chiselTypeOf(next))
when(enable) { r := next }
r
}
/** Returns a register with the specified next, update enable gate, and reset initialization.
*
* @example {{{
* val regWithEnableAndReset = RegEnable(nextVal, 0.U, ena)
* }}}
*/
def apply[T <: Data](next: T, init: T, enable: Bool): T = {
val r = RegInit(init)
when(enable) { r := next }
r
}
}
可以看到,apply
函数是为[T <: Data]
创建的模板,这意味着RegEnable
对所有的Chisel硬件类型都有效。
有一些操作只定义在子类Bits
上,比如+
,这就是为什么可以让UInt
或SInt
相加而Bundle
和Vec
不行。
在Scala中,不仅可以把对象和函数当成参数,还可以把类型当作参数。
我们通常需要提供一个类型约束。在这个例子中,我们希望能够把对象放到一个Bundle
里,然后可以用:=
连接它们,并且利用它们创建寄存器(RegNext
)。这些操作并不能在任意对象上进行,比如说wire := 3
就不行,因为3
是个Scala整数,而不是Chisel整数。如果我们用一个类型约束指定类型T
必须是Data
的子类,那么我们就可以在任意T
类型的对象上用:=
了,因为:=
是为所有Data
类型定义的。
下面是一个以类型为参数的移位寄存器的实现。gen
是类型T
的参数,可以告知应该使用什么宽度的寄存器,比如ShiftRegister(UInt(4.W))
就是个4比特UInt
的移位寄存器。gen
还可以允许Scala编译器推断类型T
,你想指定的话还是可以写new ShiftRegister[UInt](UInt(4.W))
,但如果不写[UInt]
的话Scala编译器还是有能力判断出来。
// 根据类型创建Bundle的类
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是因为in包含在out中
// 下面这两行代码由官方给出,但是实际编译运行会报错:Users cannot override cloneType. Let the compiler plugin generate it.
//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)
}
}
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)
println(s"$i: ${c.io.out.indices
.map { index => c.io.out(index).peek().litValue }}")
c.clock.step(1)
}
}
输出如下:
Testing ShiftRegister of type ShiftRegister.io.in: IO[SInt<6>] and depth 4
0: Vector(0, 0, 0, 0)
1: Vector(1, 0, 0, 0)
2: Vector(2, 1, 0, 0)
3: Vector(3, 2, 1, 0)
4: Vector(4, 3, 2, 1)
5: Vector(5, 4, 3, 2)
6: Vector(6, 5, 4, 3)
7: Vector(7, 6, 5, 4)
8: Vector(8, 7, 6, 5)
9: Vector(9, 8, 7, 6)
官方建议避免使用泛型的继承,要想写好很有技巧性而且很容易就写崩。
上面的例子限制为可以在任何Data
的实例上运行的简单操作,比如:=
或RegNext()
。要生成DSP电路的时候,我们希望做一些类似于乘法和加法的数学操作。dsptools
库提供了一些写类型参数化DSP生成器用的工具。
下面是个写乘累加模块的例子。他可以用于为FixedPoint
、SInt
甚至DspComplex[T]
(dsptools
提供的复数类型)生成一个乘累加器(Multiply-And-Accumulate,MAC)。类型界定(Bound)的语法稍有不同,因为dsptools
使用的是类型类。它们超过了这个系列的范围,可以去看看dsptools
的readme文件和文档来了解一下。
T <: Data : Ring
表示T
是Data
的子类型同时也是Ring
的子类型。Ring
是定义在dsptools
中的数字类型,可以执行+
和*
的操作。(Ring
就是离散数学中的环,可以自行了解一下)
Ring
的一个替代是Real
,但是这样的话我们的MAC就不能在DspComplex()
上执行了,因为复数不是实数。下面看代码:
import chisel3.experimental._ // 因为需要用到定点数
import dsptools.numbers._ // 需要用到Ring
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))))
输出如下:
module Mac(
input clock,
input reset,
input [3:0] io_a,
input [3:0] io_b,
input [3:0] io_c,
output [5:0] io_out
);
wire [7:0] _io_out_T = io_a * io_b; // @[UIntTypeClass.scala 39:41]
wire [7:0] _GEN_0 = {{4'd0}, io_c}; // @[UIntTypeClass.scala 18:40]
wire [7:0] _io_out_T_2 = _io_out_T + _GEN_0; // @[UIntTypeClass.scala 18:40]
assign io_out = _io_out_T_2[5:0]; // @[MyModule.scala 20:12]
endmodule
module Mac(
input clock,
input reset,
input [3:0] io_a,
input [3:0] io_b,
input [3:0] io_c,
output [5:0] io_out
);
wire [7:0] _io_out_T = $signed(io_a) * $signed(io_b); // @[SIntTypeClass.scala 44:41]
wire [7:0] _GEN_0 = {{4{io_c[3]}},io_c}; // @[SIntTypeClass.scala 18:40]
wire [7:0] _io_out_T_3 = $signed(_io_out_T) + $signed(_GEN_0); // @[SIntTypeClass.scala 18:40]
assign io_out = _io_out_T_3[5:0]; // @[MyModule.scala 20:12]
endmodule
module Mac(
input clock,
input reset,
input [3:0] io_a,
input [3:0] io_b,
input [3:0] io_c,
output [5:0] io_out
);
wire [7:0] _io_out_T = $signed(io_a) * $signed(io_b); // @[FixedPointTypeClass.scala 43:59]
wire [6:0] _GEN_0 = {$signed(io_c), 3'h0}; // @[FixedPointTypeClass.scala 21:58]
wire [7:0] _GEN_1 = {{1{_GEN_0[6]}},_GEN_0}; // @[FixedPointTypeClass.scala 21:58]
wire [7:0] _io_out_T_3 = $signed(_io_out_T) + $signed(_GEN_1); // @[FixedPointTypeClass.scala 21:58]
assign io_out = _io_out_T_3[7:2]; // @[MyModule.scala 20:12]
endmodule
我们还可以把Mac
实现为对象,这个Mac
输入很少且只有一个输出,用在其他Chisel生成器里面应该很好用,比如:
val out = Mac(a, b, c)
那么我们可以给Mac
实现一个伴生对象,在伴生对象里面实现一下apply
函数:
object Mac {
def apply[T <: Data : Ring](a: T, b: T, c: T): T = {
a * b + c
}
}
然后测试一下用法:
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())
})
// 对于每一对输入输出,执行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(getVerilogString(new MacTestModule))
输出如下:
module MacTestModule(
input clock,
input reset,
input [3:0] io_uin,
output [7:0] io_uout,
input [3:0] io_sin,
output [7:0] io_sout,
input [15:0] io_fin,
output [31:0] io_fout
);
wire [7:0] _io_uout_T = io_uin * io_uin; // @[UIntTypeClass.scala 39:41]
wire [7:0] _GEN_0 = {{4'd0}, io_uin}; // @[UIntTypeClass.scala 18:40]
wire [7:0] _io_sout_T = $signed(io_sin) * $signed(io_sin); // @[SIntTypeClass.scala 44:41]
wire [7:0] _GEN_1 = {{4{io_sin[3]}},io_sin}; // @[SIntTypeClass.scala 18:40]
wire [31:0] _io_fout_T = $signed(io_fin) * $signed(io_fin); // @[FixedPointTypeClass.scala 43:59]
wire [27:0] _GEN_2 = {$signed(io_fin), 12'h0}; // @[FixedPointTypeClass.scala 21:58]
wire [31:0] _GEN_3 = {{4{_GEN_2[27]}},_GEN_2}; // @[FixedPointTypeClass.scala 21:58]
assign io_uout = _io_uout_T + _GEN_0; // @[UIntTypeClass.scala 18:40]
assign io_sout = $signed(_io_sout_T) + $signed(_GEN_1); // @[SIntTypeClass.scala 18:40]
assign io_fout = $signed(_io_fout_T) + $signed(_GEN_3); // @[FixedPointTypeClass.scala 21:58]
endmodule
下面实现一个下图所示的积分电路,每个时钟周期,输入会和寄存器中的值累加,产生的结果在下一个时钟周期写入寄存器中。
其中, n 1 n_1 n1表示geReg
的宽度,而 n 2 n_2 n2是genIn
的宽度。记住,Reg
、RegInit
、RegNext
、RegEnable
等都是为类型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)
})
val reg = RegInit(genReg, Ring[T].zero) // 寄存器初始化为0
reg := reg + io.in
io.out := reg
}
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)
}
测试通过。
让Chisel强大的原因之一是它的可拓展性。我们可以在Chisel中添加自己的数据类型,让它们拥有自己的操作符和表示,以支持我们的应用。这一小节就试试创建自定义类型。
DspComplex
DspComplex
就是dsptools
中定义的自定义数据类型,其中关键代码如下:
class DspComplex[T <: Data:Ring](val real: T, val imag: T) extends Bundle {
...
}
可以看到,DspComplex
是一个泛型容器,这意味着复数的实部和虚部可以是任何类型,只要它们满足相关的约束,这个约束由T <: Data:Ring
给定。
其中,T <: Data
表示T
是chisel3.Data
的子类型,这意味着DspComplex
只支持Chisel类型而不支持任意的Scala类型。T : Ring
表示T
的一个Ring
类型类实现已经存在了。Ring
类型类定义了+
和*
运算符作为加法和乘法的标识符(Ring就是离散数学中的环,环对乘法和加法运算封闭)。
dsptools
为常用的Chisel类型都定义了类型类,也为DspComplex
定义了Ring
类型类,所以我们可以在复数上复用我们的MAC生成器:
println(getVerilogString(new Mac(DspComplex(SInt(4.W), SInt(4.W)), DspComplex(SInt(6.W), SInt(6.W))) ))
注意,这里在本机测试会一个莫名其妙的错误:
[error] java.lang.AssertionError: assertion failed: The Chisel compiler plugin is now required for compiling Chisel code. Please see https://github.com/chipsalliance/chisel3#build-your-own-chisel-projects.
这也是Chisel目前存在的一个很大的问题,也就是版本兼容性,Chisel-bootcamp中使用的版本是3.4.+
,我使用的版本是3.5.0
,竟然也有这么大的差距,后面的系列会填坑的,用最新的版本。下面才是正常的输出:
module Mac(
input clock,
input reset,
input [3:0] io_a_real,
input [3:0] io_a_imag,
input [3:0] io_b_real,
input [3:0] io_b_imag,
input [3:0] io_c_real,
input [3:0] io_c_imag,
output [5:0] io_out_real,
output [5:0] io_out_imag
);
wire [3:0] _T_2 = $signed(io_b_real) + $signed(io_b_imag); // @[SIntTypeClass.scala 19:40]
wire [3:0] _T_5 = $signed(io_a_real) + $signed(io_a_imag); // @[SIntTypeClass.scala 19:40]
wire [3:0] _T_8 = $signed(io_a_imag) - $signed(io_a_real); // @[SIntTypeClass.scala 29:50]
wire [7:0] _T_9 = $signed(io_a_real) * $signed(_T_2); // @[SIntTypeClass.scala 45:41]
wire [7:0] _T_10 = $signed(_T_5) * $signed(io_b_imag); // @[SIntTypeClass.scala 45:41]
wire [7:0] _T_11 = $signed(_T_8) * $signed(io_b_real); // @[SIntTypeClass.scala 45:41]
wire [7:0] _T_14 = $signed(_T_9) - $signed(_T_10); // @[SIntTypeClass.scala 29:50]
wire [7:0] _T_17 = $signed(_T_9) + $signed(_T_11); // @[SIntTypeClass.scala 19:40]
wire [7:0] _GEN_0 = {{4{io_c_real[3]}},io_c_real}; // @[SIntTypeClass.scala 19:40]
wire [7:0] _T_20 = $signed(_T_14) + $signed(_GEN_0); // @[SIntTypeClass.scala 19:40]
wire [7:0] _GEN_1 = {{4{io_c_imag[3]}},io_c_imag}; // @[SIntTypeClass.scala 19:40]
wire [7:0] _T_23 = $signed(_T_17) + $signed(_GEN_1); // @[SIntTypeClass.scala 19:40]
assign io_out_real = _T_20[5:0]; // @[cmd17.sc 11:12]
assign io_out_imag = _T_23[5:0]; // @[cmd17.sc 11:12]
endmodule
假设我们想要使用数的带符号数表示,而且要复用我们所有的DSP生成器。类型类允许这种特设的多态。下面的例子给出了SignMagnitude
类型的实现和它的Ring
类型类的实现,这样就允许把这个类型用在Mac
生成器上了。
完整的实现和测试代码如下:
import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test
import chisel3.experimental._
import dsptools.numbers._
import chisel3.experimental.BundleLiterals._
// 前面定义的Mac生成器
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
}
// 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()
}
// 加法实现,this和that分别表示加号左右两边的操作数
def +(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 = {
this.+(-that)
}
// 负号运算,变符号就行
def unary_-(): SignMagnitude = {
val result = Wire(new SignMagnitude())
result.sign := !this.sign
result.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
}
}
// SignMagnitudeRing的Ring类型特质的实现,就是离散中环的元素对应的操作
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
}
// 对于这个例子,下面的就不实现了
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 = ???
}
object MyModule extends App {
// 一定要有这句,不然会报错:could not find implicit value for evidence parameter of type dsptools.numbers.Ring
implicit object SignMagnitudeRingImpl extends SignMagnitudeRing
// 测试
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!
}
测试通过。现在看看对应的Verilog代码长啥样:
object MyModule extends App {
// 一定要有这句,不然会报错:could not find implicit value for evidence parameter of type dsptools.numbers.Ring
implicit object SignMagnitudeRingImpl extends SignMagnitudeRing
println(
getVerilogString(
new Mac(new SignMagnitude(Some(4)), new SignMagnitude(Some(5)))
)
)
}
输出如下:
module Mac(
input clock,
input reset,
input io_a_sign,
input [3:0] io_a_magnitude,
input io_b_sign,
input [3:0] io_b_magnitude,
input io_c_sign,
input [3:0] io_c_magnitude,
output io_out_sign,
output [4:0] io_out_magnitude
);
wire io_out_result_sign = io_a_sign ^ io_b_sign; // @[MyModule.scala 56:30]
wire [7:0] io_out_result_magnitude = io_a_magnitude * io_b_magnitude; // @[MyModule.scala 57:40]
wire io_out_signsTheSame = io_out_result_sign == io_c_sign; // @[MyModule.scala 30:34]
wire [7:0] _GEN_4 = {{4'd0}, io_c_magnitude}; // @[MyModule.scala 33:42]
wire [7:0] _io_out_result_magnitude_T_2 = io_out_result_magnitude + _GEN_4; // @[MyModule.scala 33:42]
wire [7:0] _io_out_result_magnitude_T_4 = io_out_result_magnitude - _GEN_4; // @[MyModule.scala 37:44]
wire [7:0] _io_out_result_magnitude_T_6 = _GEN_4 - io_out_result_magnitude; // @[MyModule.scala 40:44]
wire _GEN_0 = io_out_result_magnitude > _GEN_4 ? io_out_result_sign : io_c_sign; // @[MyModule.scala 35:45 36:21 39:21]
wire [7:0] _GEN_1 = io_out_result_magnitude > _GEN_4 ? _io_out_result_magnitude_T_4 : _io_out_result_magnitude_T_6; // @[MyModule.scala 35:45 37:26 40:26]
wire [7:0] io_out_result_1_magnitude = io_out_signsTheSame ? _io_out_result_magnitude_T_2 : _GEN_1; // @[MyModule.scala 31:24 33:24]
assign io_out_sign = io_out_signsTheSame ? io_out_result_sign : _GEN_0; // @[MyModule.scala 31:24 32:19]
assign io_out_magnitude = io_out_result_1_magnitude[4:0]; // @[MyModule.scala 18:10]
endmodule
甚至可以把SignMagnitude
用在DspComplex
里面:
object MyModule extends App {
// 一定要有这句,不然会报错:could not find implicit value for evidence parameter of type dsptools.numbers.Ring,不过这里就是还是会报错罢了
implicit object SignMagnitudeRingImpl extends SignMagnitudeRing
println(
getVerilogString(
new Mac(
DspComplex(new SignMagnitude(Some(4)), new SignMagnitude(Some(4))),
DspComplex(new SignMagnitude(Some(5)), new SignMagnitude(Some(5)))
)
)
)
}
就是还是会因为DspComplex
报错罢了,正确的输出应该是这样的:
module Mac(
input clock,
input reset,
input io_a_real_sign,
input [3:0] io_a_real_magnitude,
input io_a_imag_sign,
input [3:0] io_a_imag_magnitude,
input io_b_real_sign,
input [3:0] io_b_real_magnitude,
input io_b_imag_sign,
input [3:0] io_b_imag_magnitude,
input io_c_real_sign,
input [3:0] io_c_real_magnitude,
input io_c_imag_sign,
input [3:0] io_c_imag_magnitude,
output io_out_real_sign,
output [4:0] io_out_real_magnitude,
output io_out_imag_sign,
output [4:0] io_out_imag_magnitude
);
wire _T = io_b_real_sign == io_b_imag_sign; // @[cmd20.sc 11:34]
wire [3:0] _T_2 = io_b_real_magnitude + io_b_imag_magnitude; // @[cmd20.sc 15:42]
wire [3:0] _T_5 = io_b_real_magnitude - io_b_imag_magnitude; // @[cmd20.sc 20:44]
wire [3:0] _T_7 = io_b_imag_magnitude - io_b_real_magnitude; // @[cmd20.sc 23:44]
wire _GEN_0 = io_b_real_magnitude > io_b_imag_magnitude ? io_b_real_sign : io_b_imag_sign; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [3:0] _GEN_1 = io_b_real_magnitude > io_b_imag_magnitude ? _T_5 : _T_7; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire _GEN_2 = _T ? io_b_real_sign : _GEN_0; // @[cmd20.sc 13:24 cmd20.sc 14:19]
wire [3:0] _GEN_3 = _T ? _T_2 : _GEN_1; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_8 = io_a_real_sign == io_a_imag_sign; // @[cmd20.sc 11:34]
wire [3:0] _T_10 = io_a_real_magnitude + io_a_imag_magnitude; // @[cmd20.sc 15:42]
wire [3:0] _T_13 = io_a_real_magnitude - io_a_imag_magnitude; // @[cmd20.sc 20:44]
wire [3:0] _T_15 = io_a_imag_magnitude - io_a_real_magnitude; // @[cmd20.sc 23:44]
wire _GEN_4 = io_a_real_magnitude > io_a_imag_magnitude ? io_a_real_sign : io_a_imag_sign; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [3:0] _GEN_5 = io_a_real_magnitude > io_a_imag_magnitude ? _T_13 : _T_15; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire _GEN_6 = _T_8 ? io_a_real_sign : _GEN_4; // @[cmd20.sc 13:24 cmd20.sc 14:19]
wire [3:0] _GEN_7 = _T_8 ? _T_10 : _GEN_5; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_16 = ~io_a_real_sign; // @[cmd20.sc 33:24]
wire _T_17 = io_a_imag_sign == _T_16; // @[cmd20.sc 11:34]
wire [3:0] _T_19 = io_a_imag_magnitude + io_a_real_magnitude; // @[cmd20.sc 15:42]
wire _GEN_8 = io_a_imag_magnitude > io_a_real_magnitude ? io_a_imag_sign : _T_16; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [3:0] _GEN_9 = io_a_imag_magnitude > io_a_real_magnitude ? _T_15 : _T_13; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire _GEN_10 = _T_17 ? io_a_imag_sign : _GEN_8; // @[cmd20.sc 13:24 cmd20.sc 14:19]
wire [3:0] _GEN_11 = _T_17 ? _T_19 : _GEN_9; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_25 = io_a_real_sign ^ _GEN_2; // @[cmd20.sc 39:30]
wire [7:0] _T_26 = io_a_real_magnitude * _GEN_3; // @[cmd20.sc 40:40]
wire _T_27 = _GEN_6 ^ io_b_imag_sign; // @[cmd20.sc 39:30]
wire [7:0] _T_28 = _GEN_7 * io_b_imag_magnitude; // @[cmd20.sc 40:40]
wire _T_29 = _GEN_10 ^ io_b_real_sign; // @[cmd20.sc 39:30]
wire [7:0] _T_30 = _GEN_11 * io_b_real_magnitude; // @[cmd20.sc 40:40]
wire _T_31 = ~_T_27; // @[cmd20.sc 33:24]
wire _T_32 = _T_25 == _T_31; // @[cmd20.sc 11:34]
wire [7:0] _T_34 = _T_26 + _T_28; // @[cmd20.sc 15:42]
wire [7:0] _T_37 = _T_26 - _T_28; // @[cmd20.sc 20:44]
wire [7:0] _T_39 = _T_28 - _T_26; // @[cmd20.sc 23:44]
wire _GEN_12 = _T_26 > _T_28 ? _T_25 : _T_31; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [7:0] _GEN_13 = _T_26 > _T_28 ? _T_37 : _T_39; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire _GEN_14 = _T_32 ? _T_25 : _GEN_12; // @[cmd20.sc 13:24 cmd20.sc 14:19]
wire [7:0] _GEN_15 = _T_32 ? _T_34 : _GEN_13; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_40 = _T_25 == _T_29; // @[cmd20.sc 11:34]
wire [7:0] _T_42 = _T_26 + _T_30; // @[cmd20.sc 15:42]
wire [7:0] _T_45 = _T_26 - _T_30; // @[cmd20.sc 20:44]
wire [7:0] _T_47 = _T_30 - _T_26; // @[cmd20.sc 23:44]
wire _GEN_16 = _T_26 > _T_30 ? _T_25 : _T_29; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [7:0] _GEN_17 = _T_26 > _T_30 ? _T_45 : _T_47; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire _GEN_18 = _T_40 ? _T_25 : _GEN_16; // @[cmd20.sc 13:24 cmd20.sc 14:19]
wire [7:0] _GEN_19 = _T_40 ? _T_42 : _GEN_17; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_48 = _GEN_14 == io_c_real_sign; // @[cmd20.sc 11:34]
wire [7:0] _GEN_28 = {{4'd0}, io_c_real_magnitude}; // @[cmd20.sc 15:42]
wire [7:0] _T_50 = _GEN_15 + _GEN_28; // @[cmd20.sc 15:42]
wire [7:0] _T_53 = _GEN_15 - _GEN_28; // @[cmd20.sc 20:44]
wire [7:0] _T_55 = _GEN_28 - _GEN_15; // @[cmd20.sc 23:44]
wire _GEN_20 = _GEN_15 > _GEN_28 ? _GEN_14 : io_c_real_sign; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [7:0] _GEN_21 = _GEN_15 > _GEN_28 ? _T_53 : _T_55; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire [7:0] _GEN_23 = _T_48 ? _T_50 : _GEN_21; // @[cmd20.sc 13:24 cmd20.sc 15:24]
wire _T_56 = _GEN_18 == io_c_imag_sign; // @[cmd20.sc 11:34]
wire [7:0] _GEN_32 = {{4'd0}, io_c_imag_magnitude}; // @[cmd20.sc 15:42]
wire [7:0] _T_58 = _GEN_19 + _GEN_32; // @[cmd20.sc 15:42]
wire [7:0] _T_61 = _GEN_19 - _GEN_32; // @[cmd20.sc 20:44]
wire [7:0] _T_63 = _GEN_32 - _GEN_19; // @[cmd20.sc 23:44]
wire _GEN_24 = _GEN_19 > _GEN_32 ? _GEN_18 : io_c_imag_sign; // @[cmd20.sc 18:45 cmd20.sc 19:21 cmd20.sc 22:21]
wire [7:0] _GEN_25 = _GEN_19 > _GEN_32 ? _T_61 : _T_63; // @[cmd20.sc 18:45 cmd20.sc 20:26 cmd20.sc 23:26]
wire [7:0] _GEN_27 = _T_56 ? _T_58 : _GEN_25; // @[cmd20.sc 13:24 cmd20.sc 15:24]
assign io_out_real_sign = _T_48 ? _GEN_14 : _GEN_20; // @[cmd20.sc 13:24 cmd20.sc 14:19]
assign io_out_real_magnitude = _GEN_23[4:0]; // @[cmd17.sc 11:12]
assign io_out_imag_sign = _T_56 ? _GEN_18 : _GEN_24; // @[cmd20.sc 13:24 cmd20.sc 14:19]
assign io_out_imag_magnitude = _GEN_27[4:0]; // @[cmd17.sc 11:12]
endmodule
完结收工!