主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87905379
函数是编程语言的常用语法,即使是Verilog这样的硬件描述语言,也会用函数来构建组合逻辑。对于Chisel这样的高级语言,函数的使用更加方便,还能节省不少代码量。不管是用户自己写的函数、Chisel语言库里的函数还是Scala标准库里的函数,都能帮助用户节省构建电路的时间。
零、scala函数基础
1、普通函数
// No inputs or outputs (two versions).
def hello1(): Unit = print("Hello!")
def hello2 = print("Hello again!")
// Math operation: one input and one output.
def times2(x: Int): Int = 2 * x
// Inputs can have default values, and explicitly specifying the return type is optional.
// Note that we recommend specifying the return types to avoid surprises/bugs.
def timesN(x: Int, n: Int = 2) = n * x
// Call the functions listed above.
hello1()
hello2
times2(4)
timesN(4) // no need to specify n to use the default value
timesN(4, 3) // argument order is the same as the order where the function was defined
timesN(n=7, x=2) // arguments may be reordered and assigned to explicitly
Hello!Hello again!
defined function hello1
defined function hello2
defined function times2
defined function timesN
res2_6: Int = 8
res2_7: Int = 8
res2_8: Int = 12
res2_9: Int = 14
2、函数字面量
Scala中的函数是一等对象。这意味着我们可以将一个函数分配给一个Val,并将它作为参数传递给类、对象或其他函数。
// These are normal functions.
def plus1funct(x: Int): Int = x + 1
def times2funct(x: Int): Int = x * 2
// These are functions as vals.
// The first one explicitly specifies the return type.
val plus1val: Int => Int = x => x + 1
val times2val = (x: Int) => x * 2
// Calling both looks the same.
plus1funct(4)
plus1val(4)
plus1funct(x=4)
//plus1val(x=4) // this doesn't work
defined function plus1funct
defined function times2funct
plus1val: Int => Int = ammonite. s e s s . c m d 3 sess.cmd3 sess.cmd3HelperKaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲3224/925602942@…Lambda$3225/[email protected]
res3_4: Int = 5
res3_5: Int = 5
res3_6: Int = 5
为什么要创建一个Val而不是def?因为使用Val,可以将该函数传递给其他函数,如下所示。您甚至可以创建接受其他函数作为参数的自己的函数。形式上,接受或产生函数的函数称为高阶函数。
3、高阶函数
// create our function
val plus1 = (x: Int) => x + 1
val times2 = (x: Int) => x * 2
// pass it to map, a list function
val myList = List(1, 2, 5, 9)
val myListPlus = myList.map(plus1)
val myListTimes = myList.map(times2)
// create a custom function, which performs an operation on X N times using recursion
def opN(x: Int, n: Int, op: Int => Int): Int = {
if (n <= 0) { x }
else { opN(op(x), n-1, op) }
}
opN(7, 3, plus1)
opN(7, 3, times2)
plus1: Int => Int = ammonite. s e s s . c m d 4 sess.cmd4 sess.cmd4HelperKaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲3249/997984377@…Lambda$3250/[email protected]
myList: List[Int] = List(1, 2, 5, 9)
myListPlus: List[Int] = List(2, 3, 6, 10)
myListTimes: List[Int] = List(2, 4, 10, 18)
defined function opN
res4_6: Int = 10
res4_7: Int = 56
当使用没有参数的函数时,2/3两种情况可能会出现混淆的情况。如下所示:
import scala.util.Random
// both x and y call the nextInt function, but x is evaluated immediately and y is a function
val x = Random.nextInt
def y = Random.nextInt
// x was previously evaluated, so it is a constant
println(s"x = $x")
println(s"x = $x")
// y is a function and gets reevaluated at each call, thus these produce different results
println(s"y = $y")
println(s"y = $y")
x = 1775160696
x = 1775160696
y = 804455125
y = 958584765
可以看到,x其实就是一个常量,是调用
Random.nextInt
返回的一个常量;而y是你定义的一个函数,该函数的函数体就是执行
Random.nextInt
这个函数,所以每次调用y都会执行
Random.nextInt
,也就会出现不一样的结果。
4、匿名函数
顾名思义,匿名函数是匿名的。如果我们只使用它一次,就没有必要为函数创建Val。
val myList = List(5, 6, 7, 8)
// add one to every item in the list using an anonymous function
// arguments get passed to the underscore variable
// these all do the same thing
myList.map( (x:Int) => x + 1 )
myList.map(_ + 1)
// a common situation is to use case statements within an anonymous function
val myAnyList = List(1, 2, "3", 4L, myList)
myAnyList.map {
case (_:Int|_:Long) => "Number"
case _:String => "String"
case _ => "error"
}
myList: List[Int] = List(5, 6, 7, 8)
res6_1: List[Int] = List(6, 7, 8, 9)
res6_2: List[Int] = List(6, 7, 8, 9)
myAnyList: List[Any] = List(1, 2, “3”, 4L, List(5, 6, 7, 8))
res6_4: List[String] = List(“Number”, “Number”, “String”, “Number”, “error”)
一、用自定义函数抽象组合逻辑
与Verilog一样,对于频繁使用的组合逻辑电路,可以定义成Scala的函数形式,然后通过函数调用的方式来使用它。这些函数既可以定义在某个单例对象里,供多个模块重复使用,也可以直接定义在电路模块里。例如:
// function.scala
import chisel3._
class UseFunc extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out1 = Output(Bool())
val out2 = Output(Bool())
})
def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt =
(a & b) | (~c & d)
io.out1 := clb(io.in(0), io.in(1), io.in(2), io.in(3))
io.out2 := clb(io.in(0), io.in(2), io.in(3), io.in(1))
}
二、用工厂方法简化模块的例化
在Scala里,往往在类的伴生对象里定义一个工厂方法,来简化类的实例化。同样,Chisel的模块也是Scala的类,也可以在其伴生对象里定义工厂方法来简化例化、连线模块。例如用双输入多路选择器构建四输入多路选择器:
// mux4.scala
import chisel3._
class Mux2 extends Module {
val io = IO(new Bundle {
val sel = Input(UInt(1.W))
val in0 = Input(UInt(1.W))
val in1 = Input(UInt(1.W))
val out = Output(UInt(1.W))
})
io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}
object Mux2 {
def apply(sel: UInt, in0: UInt, in1: UInt) = {
val m = Module(new Mux2)
m.io.in0 := in0
m.io.in1 := in1
m.io.sel := sel
m.io.out
}
}
class Mux4 extends Module {
val io = IO(new Bundle {
val sel = Input(UInt(2.W))
val in0 = Input(UInt(1.W))
val in1 = Input(UInt(1.W))
val in2 = Input(UInt(1.W))
val in3 = Input(UInt(1.W))
val out = Output(UInt(1.W))
})
io.out := Mux2(io.sel(1),
Mux2(io.sel(0), io.in0, io.in1),
Mux2(io.sel(0), io.in2, io.in3))
}
注:
其实就是把例化模块和端口连接的代码放入了伴生对象的apply方法里,这样可以被重复使用。当然也可以只定义一个普通函数,不使用伴生对象。函数的输入输出其实就是待使用的模块的输入输出。
三、用Scala的函数简化代码
Scala的函数也能在Chisel里使用,只要能通过Firrtl编译器的检查。比如在生成长的序列上,利用Scala的函数就能减少大量的代码。
假设要构建一个译码器:
- 在Verilog里需要写多条case语句,当n很大时就会使代码显得冗长而枯燥。
- 利用Scala的for、yield组合可以产生相应的判断条件与输出结果的序列,再用zip函数将两个序列组成一个对偶序列,再把对偶序列作为MuxCase的参数,就能用几行代码构造出任意位数的译码器。例如:
// decoder.scala
package decoder
import chisel3._
import chisel3.util._
import chisel3.experimental._
class Decoder(n: Int) extends RawModule {
val io = IO(new Bundle {
val sel = Input(UInt(n.W))
val out = Output(UInt((1 << n).W))
})
val x = for(i <- 0 until (1 << n)) yield io.sel === i.U
val y = for(i <- 0 until (1 << n)) yield 1.U << i
io.out := MuxCase(0.U, x zip y)
}
object DecoderGen extends App {
chisel3.Driver.execute(args, () => new Decoder(args(0).toInt))
}
只需要输入参数n,就能立即生成对应的n位译码器。
四、Chisel的对数函数
在二进制运算里,求以2为底的对数也是常用的运算。
chisel3.util
包里有一个单例对象Log2,它的一个apply方法接收一个Bits类型的参数,计算并返回该参数值以2为底的幂次。返回类型是UInt类型,并且是向下截断的。另一个apply的重载版本可以接受第二个Int类型的参数,用于指定返回结果的位宽。例如:
Log2(8.U) // 等于3.U
Log2(13.U) // 等于3.U(向下截断)
Log2(myUIntWire) // 动态求值
chisel3.util
包里还有四个单例对象:
log2Ceil、log2Floor、log2Up和log2Down
,它们的apply方法的参数都是Int和BigInt类型,返回结果都是Int类型。log2Ceil是把结果向上舍入,log2Floor则向下舍入。log2Up和log2Down不仅分别把结果向上、向下舍入,而且结果最小为1。
单例对象isPow2的apply方法接收Int和BigInt类型的参数,判断该整数是不是2的n次幂,返回Boolean类型的结果。
五、chisel获取数据位宽
在
chisel3.util
包里还有两个用来获取数据位宽的函数,分别是
signedBitLength
和
unsignedBitLength
,使用方法如下例所示。需要注意的是入参类型是scala的
BigInt
,返回值是
Int
,因此如果用在chisel构建电路的过程中,不要忘了根据需要转换成chisel的数据类型。
val a = -12
val b = 15
println(signedBitLength(a))//5
println(unsignedBitLength(b))//4
六、与硬件相关的函数
chisel3.util
包里还有一些常用的操作硬件的函数:
Ⅰ、位旋转
单例对象
Reverse
的apply方法可以把一个UInt类型的对象进行旋转,返回一个对应的UInt值。在转换成Verilog时,都是通过拼接完成的组合逻辑。例如:
Reverse("b1101".U) // 等于"b1011".U
Reverse("b1101".U(8.W)) // 等于"b10110000".U
Reverse(myUIntWire) // 动态旋转
Ⅱ、位拼接
单例对象
Cat
有两个apply方法,分别接收一个Bits类型的序列和Bits类型的重复参数,将它们拼接成一个UInt数。前面的参数在高位。例如:
Cat("b101".U, "b11".U) // 等于"b10111".U
Cat(myUIntWire0, myUIntWire1) // 动态拼接
Cat(Seq("b101".U, "b11".U)) // 等于"b10111".U
Cat(mySeqOfBits) // 动态拼接
Ⅲ、1计数器
单例对象
PopCount
有两个apply方法,分别接收一个Bits类型的参数和Bool类型的序列,计算参数里“1”或“true.B”的个数,返回对应的UInt值。例如:
PopCount(Seq(true.B, false.B, true.B, true.B)) // 等于3.U
PopCount(Seq(false.B, false.B, true.B, false.B)) // 等于1.U
PopCount("b1011".U) // 等于3.U
PopCount("b0010".U) // 等于1.U
PopCount(myUIntWire) // 动态计数
Ⅳ、独热码转换器
- 单例对象
的apply方法可以接收一个Bits类型或Bool序列类型的独热码参数,计算独热码里的“1”在第几位(从0开始),返回对应的UInt值。如果不是独热码,则行为不确定。例如:OHToUInt
OHToUInt("b1000".U) // 等于3.U
OHToUInt("b1000_0000".U) // 等于7.U
- 还有一个行为相反的单例对象
,它的apply方法是根据输入的UInt类型参数,返回对应位置的独热码,独热码也是UInt类型。例如:UIntToOH
UIntToOH(3.U) // 等于"b1000".U
UIntToOH(7.U) // 等于"b1000_0000".U
Ⅴ、无关位
Verilog里可以用问号表示无关位,那么用case语句进行比较时就不会关心这些位。Chisel里有对应的
BitPat
类,可以指定无关位。在其伴生对象里:
- 一个apply方法可以接收一个字符串来构造BitPat对象,字符串里用问号表示无关位。例如:
"b10101".U === BitPat("b101??") // 等于true.B
"b10111".U === BitPat("b101??") // 等于true.B
"b10001".U === BitPat("b101??") // 等于false.B
- 另一个apply方法则用UInt类型的参数来构造BitPat对象,UInt参数必须是字面量。这允许把UInt类型用在期望BitPat的地方,当用BitPat定义接口又并非所有情况要用到无关位时,该方法就很有用。
另外,
bitPatToUInt
方法可以把一个BitPat对象转换成UInt对象,但是BitPat对象不能包含无关位。
dontCare
方法接收一个Int类型的参数,构造等值位宽的全部无关位。例如:
Ⅵ、查找表
BitPat通常配合两种查找表使用。
- 一种是单例对象
,其apply方法定义为:Lookup
def apply[T <: Bits](addr: UInt, default: T, mapping: Seq[(BitPat, T)]): T
参数addr会与每个BitPat进行比较,如果相等,就返回对应的值,否则就返回default。
Lookup(2.U, // address for comparison
* 10.U, // default "row" if none of the following cases match
* Array(BitPat(2.U) -> 20.U, // this "row" hardware-selected based off address 2.U
* BitPat(3.U) -> 30.U)
* ) // hardware-evaluates to 20.U
- 第二种是单例对象
,它的apply方法与上面的类似,区别在于返回结果是一个T类型的列表:ListLookup
ListLookup(2.U, // address for comparison
* List(10.U, 11.U, 12.U), // default "row" if none of the following cases match
* Array(BitPat(2.U) -> List(20.U, 21.U, 22.U), // this "row" hardware-selected based off address 2.U
* BitPat(3.U) -> List(30.U, 31.U, 32.U))
* ) // hardware-evaluates to List(20.U, 21.U, 22.U)
这两种查找表的常用场景是构造CPU的控制器,因为CPU指令里有很多无关位,所以根据输入的指令(即addr)与预先定义好的带无关位的指令进行匹配,就能得到相应的控制信号。
Ⅶ、数据重复和位重复
单例对象
Fill
是对输入的数据进行重复,它的apply方法是:
def apply(n: Int, x: UInt): UInt
,第一个参数是重复次数,第二个是被重复的数据,返回的是UInt类型的数据,如下例所示:
Fill(2, "b1000".U) // 等于 "b1000 1000".U
Fill(2, "b1001".U) // 等于 "b1001 1001".U
Fill(2, myUIntWire) // 动态重复
还有一个单例对象
FillInterleaved
,它是对输入数据的每一位进行重复,它有两个apply方法:
第一个是
def apply(n: Int, in: Seq[Bool]): UInt
,n表示位重复的次数,in是被重复的数据,它是由Bool类型元素组成的序列,返回的是UInt类型的数据,如下例所示:
FillInterleaved(2, Seq(true.B, false.B, false.B, false.B)) // 等于 "b11 00 00 00".U
FillInterleaved(2, Seq(true.B, false.B, false.B, true.B)) // 等于 "b11 00 00 11".U
第二个是
def apply(n: Int, in: Seq[Bool]): UInt
,n表示位重复的次数,in是被重复的UInt类型的数据,返回的是UInt类型的数据,如下例所示:
FillInterleaved(2, "b1 0 0 0".U) // 等于 "b11 00 00 00".U
FillInterleaved(2, "b1 0 0 1".U) // 等于 "b11 00 00 11".U
FillInterleaved(2, myUIntWire) // 动态位重复