ChiSel 學習筆記


原文鏈接

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負責返回該對象的復制對象


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM