天天看點

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)  // 動态位重複