主體内容摘自: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) // 動态位重複