原文鏈接
https://blog.csdn.net/qq_34291505/article/details/87365907
第十七章 Chisel基礎——數據類型
為了從Chisel轉變成Verilog,語言開發人員開發了一個中間的標准交換格式——Firrtl,它跟Vrilog是同一級別的,兩者都比Chisel低一級。編寫的Chisel代碼首先會經過Firrtl編譯器,生成Firrtl代碼,也就是一個后綴格式為“.fir”的文件,然后由這個Firrtl文件再去生成對應的Verilog代碼。如果讀者有興趣看一看Firrtl的格式,其實與Verilog很接近,只不過是由機器生成的、很死板的代碼。Firrtl編譯器也並不是只針對Chisel
Verilog的最初目的是用於電路驗證,所以它有很多不可綜合的語法。Firrtl在轉變成Verilog時,只會采用可綜合的語法,因此讀者完全不用擔心用Chisel寫出來的電路不可綜合。只要能正確生成Verilog,那就能被綜合器生成電路
Chisel目前不支持四態邏輯里的x和z,只支持0和1。由於只有芯片對外的IO處才能出現三態門,所以內部設計幾乎用不到x和z。而且x和z在設計中會帶來危害,忽略掉它們也不影響大多數設計,還簡化了模型。當然,如果確實需要,可以通過黑盒語法與外部的Verilog代碼互動,也可以在下游工具鏈里添加四態邏輯
Chisel會對未被驅動的輸出型端口和線網進行檢測,如果存在,會進行報錯
Chisel的代碼包並不會像Scala的標准庫那樣被編譯器隱式導入,所以每個Chisel文件都應該在開頭至少寫一句“import chisel3._”。這個包包含了基本的語法,對於某些高級語法,則可能需要“import chisel3.util._”、“import chisel3.experimental._”、 “import chisel3.testers._”等等
Chisel的數據類型
hisel定義了自己的一套數據類型,讀者應該跟Scala的九種基本值類區分開來。而且Chisel也能使用Scala的數據類型,但是Scala的數據類型都是用於參數和內建控制結構,構建硬件電路還是得用Chisel自己的數據類型,在使用時千萬不要混

在實際硬件構成里,並不會用到Data,讀者也不用關心它的具體實現細節,應該關注Data類的兩大子類:聚合類Aggregate和元素類Element
聚合類Aggregate的常用子類是向量類Vec[T]和包裹類Bundle
Vec[T]類用於包含相同的元素,元素類型T可以是任意的Data子類。
Bundle類用於被自定義的類繼承,這樣自定義的類就能包含任意Data的子類對象,常用於協助構造模塊的端口,故而衍生出了一些預定義的端口子類
Element類衍生出了Analog、Bits和Clock三個子類,單例對象DontCare和特質Reset
Analog用於在黑盒中模擬inout端口,目前在實際Chisel里並無其他用途。Bits類的兩個子類SInt和UInt是最常用的兩個數據類型,它們是用補碼表示的有符號整數和無符號整數。不僅用來協助定義端口位寬,還用來進行賦值
FixedPoint類提供的API帶有試驗性質,而且將來可能會發生改變,所以不常用。Bool類是Chisel自己的布爾類型,區別於Scala的Boolean。Bool類是UInt類的子類
Clock類表示時鍾,Chisel里的時鍾是專門的一個類型,並不像Verilog里那樣是1bit的線網。復位類型Reset也是如此。單例對象DontCare用於賦值給未驅動的端口或線網,防止編譯器報錯
數據字面量
能夠表示具體值的數據類型為UInt、SInt和Bool。實際可綜合的電路都是若干個bit,所以只能表示整數,這與Verilog是一致的。要表示浮點數,本質還是用多個bit來構建,而且要遵循IEEE的浮點標准。對於UInt,可以構成任意位寬的線網或寄存器。對於SInt,在Chisel里會按補碼解讀,轉換成Verilog后會使用系統函數$signed,這是可綜合的。對於Bool,轉換成Verilog后就是1bit的線網或寄存器。
Chisel定義了一系列隱式類:fromBigIntToLiteral、fromtIntToLiteral、fromtLongToLiteral、fromStringToLiteral、fromBooleanToLiteral。回顧前面講述的隱式類的內容,也就是會有相應的隱式轉換。以隱式類fromtIntToLiteral為例,存在一個同名的隱式轉換,把相應的Scala的Int對象轉換成一個fromtIntToLiteral的對象。而fromtIntToLiteral類有兩個方法U和S,分別構造一個等值的UInt對象和SInt對象。再加上Scala的基本值類都是用字面量構造對象,所以要表示一個UInt對象,可以寫成“1.U”的格式,這樣編譯器會插入隱式轉換,變成“fromtIntToLiteral(1).U”,進而構造出字面值為“1”的UInt對象。同理,也可以構造SInt。還有相同行為的方法asUInt和asSInt。
1.U // 字面值為“1”的UInt對象
-8.S // 字面值為“-8”的SInt對象
"b0101".U // 字面值為“5”的UInt對象
true.B // 字面值為“true”的Bool對象
數據寬度
Chisel3專門設計了寬度類Width。還有一個隱式類fromIntToWidth,就是把Int對象轉換成fromIntToWidth類型的對象,然后通過方法W返回一個Width對象
1.U // 字面值為“1”、寬度為1bit的UInt對象 1.U(32.W) // 字面值為“1”、寬度為32bit的UInt對象
UInt、SInt和Bool都不是抽象類,除了可以通過字面量構造對象以外,也可以直接通過apply工廠方法構造沒有字面量的對象
有字面量的數據類型用於賦值、初始化寄存器等操作,而無字面量的數據類型則用於聲明端口、構造向量等
類型轉換
UInt、SInt和Bool三個類都包含四個方法:asUInt、asSInt、toBool和toBools
toBool會把1bit的“1”轉換成Bool類型的true,“0”轉換成false。如果位寬超過1bit,則用toBools轉換成Bool類型的序列Seq[Bool]
Bool類還有一個方法asClock,把true轉換成電壓常高的時鍾,false轉換成電壓常低的時鍾。Clock類只有一個方法asUInt,轉換成對應的0或1
向量
如果需要一個集合類型的數據,除了可以使用Scala內建的數組、列表、集等數據結構外,還可以使用Chisel專屬的Vec[T]。T必須是Data的子類,而且每個元素的類型、位寬必須一樣。Vec[T]的伴生對象里有一個apply工廠方法,接收兩個參數,第一個是Int類型,表示元素的個數,第二個是元素
val myVec = Wire(Vec(3, UInt(32.W)))
val myReg = myVec(0)
還有一個工廠方法VecInit[T],通過接收一個Seq[T]作為參數來構造向量,或者是多個重復參數。不過,這個工廠方法常把有字面值的數據作為參數,用於初始化寄存器組、ROM、RAM等,或者用來構造多個模塊。
因為Vec[T]也是一種序列,所以它也定義了諸如map、flatMap、zip、foreach、filter、exists、contains等方法。盡管這些方法應該出現在軟件里,但是它們也可以簡化硬件邏輯的編寫,減少手工代碼量。
混合向量
混合向量MixedVec[T]與普通的向量Vec[T]類似,只不過包含的元素可以不全都一樣
對於構造Vec[T]和MixedVec[T]的序列,並不一定要逐個手寫,可以通過Scala的函數,比如fill、map、flatMap、to、until等來生成
val mixVec = Wire(MixedVec((1 to 10) map { i => UInt(i.W) }))
包裹
抽象類Bundle很像C語言的結構體(struct),用戶可以編寫一個自定義類來繼承自它,然后在自定義的類里包含其它各種Data類型的字段。它可以協助構建線網或寄存器,但是最常見的用途是用於構建一個模塊的端口列表,或者一部分端口
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(32.W))
val out = Output(UInt(32.W))
})
Chisel的內建操作符



位寬推斷

第十八章 Chisel基礎——模塊與硬件類型
實際的電路應該是由硬件類型的對象構成的,不管是信號的聲明,還是用賦值進行信號傳遞,都是由硬件類型的對象來完成的。數據類型和硬件類型融合在一起,才能構成完整、可運行的組件。
Chisel是如何賦值的
在Chisel里,所有對象都應該由val類型的變量來引用,因為硬件電路的不可變性。
引用的對象很可能需要被重新賦值。例如,輸出端口在定義時使用了“=”與端口變量名進行了綁定,那等到驅動該端口時,就需要通過變量名來進行賦值操作,更新數據
Chisel類都定義了方法“:=”,作為等號賦值的代替。所以首次創建變量時用等號初始化,如果變量引用的對象不能立即確定狀態或本身就是可變對象,則在后續更新狀態時應該用“:=”
val x = Wire(UInt(4.W)) val y = Wire(UInt(4.W)) x := "b1010".U // 向4bit的線網x賦予了無符號數10 y := ~x // 把x按位取反,傳遞給y
端口
定義端口列表
整個端口列表是由方法“IO[T <: Data](iodef: T)”來定義的,通常其參數是一個Bundle類型的對象,而且引用的字段名稱必須是“io”。因為端口存在方向,所以還需要方法“Input[T <: Data](source: T)”和“Output[T <: Data](source: T)”來為每個端口表明具體的方向。
目前Chisel還不支持雙向端口inout,只能通過黑盒里的Analog端口來模擬外部Verilog的雙向端口
class MyIO extends Bundle { val in = Input(Vec(5, UInt(32.W))) val out = Output(UInt(32.W)) } ...... val io = IO(new MyIO) // 模塊的端口列表 ......
翻轉端口列表的方向
對於兩個相連的模塊,可能存在大量同名但方向相反的端口。僅僅為了翻轉方向而不得不重寫一遍端口顯得費時費力,所以Chisel提供了“Flipped[T <: Data](source: T)”方法,可以把參數里所有的輸入轉輸出,輸出轉輸入。如果是黑盒里的Analog端口,則仍是雙向的。
class MyIO extends Bundle { val in = Input(Vec(5, UInt(32.W))) val out = Output(UInt(32.W)) } ...... val io = IO(new MyIO) // in是輸入,out是輸出 ...... val io = IO(Flipped(new MyIO)) // out是輸入,in是輸出
整體連接
翻轉方向的端口列表通常配合整體連接符號“<>”使用。該操作符會把左右兩邊的端口列表里所有同名的端口進行連接,而且同一級的端口方向必須是輸入連輸出、輸出連輸入,父級和子級的端口方向則是輸入連輸入、輸出連輸出。
方向必須按這個規則匹配,而且不能存在端口名字、數量、類型不同的情況。這樣就省去了大量連線的代碼
模塊
定義模塊
Chisel里面是用一個自定義的類來定義模塊的,這個類有以下三個特點:
①繼承自Module類。
②有一個抽象字段“io”需要實現,該字段必須引用前面所說的端口對象。
③在類的主構造器里進行內部電路連線。
這樣定義的模塊會繼承一個字段“clock”,類型是Clock,它表示全局時鍾,在整個模塊內都可見。對於組合邏輯,是用不上它的,而時序邏輯雖然需要這個時鍾,但也不用顯式聲明。還有一個繼承的字段“reset”,類型是Reset,表示全局復位信號,在整個模塊內可見。對於需要復位的時序元件,也可以不用顯式使用該字段。
// mux2.scala package test 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) }
“new Bundle { ... }”的寫法是聲明一個匿名類繼承自Bundle,然后實例化匿名類。對於短小、簡單的端口列表,可以使用這種簡便寫法。對於大的公用接口,應該單獨寫成具名的Bundle子類,方便修改。“io.out := ...”其實就是主構造方法的一部分
例化模塊
並不是直接用new生成一個實例對象就完成了,還需要再把實例的對象傳遞給單例對象Module的apply方法。
val m0 = Module(new Mux2)
例化多個模塊
對於要多次例化的重復模塊,可以利用向量的工廠方法VecInit[T <: Data]。
所以可以把待例化模塊的io字段組成一個序列
生成序列的一種方法是調用單例對象Seq里的方法fill,該方法的一個重載版本有兩個單參數列表,第一個接收Int類型的對象,表示序列的元素個數,第二個是傳名參數,接收序列的元素。
// mux4_2.scala package test import chisel3._ class Mux4_2 extends Module { val io = IO(new Bundle { 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 sel = Input(UInt(2.W)) val out = Output(UInt(1.W)) }) val m = VecInit(Seq.fill(3)(Module(new Mux2).io)) // 例化了三個Mux2,並且參數是端口字段io m(0).sel := io.sel(0) // 模塊的端口通過下標索引,並且路徑里沒有“io” m(0).in0 := io.in0 m(0).in1 := io.in1 m(1).sel := io.sel(0) m(1).in0 := io.in2 m(1).in1 := io.in3 m(2).sel := io.sel(1) m(2).in0 := m(0).out m(2).in1 := m(1).out io.out := m(2).out }
線網
Chisel把線網作為電路的節點,通過工廠方法“Wire[T <: Data](t: T)”來定義。可以對線網進行賦值,也可以連接到其他電路節點,這是組成組合邏輯的基本硬件類型。
val myNode = Wire(UInt(8.W))
myNode := 0.U
因為Scala作為軟件語言是順序執行的,定義具有覆蓋性,所以如果對同一個線網多次賦值,則只有最后一次有效
寄存器
如果模塊里沒有多時鍾域的語句塊,那么寄存器都是由隱式的全局時鍾來控制。對於有復位信號的寄存器,如果不在多時鍾域語句塊里,則由隱式的全局復位來控制,並且高有效
目前Chisel所有的復位都是同步復位,異步復位功能還在開發中。如果需要異步復位寄存器,則需要通過黑盒引入
有五種內建的寄存器,
第一種是跟隨寄存器“RegNext[T <: Data](next: T)”,在每個時鍾上升沿,它都會采樣一次傳入的參數,並且沒有復位信號。它的另一個版本的apply工廠方法是“RegNext[T <: Data](next: T, init: T)”,也就是由復位信號控制,當復位信號有效時,復位到指定值,否則就跟隨。
第二種是復位到指定值的寄存器“RegInit[T <: Data](init: T)”,參數需要聲明位寬,否則就是默認位寬。可以用內建的when語句進行條件賦值。
第三種是普通的寄存器“Reg[T <: Data](t: T)”,它可以在when語句里用全局reset信號進行同步復位(reset信號是Reset類型,要用toBool進行類型轉換),也可以進行條件賦值或無條件跟隨。參數同樣要指定位寬。
第四種是util包里的帶一個使能端的寄存器“RegEnable[T <: Data](next: T, init: T, enable: Bool)”,如果不需要復位信號,則第二個參數可以省略給出。
第五種是util包里的移位寄存器“ShiftRegister[T <: Data](in: T, n: Int, resetData: T, en: Bool)”,其中第一個參數in是帶移位的數據,第二個參數n是需要延遲的周期數,第三個參數resetData是指定的復位值,可以省略,第四個參數en是使能移位的信號,默認為true.B。
// reg.scala package test import chisel3._ import chisel3.util._ class REG extends Module { val io = IO(new Bundle { val a = Input(UInt(8.W)) val en = Input(Bool()) val c = Output(UInt(1.W)) }) val reg0 = RegNext(io.a) val reg1 = RegNext(io.a, 0.U) val reg2 = RegInit(0.U(8.W)) val reg3 = Reg(UInt(8.W)) val reg4 = Reg(UInt(8.W)) val reg5 = RegEnable(io.a + 1.U, 0.U, io.en) val reg6 = RegEnable(io.a - 1.U, io.en) val reg7 = ShiftRegister(io.a, 3, 0.U, io.en) val reg8 = ShiftRegister(io.a, 3, io.en) reg2 := io.a.andR reg3 := io.a.orR when(reset.toBool) { reg4 := 0.U } .otherwise { reg4 := 1.U } io.c := reg0(0) & reg1(0) & reg2(0) & reg3(0) & reg4(0) & reg5(0) & reg6(0) & reg7(0) & reg8(0) }
寄存器組
如果把子類型Vec[T]作為參數傳遞進去,就會生成多個位寬相同、行為相同、名字前綴相同的寄存器。同樣,寄存器組在Chisel代碼里可以通過下標索引。
// reg2.scala package test import chisel3._ import chisel3.util._ class REG2 extends Module { val io = IO(new Bundle { val a = Input(UInt(8.W)) val en = Input(Bool()) val c = Output(UInt(1.W)) }) val reg0 = RegNext(VecInit(io.a, io.a)) val reg1 = RegNext(VecInit(io.a, io.a), VecInit(0.U, 0.U)) val reg2 = RegInit(VecInit(0.U(8.W), 0.U(8.W))) val reg3 = Reg(Vec(2, UInt(8.W))) val reg4 = Reg(Vec(2, UInt(8.W))) val reg5 = RegEnable(VecInit(io.a + 1.U, io.a + 1.U), VecInit(0.U(8.W), 0.U(8.W)), io.en) val reg6 = RegEnable(VecInit(io.a - 1.U, io.a - 1.U), io.en) val reg7 = ShiftRegister(VecInit(io.a, io.a), 3, VecInit(0.U(8.W), 0.U(8.W)), io.en) val reg8 = ShiftRegister(VecInit(io.a, io.a), 3, io.en) reg2(0) := io.a.andR reg2(1) := io.a.andR reg3(0) := io.a.orR reg3(1) := io.a.orR when(reset.toBool) { reg4(0) := 0.U reg4(1) := 0.U } .otherwise { reg4(0) := 1.U reg4(1) := 1.U } io.c := reg0(0)(0) & reg1(0)(0) & reg2(0)(0) & reg3(0)(0) & reg4(0)(0) & reg5(0)(0) & reg6(0)(0) & reg7(0)(0) & reg8(0)(0) & reg0(1)(0) & reg1(1)(0) & reg2(1)(0) & reg3(1)(0) & reg4(1)(0) & reg5(1)(0) & reg6(1)(0) & reg7(1)(0) & reg8(1)(0) }
用when給電路賦值
由於Scala已經占用了“if…else if…else”語法,所以相應的Chisel控制結構改成了when語句,其語法如下:
when用於給帶使能信號的寄存器更新數據,組合邏輯不常用。對於有復位信號的寄存器,推薦使用RegInit來聲明,這樣生成的Verilog會自動根據當前的時鍾域來同步復位,盡量不要在when語句里用“reset.toBool”作為復位條件
除了when結構,util包里還有一個與之對偶的結構“unless”,如果unless的判定條件為false.B則一直執行,否則不執行
數據類型與硬件類型的區別
hisel的數據類型,其中常用的就五種:UInt、SInt、Bool、Bundle和Vec[T]。本章介紹了硬件類型,最基本的是IO、Wire和Reg三種,還有指明端口方向的Input、Output和Flipped
在編寫Chisel時,要注意哪些地方是數據類型,哪些地方又是硬件類型。這時,靜態語言的優勢便體現出來了,因為編譯器會幫助程序員檢查類型是否匹配。如果在需要數據類型的地方出現了硬件類型、在需要硬件類型的地方出現了數據類型
為VecInit專門接收硬件類型的參數來構造硬件向量,給VecInit傳入數據類型反而會報錯
第十九章 Chisel基礎——常用的硬件原語
Chisel在語言庫里定義了很多常用的硬件原語,讀者可以直接導入相應的包來使用
多路選擇器
第一種形式是二輸入多路選擇器“Mux(sel, in1, in2)”。sel是Bool類型,in1和in2的類型相同,都是Data的任意子類型。當sel為true.B時,返回in1,否則返回in2
所以Mux可以內嵌Mux,Mux(c1, a, Mux(c2, b, Mux(..., default)))
第二種就是針對上述n輸入多路選擇器的簡便寫法,形式為“MuxCase(default, Array(c1 -> a, c2 -> b, ...))”,它的展開與嵌套的Mux是一樣的。第一個參數是默認情況下返回的結果,第二個參數是一個數組,數組的元素是對偶“(成立條件,被選擇的輸入)”。MuxCase在chisel3.util包里
第三種是MuxCase的變體,它相當於把MuxCase的成立條件依次換成從0開始的索引值,就好像一個查找表,其形式為“MuxLookup(idx, default, Array(0.U -> a, 1.U -> b, ...))”。它的展開相當於“MuxCase(default, Array((idx === 0.U) -> a, (idx === 1.U) -> b, ...))”。MuxLookup也在chisel3.util包里。
第四種是chisel3.util包里的獨熱碼多路選擇器,它的選擇信號是一個獨熱碼。如果零個或多個選擇信號有效,則行為未定義
val hotValue = Mux1H(Seq( io.selector(0) -> 2.U, io.selector(1) -> 4.U, io.selector(2) -> 8.U, io.selector(4) -> 11.U ))
ROM
可以通過工廠方法“VecInit[T <: Data](elt0: T, elts: T*)”或“VecInit[T <: Data](elts: Seq[T])”來創建一個只讀存儲器,參數就是ROM里的常量數值,對應的Verilog代碼就是給讀取ROM的線網或寄存器賦予常量值
// rom.scala package test import chisel3._ class ROM extends Module { val io = IO(new Bundle { val sel = Input(UInt(2.W)) val out = Output(UInt(8.W)) }) val rom = VecInit(1.U, 2.U, 3.U, 4.U) io.out := rom(io.sel) }
RAM
Chisel支持兩種類型的RAM。第一種RAM是同步(時序)寫,異步(組合邏輯)讀,通過工廠方法“Mem[T <: Data](size: Int, t: T)”來構建
val asyncMem = Mem(16, UInt(32.W))
由於現代的FPGA和ASIC技術已經不再支持異步讀RAM,所以這種RAM會被綜合成寄存器陣列。第二種RAM則是同步(時序)讀、寫,通過工廠方法“SyncReadMem[T <: Data](size: Int, t: T)”來構建,這種RAM會被綜合成實際的SRAM
val syncMem = SyncReadMem(16, UInt(32.W))
寫RAM的語法是
when(wr_en) {
mem.write(address, dataIn)
out := DontCare
}
讀RAM的語法是
out := mem.read(address, rd_en)
帶寫掩模的RAM
RAM通常都具備按字節寫入的功能,比如數據寫入端口的位寬是32bit,那么就應該有4bit的寫掩模信號,只有當寫掩模比特有效時,對應的字節才會寫入。Chisel也具備構建帶寫掩模的RAM的功能。
而write方法有一個重載版本,就是第三個參數是接收寫掩模信號的。當下標為0的寫掩模比特是true.B時,最低的那個字節會被寫入,依次類推。下面是一個帶寫掩模的單端口RAM
// maskram.scala package test import chisel3._ import chisel3.util._ class MaskRAM extends Module { val io = IO(new Bundle { val addr = Input(UInt(10.W)) val dataIn = Input(UInt(32.W)) val en = Input(Bool()) val we = Input(UInt(4.W)) val dataOut = Output(UInt(32.W)) }) val dataIn_temp = Wire(Vec(4, UInt(8.W))) val dataOut_temp = Wire(Vec(4, UInt(8.W))) val mask = Wire(Vec(4, Bool())) val syncRAM = SyncReadMem(1024, Vec(4, UInt(8.W))) when(io.en) { syncRAM.write(io.addr, dataIn_temp, mask) dataOut_temp := syncRAM.read(io.addr) } .otherwise { dataOut_temp := DontCare } for(i <- 0 until 4) { dataIn_temp(i) := io.dataIn(8*i+7, 8*i) mask(i) := io.we(i).toBool io.dataOut := Cat(dataOut_temp(3), dataOut_temp(2), dataOut_temp(1), dataOut_temp(0)) } }
從文件讀取數據到RAM
在experimental包里有一個單例對象loadMemoryFromFile,它的apply方法可以在Chisel層面上從txt文件讀取數據到RAM里、、
MemBase[T]類型的,也就是Mem[T]和SyncReadMem[T]的超類,該參數接收一個自定義的RAM對象。第二個參數是文件的名字及路徑,用字符串表示。第三個參數表示讀取的方式為十六進制或二進制,默認是MemoryLoadFileType.Hex,也可以改成MemoryLoadFileType.Binary。注意,沒有十進制和八進制
計數器
Chisel在util包里定義了一個自增計數器原語Counter,它的工廠方法接收兩個參數:第一個參數是Bool類型的使能信號,為true.B時計數器從0開始每個時鍾上升沿加1自增,為false.B時則計數器保持不變;第二個參數需要一個Int類型的具體正數,當計數到該值時歸零。該方法返回一個二元組,其第一個元素是計數器的計數值,第二個元素是判斷計數值是否等於期望值的結果。
// counter.scala
package test
import chisel3._
import chisel3.util._
class MyCounter extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val out = Output(UInt(8.W))
val valid = Output(Bool())
})
val (a, b) = Counter(io.en, 233)
io.out := a
io.valid := b
}
16位線性反饋移位寄存器
如果要產生偽隨機數,可以使用util包里的16位線性反饋移位寄存器原語LFSR16,它接收一個Bool類型的使能信號,用於控制寄存器是否移位,缺省值為true.B。它返回一個UInt(16.W)類型的結果。
// lfsr.scala package test import chisel3._ import chisel3.util._ class LFSR extends Module { val io = IO(new Bundle { val en = Input(Bool()) val out = Output(UInt(16.W)) }) io.out := LFSR16(io.en) }
狀態機
Chisel沒有直接構建狀態機的原語。不過,util包里定義了一個Enum特質及其伴生對象。伴生對象里的apply方法定義如下
def apply(n: Int): List[UInt]
參數n返回對應元素數的List[UInt],每個元素都是不同的,所以可以作為枚舉值來使用。最好把枚舉狀態的變量名也組成一個列表,然后用列表的模式匹配來進行賦值。有了枚舉值后,可以通過“switch…is…is”語句來使用
// fsm.scala package test import chisel3._ import chisel3.util._ class DetectTwoOnes extends Module { val io = IO(new Bundle { val in = Input(Bool()) val out = Output(Bool()) }) val sNone :: sOne1 :: sTwo1s :: Nil = Enum(3) val state = RegInit(sNone) io.out := (state === sTwo1s) switch (state) { is (sNone) { when (io.in) { state := sOne1 } } is (sOne1) { when (io.in) { state := sTwo1s } .otherwise { state := sNone } } is (sTwo1s) { when (!io.in) { state := sNone } } } }
枚舉狀態名的首字母要小寫,這樣Scala的編譯器才能識別成變量模式匹配。
Chisel基礎——生成Verilog與基本測試
把一個Chisel模塊編譯成Verilog代碼,並進一步使用Verilator做一些簡單的測試
生成Verilog
生成Verilog的程序自然是在主函數里例化待編譯的模塊,然后運行這個主函數。例化待編譯模塊需要特殊的方法調用。chisel3包里有一個單例對象Driver,它包含一個方法execute,該方法接收兩個參數,第一個參數是命令行傳入的實參即字符串數組args,第二個是返回待編譯模塊的對象的無參函數。運行這個execute方法,就能得到Verilog代碼。
接着,讀者需要在src/test/scala文件夾下編寫對應的主函數文件
// fullAdderGen.scala package test object FullAdderGen extends App { chisel3.Driver.execute(args, () => new FullAdder) }
在這個主函數里,只有一個execute函數的調用,第一個參數固定是“args”,第二個參數則是無參的函數字面量“() => new FullAdder”。因為Chisel的模塊本質上還是Scala的class,所以只需用new構造一個對象作為返回結果即可。主函數里可以包括多個execute函數,也可以包含其它代碼。還有一點要注意的是,建議把設計文件和主函數放在一個包里,比如這里的“package test”,這樣省去了編寫路徑的麻煩。
要運行這個主函數,需要在build.sbt文件所在的路徑下打開終端,然后執行命令
:~/chisel-template$ sbt 'test:runMain test.FullAdderGen'
sbt后面有空格,再后面的內容都是被單引號對或雙引號對包起來。其中,test:runMain是讓sbt執行主函數的命令,而test.FullAdderGen就是要執行的那個主函數
終端的路徑下就會生成三個文件:FullAdder.anno.json、FullAdder.fir和FullAdder.v。
第二個后綴為“.fir”的文件就是對應的Firrtl代碼,第三個自然是對應的Verilog文件。
在命令里增加參數
給Firrtl傳遞參數
命令后面繼續增加可選的參數。例如,增加參數“--help”查看幫助菜單
最常用的是參數“-td”,可以在后面指定一個文件夾,這樣之前生成的三個文件就在該文件夾里,而不是在當前路徑下
給主函數傳遞參數
Scala的類可以接收參數,自然Chisel的模塊也可以接收參數。假設要構建一個n位的加法器,具體位寬不確定,根據需要而定。那么,就可以把端口位寬參數化,例化時傳入想要的參數即可。
package test import chisel3._ class Adder(n: Int) extends Module { val io = IO(new Bundle { val a = Input(UInt(n.W)) val b = Input(UInt(n.W)) val s = Output(UInt(n.W)) val cout = Output(UInt(1.W)) }) io.s := (io.a +& io.b)(n-1, 0) io.cout := (io.a +& io.b)(n) } // adderGen.scala package test object AdderGen extends App { chisel3.Driver.execute(args, () => new Adder(args(0).toInt)) }
比如例子中的主函數期望第一個參數即args(0)是一個數字字符串,這樣就能通過方法toInt轉換成Adder所需的參數。
~/chisel-template$ sbt 'test:runMain test.AdderGen 8 -td ./generated/adder'
編寫簡單的測試
Chisel的測試有兩種,第一種是利用Scala的測試來驗證Chisel級別的代碼邏輯有沒有錯誤。因為這部分內容比較復雜,而且筆者目前也沒有深入學習有關Scala測試的內容,所以這部分內容可有讀者自行選擇研究。第二種是利用Chisel庫里的peek和poke函數,給模塊的端口加激勵、查看信號值,並交由下游的Verilator來仿真、產生波形。這種方式比較簡單,類似於Verilog的testbench,適合小型電路的驗證。對於超大型的系統級電路,最好還是生成Verilog,交由成熟的EDA工具,用UVM進行驗證。
要編寫一個簡單的testbench,首先也是定義一個類,這個類的主構造方法接收一個參數,參數類型就是待測模塊的類名。其次,這個類繼承自PeekPokeTester類,並且把接收的待測模塊也傳遞給此超類。最后,測試類內部有四種方法可用:①“poke(端口,激勵值)”方法給相應的端口添加想要的激勵值,激勵值是Int類型的;②“peek(端口)”方法返回相應的端口的當前值;③“expect(端口,期望值)”方法會對第一個參數(端口)使用peek方法,然后與Int類型的期望值進行對比,如果兩者不相等則出錯;④“step(n)”方法則讓仿真前進n個時鍾周期。
package test import scala.util._ import chisel3.iotesters._ class AdderTest(c: Adder) extends PeekPokeTester(c) { val randNum = new Random for(i <- 0 until 10) { val a = randNum.nextInt(256) val b = randNum.nextInt(256) poke(c.io.a, a) poke(c.io.b, b) step(1) expect(c.io.s, (a + b) & 0xff) expect(c.io.cout, ((a + b) & 0x100) >> 8) } }
第一個包scala.util里包含了Scala生成偽隨機數的類Random,第二個包chisel3.iotesters包含了測試類PeekPokeTester
運行測試
自然也是通過主函數,但是這次是使用iotesters包里的execute方法。該方法與前面生成Verilog的方法類似,僅僅是多了一個參數列表,多出的第二個參數列表接收一個返回測試類的對象的函數:
// addertest.scala object AdderTestGen extends App { chisel3.iotesters.Driver.execute(args, () => new Adder(8))(c => new AdderTest(c)) }
~/chisel-template$ sbt 'test:runMain test.AdderTestGen -td ./generated/addertest --backend-name verilator'
執行成功后,就能在相應文件夾里看到一個新生成的文件夾,里面是仿真生成的文件。其中,“Adder.vcd”文件就是波形文件,使用GTKWave軟件打開就能查看,將相應的端口拖拽到右側就能顯示波形。
第二十一章 Chisel基礎——黑盒
例化黑盒
如果定義Dut類時,不是繼承自Module,而是繼承自BlackBox,則允許只有端口定義,也只需要端口定義。此外,在別的模塊里例化黑盒時,編譯器不會給黑盒的端口名加上“io_”
// blackbox.scala package test import chisel3._ class Dut extends BlackBox { val io = IO(new Bundle { val a = Input(UInt(32.W)) val clk = Input(Clock()) val reset = Input(Bool()) val b = Output(UInt(4.W)) }) } class UseDut extends Module { val io = IO(new Bundle { val toDut_a = Input(UInt(32.W)) val toDut_b = Output(UInt(4.W)) }) val u0 = Module(new Dut) u0.io.a := io.toDut_a u0.io.clk := clock u0.io.reset := reset io.toDut_b := u0.io.b } object UseDutTest extends App { chisel3.Driver.execute(args, () => new UseDut) }
BlackBox的構造方法可以接收一個Map[String, Param]類型的參數,這會使得例化外部的Verilog模塊時具有配置模塊的“#(參數配置)”。映射的鍵固定是字符串類型,它對應Verilog里聲明的參數名;映射的值對應傳入的配置參數,可以是字符串,也可以是整數和浮點數。雖然值的類型是Param,這是一個Chisel的印章類,但是單例對象chisel3.experimental里定義了相應的隱式轉換,可以把BigInt、Int、Long、Double和String轉換成對應的Param類型
... import chisel3.experimental._ class Dut extends BlackBox(Map("DATA_WIDTH" -> 32, "MODE" -> "Sequential", "RESET" -> "Asynchronous")) { val io = IO(new Bundle { val a = Input(UInt(32.W)) val clk = Input(Clock()) val reset = Input(Bool()) val b = Output(UInt(4.W)) }) } ..
復制Verilog文件
chisel3.util包里有一個特質HasBlackBoxResource,如果在黑盒類里混入這個特質,並且在src/main/resources文件夾里有對應的Verilog源文件,那么在Chisel轉換成Verilog時,就會把Verilog文件一起復制到目標文件夾。
...
import chisel3.util._
class Dut extends BlackBox with HasBlackBoxResource {
val io = IO(new Bundle {
val a = Input(UInt(32.W))
val clk = Input(Clock())
val reset = Input(Bool())
val b = Output(UInt(4.W))
})
setResource("/dut.v")
}
注意,相比一般的黑盒,除了端口列表的聲明,還多了一個特質里的setResource方法的調用。方法的入參是Verilog文件的相對地址,即相對src/main/resources的地址
內聯Verilog文件
hisel3.util包里還有有一個特質HasBlackBoxInline,混入該特質的黑盒類可以把Verilog代碼直接內嵌進去。內嵌的方式是調用特質里的方法“setInline(blackBoxName: String, blackBoxInline: String)”,類似於setResource的用法。這樣,目標文件夾里就會生成一個單獨的Verilog文件,復制內嵌的代碼。該方法適合小型Verilog設計。
inout端口
Chisel目前只支持在黑盒中引入Verilog的inout端口。Bundle中使用 “Analog(位寬)”聲明Analog類型的端口,經過編譯后變成Verilog的inout端口
模塊里的端口可以聲明成Analog類型,但只能用於與黑盒連接,不能在Chisel代碼中進行讀寫。
使用前,要先用“chisel3.experimental._”進行導入。
第二十二章 Chisel基礎——多時鍾域設計
// inout.scala
package test
import chisel3._
import chisel3.util._
import chisel3.experimental._
class InoutIO extends Bundle {
val a = Analog(16.W)
val b = Input(UInt(16.W))
val sel = Input(Bool())
val c = Output(UInt(16.W))
}
class InoutPort extends BlackBox with HasBlackBoxInline {
val io = IO(new InoutIO)
setInline("InoutPort.v",
"""
|module InoutPort( inout [15:0] a,
| input [15:0] b,
| input sel,
| output [15:0] c);
| assign a = sel ? 'bz : b;
| assign c = sel ? a : 'bz;
|endmodule
""".stripMargin)
}
class MakeInout extends Module {
val io = IO(new InoutIO)
val m = Module(new InoutPort)
m.io <> io
}
object InoutGen extends App {
chisel3.Driver.execute(args, () => new MakeInout)
}
第二十二章 Chisel基礎——多時鍾域設計
數字電路中免不了用到多時鍾域設計,尤其是設計異步FIFO這樣的同步元件
在Chisel里,則相對復雜一些,因為這與Scala的變量作用域相關,而且時序元件在編譯時都是自動地隱式跟隨當前時鍾域。
沒有隱式端口的模塊
繼承自Module的模塊類會獲得隱式的全局時鍾與同步復位信號,即使在設計中用不上它們也沒關系。如果讀者確實不喜歡這兩個隱式端口,則可以選擇繼承自RawModule,這樣在轉換成Verilog時就沒有隱式端口。
// module.scala package test import chisel3._ import chisel3.experimental._ class MyModule extends RawModule { val io = IO(new Bundle { val a = Input(UInt(4.W)) val b = Input(UInt(4.W)) val c = Output(UInt(4.W)) }) io.c := io.a & io.b } object ModuleGen extends App { chisel3.Driver.execute(args, () => new MyModule) }
RawModule也可以包含時序邏輯,但要使用多時鍾域語法。
定義一個時鍾域和復位域
chisel3.core包里有一個單例對象withClockAndReset,其apply方法定義如下:
def apply[T](clock: Clock, reset: Reset)(block: ⇒ T): T
在編寫代碼時不能寫成“import chisel3.core._”,這會擾亂“import chisel3._”的導入內容。正確做法是用“import chisel3.experimental._”導入experimental對象,它里面用同名字段引用了單例對象chisel3.core.withClockAndReset,這樣就不需要再導入core包。
class MultiClockModule extends Module { val io = IO(new Bundle { val clockB = Input(Clock()) val resetB = Input(Bool()) val stuff = Input(Bool()) }) // 這個寄存器跟隨當前模塊的隱式全局時鍾clock val regClock1 = RegNext(io.stuff) withClockAndReset(io.clockB, io.resetB) { // 在該花括號內,所有時序元件都跟隨時鍾io.clockB // 所有寄存器的復位信號都是io.resetB // 這個寄存器跟隨io.clockB val regClockB = RegNext(io.stuff) // 還可以例化其它模塊 val m = Module(new ChildModule) } // 這個寄存器跟隨當前模塊的隱式全局時鍾clock val regClock2 = RegNext(io.stuff) } ————————————————
因為第二個參數列表只有一個傳名參數,所以可以把圓括號寫成花括號,這樣還有自動的分號推斷。再加上傳名參數的特性,盡管需要一個無參函數,但是可以省略書寫“() =>”
withClockAndReset(io.clockB, io.resetB) {
sentence1
sentence2
...
sentenceN
}
實際上相當於:
withClockAndReset(io.clockB, io.resetB)( () => (sentence1; sentence2; ...; sentenceN) )
讀者再仔細看一看apply方法的定義,它的第二個參數是一個函數,同時該函數的返回結果也是整個apply方法的返回結果
class MultiClockModule extends Module { val io = IO(new Bundle { val clockB = Input(Clock()) val resetB = Input(Bool()) val stuff = Input(Bool()) }) val clockB_child = withClockAndReset(io.clockB, io.resetB) { Module(new ChildModule) } clockB_child.io.in := io.stuff }
如果傳名參數全都是定義,最后沒有表達式用於返回,那么apply的返回結果類型自然就是Unit。
class MultiClockModule extends Module { val io = IO(new Bundle { val clockB = Input(Clock()) val resetB = Input(Bool()) val stuff = Input(Bool()) }) val clockB_child = withClockAndReset(io.clockB, io.resetB) { val m = Module(new ChildModule) } clockB_child.m.io.in := io.stuff }
除了單例對象withClockAndReset,還有單例對象withClock和withReset
使用時鍾負沿和低有效的復位信號
可以改變其行為。復位信號比較簡單,只需要加上取反符號或邏輯非符號。時鍾信號稍微麻煩一些,需要先用asUInt方法把Clock類型轉換成UInt類型,再用toBool轉換成Bool類型,此時可以加上取反符號或邏輯非符號,最后再用asClock變回Clock類型
// negclkrst.scala package test import chisel3._ import chisel3.experimental._ class NegativeClkRst extends RawModule { val io = IO(new Bundle { val in = Input(UInt(4.W)) val myClk = Input(Clock()) val myRst = Input(Bool()) val out = Output(UInt(4.W)) }) withClockAndReset((~io.myClk.asUInt.toBool).asClock, ~io.myRst) { val temp = RegInit(0.U(4.W)) temp := io.in io.out := temp } } object NegClkRstGen extends App { chisel3.Driver.execute(args, () => new NegativeClkRst) }
示例:異步FIFO
// FIFO.scala package fifo import chisel3._ import chisel3.util._ import chisel3.experimental._ class FIFO(width: Int, depth: Int) extends RawModule { val io = IO(new Bundle { // write-domain val dataIn = Input(UInt(width.W)) val writeEn = Input(Bool()) val writeClk = Input(Clock()) val full = Output(Bool()) // read-domain val dataOut = Output(UInt(width.W)) val readEn = Input(Bool()) val readClk = Input(Clock()) val empty = Output(Bool()) // reset val systemRst = Input(Bool()) }) val ram = SyncReadMem(1 << depth, UInt(width.W)) // 2^depth val writeToReadPtr = Wire(UInt((depth + 1).W)) // to read clock domain val readToWritePtr = Wire(UInt((depth + 1).W)) // to write clock domain // write clock domain withClockAndReset(io.writeClk, io.systemRst) { val binaryWritePtr = RegInit(0.U((depth + 1).W)) val binaryWritePtrNext = Wire(UInt((depth + 1).W)) val grayWritePtr = RegInit(0.U((depth + 1).W)) val grayWritePtrNext = Wire(UInt((depth + 1).W)) val isFull = RegInit(false.B) val fullValue = Wire(Bool()) val grayReadPtrDelay0 = RegNext(readToWritePtr) val grayReadPtrDelay1 = RegNext(grayReadPtrDelay0) binaryWritePtrNext := binaryWritePtr + (io.writeEn && !isFull).asUInt binaryWritePtr := binaryWritePtrNext grayWritePtrNext := (binaryWritePtrNext >> 1) ^ binaryWritePtrNext grayWritePtr := grayWritePtrNext writeToReadPtr := grayWritePtr fullValue := (grayWritePtrNext === Cat(~grayReadPtrDelay1(depth, depth - 1), grayReadPtrDelay1(depth - 2, 0))) isFull := fullValue when(io.writeEn && !isFull) { ram.write(binaryWritePtr(depth - 1, 0), io.dataIn) } io.full := isFull } // read clock domain withClockAndReset(io.readClk, io.systemRst) { val binaryReadPtr = RegInit(0.U((depth + 1).W)) val binaryReadPtrNext = Wire(UInt((depth + 1).W)) val grayReadPtr = RegInit(0.U((depth + 1).W)) val grayReadPtrNext = Wire(UInt((depth + 1).W)) val isEmpty = RegInit(true.B) val emptyValue = Wire(Bool()) val grayWritePtrDelay0 = RegNext(writeToReadPtr) val grayWritePtrDelay1 = RegNext(grayWritePtrDelay0) binaryReadPtrNext := binaryReadPtr + (io.readEn && !isEmpty).asUInt binaryReadPtr := binaryReadPtrNext grayReadPtrNext := (binaryReadPtrNext >> 1) ^ binaryReadPtrNext grayReadPtr := grayReadPtrNext readToWritePtr := grayReadPtr emptyValue := (grayReadPtrNext === grayWritePtrDelay1) isEmpty := emptyValue io.dataOut := ram.read(binaryReadPtr(depth - 1, 0), io.readEn && !isEmpty) io.empty := isEmpty } } object FIFOGen extends App { chisel3.Driver.execute(args, () => new FIFO(args(0).toInt, args(1).toInt)) }
第二十三章 Chisel基礎——函數的應用
對於Chisel這樣的高級語言,函數的使用更加方便,還能節省不少代碼量。不管是用戶自己寫的函數、Chisel語言庫里的函數還是Scala標准庫里的函數,都能幫助用戶節省構建電路的時間
用函數抽象組合邏輯
與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)) }
用Scala的函數簡化代碼
比如在生成長的序列上,利用Scala的函數就能減少大量的代碼。
利用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))
}
Chisel的打印函數
printf函數只能在Chisel的模塊里使用,並且會轉換成Verilog的系統函數“$fwrite”,包含在宏定義塊“ `ifndef SYNTHESIS......`endif ”里。通過Verilog的宏定義,可以取消這部分不可綜合的代碼。因為后導入的chisel3包覆蓋了Scala的標准包,所以Scala里的printf函數要寫成“Predef.printf”的完整路徑形式。
————————————————
Scala風格
Chisel自定義了一個p插值器,該插值器可以對字符串內的一些自定義表達式進行求值、Chiel類型轉化成字符串類型等
val myUInt = 33.U // 顯示Chisel自定義的類型的數據 printf(p"myUInt = $myUInt") // myUInt = 33 // 顯示成十六進制 printf(p"myUInt = 0x${Hexadecimal(myUInt)}") // myUInt = 0x21 // 顯示成二進制 printf(p"myUInt = ${Binary(myUInt)}") // myUInt = 100001 // 顯示成字符(ASCⅡ碼) printf(p"myUInt = ${Character(myUInt)}") // myUInt = !
對於自定義的Bundle類型,可以重寫toPrintable方法來定制打印內容。當自定義的Bundle配合其他硬件類型例如Wire構成具體的硬件
class Message extends Bundle { val valid = Bool() val addr = UInt(32.W) val length = UInt(4.W) val data = UInt(64.W) override def toPrintable: Printable = { val char = Mux(valid, 'v'.U, '-'.U) p"Message:\n" + p" valid : ${Character(char)}\n" + p" addr : 0x${Hexadecimal(addr)}\n" + p" length : $length\n" + p" data : 0x${Hexadecimal(data)}\n" } } val myMessage = Wire(new Message) myMessage.valid := true.B myMessage.addr := "h1234".U myMessage.length := 10.U myMessage.data := "hdeadbeef".U printf(p"$myMessage")
C風格

val myUInt = 32.U
printf("myUInt = %d", myUInt) // myUInt = 32
Chisel的對數函數
chisel3.util包里有一個單例對象Log2,它的一個apply方法接收一個Bits類型的參數,計算並返回該參數值以2為底的冪次。返回類型是UInt類型,並且是向下截斷的
chisel3.util包里還有四個單例對象:log2Ceil、log2Floor、log2Up和log2Down,它們的apply方法的參數都是Int和BigInt類型,返回結果都是Int類型。log2Ceil是把結果向上舍入,log2Floor則向下舍入。log2Up和log2Down不僅分別把結果向上、向下舍入,而且結果最小為1。
單例對象isPow2的apply方法接收Int和BigInt類型的參數,判斷該整數是不是2的n次冪,返回Boolean類型的結果
與硬件相關的函數
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計數器
分別接收一個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("b1000".U) // 等於3.U
OHToUInt("b1000_0000".U) // 等於7.U
無關位
Verilog里可以用問號表示無關位,那么用case語句進行比較時就不會關心這些位。Chisel里有對應的BitPat類,可以指定無關位。
"b10101".U === BitPat("b101??") // 等於true.B
"b10111".U === BitPat("b101??") // 等於true.B
"b10001".U === BitPat("b101??") // 等於false.B
dontCare方法接收一個Int類型的參數,構造等值位寬的全部無關位。例如:
val myDontCare = BitPat.dontCare(4) // 等於BitPat("b????")
一種是單例對象Lookup,其apply方法定義
def apply[T <: Bits](addr: UInt, default: T, mapping: Seq[(BitPat, T)]): T
第二種是單例對象ListLookup,它的apply方法與上面的類似,區別在於返回結果是一個T類型的列表
defapply[T <: Data](addr: UInt, default: List[T], mapping: Array[(BitPat, List[T])]): List[T]
第二十四章 Chisel基礎——其它議題
動態命名模塊
轉成Verilog時的模塊名不使用定義的類名,而是使用重寫的desiredName方法的返回字符串。模塊和黑盒都適用
class Coffee extends BlackBox { val io = IO(new Bundle { val I = Input(UInt(32.W)) val O = Output(UInt(32.W)) }) override def desiredName = "Tea" } class Salt extends Module { val io = IO(new Bundle {}) val drink = Module(new Coffee) override def desiredName = "SodiumMonochloride" }
動態修改端口
選值以及if語句可以創建出可選的端口,在例化該模塊時可以通過控制Boolean入參來生成不同的端口
class ModuleWithOptionalIOs(flag: Boolean) extends Module { val io = IO(new Bundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) val out2 = if (flag) Some(Output(UInt(12.W))) else None }) io.out := io.in if(flag) { io.out2.get := io.in } }
生成正確的塊內信號名
在when、withClockAndReset等語句塊里定義的信號(線網和寄存器),轉換成Verilog時不會生成正確的變量名
package test import chisel3._ class TestMod extends Module { val io = IO(new Bundle { val a = Input(Bool()) val b = Output(UInt(4.W)) }) when (io.a) { val innerReg = RegInit(5.U(4.W)) innerReg := innerReg + 1.U io.b := innerReg } .otherwise { io.b := 10.U } } object NameGen extends App { chisel3.Driver.execute(args, () => new TestMod)
如果想讓名字正確,則需要在build.sbt文件里加上:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
拆包一個值(給拼接變量賦值)
class MyBundle extends Bundle { val a = UInt(2.W) val b = UInt(4.W) val c = UInt(3.W) } val z = Wire(UInt(9.W)) z := ... val unpacked = z.asTypeOf(new MyBundle) unpacked.a unpacked.b unpacked.c
子字賦值
在Verilog中,可以直接給向量的某幾位賦值。同樣,Chisel受限於Scala,不支持直接給Bits類型的某幾位賦值
辦法是先調用Bits類型的toBools方法。該方法根據調用對象的0、1排列返回一個相應的Seq[Bool]類型的結果,並且低位在序列里的下標更小,比如第0位的下標就是0、第n位的下標就是n。然后用這個Seq[Bool]對象配合VecInit構成一個向量,此時就可以給單個比特賦值。注意,必須都是Bool類型,要注意賦值前是否需要類型轉換。子字賦值完成后,Bool向量再調用asUInt、asSInt方法轉換回來
class TestModule extends Module { val io = IO(new Bundle { val in = Input(UInt(10.W)) val bit = Input(Bool()) val out = Output(UInt(10.W)) }) val bools = VecInit(io.in.toBools) bools(0) := io.bit io.out := bools.asUInt }
參數化的Bundle
Chisel提供了一個內部的API函數cloneType,任何繼承自Data的Chisel對象,要復制自身時,都是由cloneType負責返回該對象的復制對象當自定義的Bundle的主構造方法沒有參數時,Chisel會自動推斷出如何構造Bundle對象的復制,原因很簡單,因為構造一個新的復制對象不需要任何參數,僅僅使用關鍵字new就行了。但是,如果自定義的Bundle帶有參數列表,那么Chisel就無法推斷了,因為傳遞進去的參數可以是任意的,並不一定就是完全地復制。此時需要用戶自己重寫Bundle類的cloneType方法override def cloneType = (new CustomBundle(arguments)).asInstanceOf[this.type]
class ExampleBundle(a: Int, b: Int) extends Bundle {
val foo = UInt(a.W)
val bar = UInt(b.W)
override def cloneType = (new ExampleBundle(a, b)).asInstanceOf[this.type]
}
class ExampleBundleModule(btype: ExampleBundle) extends Module {
val io = IO(new Bundle {
val out = Output(UInt(32.W))
val b = Input(chiselTypeOf(btype))
})
io.out := io.b.foo + io.b.bar
}
class Top extends Module {
val io = IO(new Bundle {
val out = Output(UInt(32.W))
val in = Input(UInt(17.W))
})
val x = Wire(new ExampleBundle(31, 17))
x := DontCare
val m = Module(new ExampleBundleModule(x))
m.io.b.foo := io.in
m.io.b.bar := io.in
io.out := m.io.out
}
Chisel泛型
無論是Chisel的函數還是模塊,都可以用類型參數和上、下界來泛化。在例化模塊時,傳入不同類型的參數,就可能會產生不同的電路,而無需編寫額外的代碼,當然前提是邏輯、類型必須正確。
未驅動的線網
Chisel的Invalidate API支持檢測未驅動的輸出型IO以及定義不完整的Wire定義,在編譯成firrtl時會產生“not fully initialized”錯誤。換句話說,就是組合邏輯的真值表不完整,不能綜合出完整的電路。如果確實需要不被驅動的線網,則可以賦給一個DontCare對象,這會告訴Firrtl編譯器,該線網故意不被驅動。轉換成的Verilog會賦予該信號全0值
val io = IO(new Bundle {
val outs = Output(Vec(10, Bool()))
})
io.outs <> DontCare
第二十五章 Chisel進階——隱式參數的應用
用Chisel編寫的CPU,比如Rocket-Chip、RISCV-Mini等,都有一個特點,就是可以用一個配置文件來裁剪電路。這利用了Scala的模式匹配、樣例類、偏函數、可選值、隱式定義等語法
相關定義
Chisel提供了一個內部的API函數cloneType,任何繼承自Data的Chisel對象,要復制自身時,都是由cloneType負責返回該對象的復制對象
