一、概述
????????Chisel(Constructing Hardware In a Scala Embedded Language)是一種嵌入在高級編程語言Scala的硬件構建語言。Chisel實際上只是一些特殊的類定義,預定義對象的集合,使用Scala的用法,所以在寫Chisel程序時實際上是在寫Scala程序。本文假設對Scala程序無任何了解,會通過一些Chisel的例子來說明某些重要的Scala特征,只使用本文介紹的東西也能完成一些的硬件設計。 當希望自己的代碼能夠更加簡化或提高復用性,你會發現有必要了解Scala語言的潛力。作者也在學習中,后續文章陸續介紹。
二、Chisel硬件表達
????????Chisel只支持二進制邏輯,不支持三態信號。
????????由于只有芯片對外的IO處才能出現三態門,所以內部設計幾乎用不到x和z。而且x和z在設計中會帶來危害,忽略掉它們也不影響大多數設計,還簡化了模型。當然,如果確實需要,可以通過黑盒語法與外部的Verilog代碼互動,也可以在下游工具鏈里添加四態邏輯。
三、Chisel數據類型
????????Chisel數據類型用于指定狀態元素中保存的值或wire上傳輸的值。
????????雖然硬件設計最終操作的是二進制數值向量,但對于值的其他抽象表示具有更清晰的規范,并且能夠幫助工具生成更優化的電路。
????????在Chisel中,原始比特集合可以用Bits類型來表示。帶符號和無符號整數被認為是定點數的子集,可以用SInt和UInt來表示。帶符號定點整數(包括整數)使用二進制補碼格式來表示。布爾值可以用Bool類型表示。注意,這些類型與Scala的內建類型不同,例如Int或Boolean。另外,Chisel定義了Bundle用來將值進行集合(類似于其他語言中的struct),還定義了Vec用來對值的集合進行索引。
????????常量或字面值使用Scala整數或傳遞給構造函數的字符串表示:
UInt(1) // decimal 1-bit lit from Scala Int.
UInt("ha") // hexadecimal 4-bit lit from string.
UInt("o12") // octal 4-bit lit from string.
UInt("b1010") // binary 4-bit lit from string.
SInt(5) // signed decimal 4-bit lit from Scala Int.
SInt(-8) // negative decimal 4-bit lit from Scala Int.
UInt(5) // unsigned decimal 3-bit lit from Scala Int.
Bool(true) // Bool lits from Scala lits.
Bool(false)
????????下劃線可以用作長字符串文字中的分隔符,以幫助可讀性,但在創建值時會被忽略,例如:
UInt("h_dead_beef") // 32-bit lit of type UInt
????????默認情況下,Chisel編譯器將每個常量的大小設置為保存常量所需的最小位數,包括帶符號類型的符號位。位寬也可以在字面上明確指定,如下所示:
UInt("ha", 8) // hexadecimal 8-bit lit of type UInt
UInt("o12", 6) // octal 6-bit lit of type UInt
UInt("b1010", 12) // binary 12-bit lit of type UInt
SInt(5, 7) // signed decimal 7-bit lit of type SInt
UInt(5, 8) // unsigned decimal 8-bit lit of type UInt
????????對于UInt類型值,值被零擴展到所需的位寬。對于類型為SInt的文字,該值被符號擴展以填充所需的位寬度。如果給定的位寬太小而不能容納參數值,則會生成Chisel錯誤。
四、組合電路
????????在Chisel中,電路會被表示為一張節點圖。每個節點是具有零個或多個輸入并驅動一個輸出的硬件運算符。
????????上面介紹的Uint是一種退化類型的節點,它沒有輸入,并且在其輸出上驅動一個恒定的值。
????????創建和連接節點的一種方法是使用字面表達式。例如,我們可以使用以下表達式來表示簡單的組合邏輯電路:
(a & b) | (~c & d)
????????語法應該看起來很熟悉,用&和|分別表示按位與和按位或,~表示按位非。a到d表示某些(未指定)寬度的命名導線。
????????任何簡單的表達式都可以直接轉換成電路樹,在葉子處使用命名的導線和操作符形成內部節點。表達式的電路輸出取自樹根處的運算符,在本示例中是按位或運算。
????????簡單表達式可以以樹的形式構建電路,但是如果想以任意有向非循環圖(DAG)的形式構建電路,我們需要描述扇出。在Chisel中,我們通過命名一根wire來表示一個子表達式,這樣我們就可以在后續表達式中多次引用。我們通過聲明變量來命名Chisel中的wire。例如,考慮如下示例的select表達式,它在后續的多選器描述中可以多次使用:
val sel = a | b
val out = (sel & in1) | (~sel & in0)
????????關鍵字val是Scala的一部分,用于命名具有不會再更改的值的變量。 在上面的例子中它命名了wire類型的sel,保存了第一個按位或運算符的輸出,以便輸出可在第二個表達式中多次使用。
五、內建操作符
????????Chisel定義了一組硬件操作符,如下表所示:
1、位寬接口
????????用戶需要設置端口和寄存器的位寬,除非用戶手動設置,否則編譯器會自動推測wire上的位寬。位寬推測引擎會從節點圖的輸入端口開始,并根據以下規則集從它們各自的輸入位寬度計算節點輸出位寬度:
?????????位寬推測過程會持續到沒有位寬改變。 除了通過已知固定數量的右移之外,位寬推測規定了輸出位寬度不能小于輸入位寬度,因此輸出位寬度增長或保持相同。 此外,寄存器的寬度必須由用戶明確地或根據復位值或下一個參數的位寬指定。根據這兩個要求,我們可以將位寬推測過程將收斂到一個固定點。
我們選擇的運算符名稱受到Scala語言的限制。所以我們必須使用===表示等于判斷邏輯和=/=表示不等判斷邏輯,這樣可以保持原生Scala相關運算符可用。
六、功能抽象
????????我們可以定義函數來分解一個重復的邏輯,這樣可以在后續設計中重復使用。例如,我們可以包裝一個簡單的組合邏輯塊:
def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt = (a & b) | (~c & d)
????????其中clb是表示以a,b,c,d為參數的函數,并返回一個布爾電路的輸出。?def關鍵字是Scala的一部分,表示引入了一個函數定義,每個語句后面跟一個冒號,然后是它的類型,函數返回類型在參數列表之后的冒號之后。(=)符號將函數參數列表與函數定義分隔開。
????????然后我們就可以在其他的電路中使用了:
val out = clb(a,b,c,d)
????????我們將在后面介紹許多酷炫的函數使用方法來構造硬件。
七、Bundles & Vecs
????????Bundle和Vec是可以允許用戶使用其他數據類型來擴展Chisel數據類型集合的類。
1、Bundles
????????Bundle可以將一些不同類型的命名字段組合成一個單元,類似于C語言中的struct。用戶可以通過將一個類定義為Bundle的子類來定義自己的bundle:
class MyFloat extends Bundle {val sign = Bool()val exponent = UInt(width = 8) val significand = UInt(width = 23)
}
val x = new MyFloat()
val xs = x.sign
????????scala約定將新類的名稱的首字母大寫,所以我們建議在Chisel中也遵循這個約定。 UInt構造函數的width命名參數指定類型中的位數。
2、Vecs
????????Vecs用來創建一個可索引的元素向量,其構造如下所示:
// Vector of 5 23-bit signed integers.
val myVec = Vec.fill(5){ SInt(width = 23) }
// Connect to one element of vector.
val reg3 = myVec(3)
????????注意,我們必須在花括號內指定Vec元素的類型,因為我們必須將位寬參數傳遞給SInt構造器。
????????原始類(SInt,UInt和Bool)加上聚合類(Bundles和Vecs)都繼承自一個公共的超類Data。在電路中,每個最終繼承自Data的對象都可以表示為一個位向量。
????????Bundle和Vec可以任意嵌套,從而構建復雜的數據結構:
class BigBundle extends Bundle {// Vector of 5 23-bit signed integers.val myVec = Vec.fill(5) { SInt(width = 23) } val flag = Bool()// Previously defined bundle.val f = new MyFloat()
}
八、端口
????????端口用作硬件組件的接口。一個端口可以是任意的Data對象,但它是具有方向的。
????????Chisel提供端口構造函數,以允許在構建時給對象添加(輸入或輸出)。原始的端口構造函數需要將方向作為第一個參數(方向為INPUT或OUTPUT),將位數作為第二個參數(除了始終為1位的布爾值)。
????????端口的聲明如下所示:
class Decoupled extends Bundle { val ready = Bool(OUTPUT)val data = UInt(INPUT, 32) val valid = Bool(INPUT)
}
????????Decoupled被定義后,它就會變成一個新的類型,可以根據需要用于模塊接口或命名的wire集合。
????????對象的方向也可以實例化時確定:
class ScaleIO extends Bundle {val in = new MyFloat().asInput val scale = new MyFloat().asInput val out = new MyFloat().asOutput
}
????????asInput和asOutput方法可以強制數據對象的所有模塊設置成對應的方向。
????????通過將方向折疊到對象聲明中,Chisel能夠提供強大的布線能力,稍后會詳細介紹。
九、Modules
????????Chisel 模塊與 Verilog 模塊非常相似,在生成的電路中定義層次結構。 層次化的命名空間可在下游工具中訪問,以幫助調試和物理布局。
????????用戶定義的模塊被定義為一個類,它:
- 繼承自 Module,
- 包含一個存儲在名為 io 的端口字段中的接口
- 在其構造函數中將子電路連接在一起。
?????????例如,將雙輸入多路復用器定義為一個模塊:
class Mux2 extends Module {
val io = new Bundle{
val sel = UInt(INPUT, 1)
val in0 = UInt(INPUT, 1)
val in1 = UInt(INPUT, 1)
val out = UInt(OUTPUT, 1)
}
io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}
????????模塊的接線接口是Bundle的端口集合。 模塊的接口是通過名為 io 的字段定義的。 對于 Mux2,io 被定義為具有四個字段的包,每個多路復用器端口一個。
????????:= 賦值運算符,在定義的主體中使用,是 Chisel 中的一個特殊運算符,它將左側的輸入連接到右側的輸出。?
????????我們現在可以構建電路層次,我們可以從較小的子模塊開開始構建更大的模塊。例如,我們可以通過將三個2輸入多路選擇器連接在一起,構建一個4輸入多路選擇器模塊:
class Mux4 extends Module { val io = new Bundle {val in0 = UInt(INPUT, 1) val in1 = UInt(INPUT, 1) val in2 = UInt(INPUT, 1) val in3 = UInt(INPUT, 1) val sel = UInt(INPUT, 2) val out = UInt(OUTPUT, 1)}val m0 = Module(new Mux2())m0.io.sel := io.sel(0)m0.io.in0 := io.in0; m0.io.in1 := io.in1val m1 = Module(new Mux2())m1.io.sel := io.sel(0)m1.io.in0 := io.in2; m1.io.in1 := io.in3val m3 = Module(new Mux2())m3.io.sel := io.sel(1)m3.io.in0 := m0.io.out; m3.io.in1 := m1.io.outio.out := m3.io.out
}