天天看点

chisel使用自定义/标准库中的函数简化设计(更新)

主体内容摘自: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)  // 动态计数
           
Ⅳ、独热码转换器
  • 单例对象

    OHToUInt

    的apply方法可以接收一个Bits类型或Bool序列类型的独热码参数,计算独热码里的“1”在第几位(从0开始),返回对应的UInt值。如果不是独热码,则行为不确定。例如:
OHToUInt("b1000".U)  // 等于3.U

OHToUInt("b1000_0000".U)  // 等于7.U 
           
  • 还有一个行为相反的单例对象

    UIntToOH

    ,它的apply方法是根据输入的UInt类型参数,返回对应位置的独热码,独热码也是UInt类型。例如:
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通常配合两种查找表使用。

  • 一种是单例对象

    Lookup

    ,其apply方法定义为:
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
           
  • 第二种是单例对象

    ListLookup

    ,它的apply方法与上面的类似,区别在于返回结果是一个T类型的列表:
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)  // 动态位重复