雷思磊
(酒泉衛星發射中心,酒泉 735000)
雷思磊
(酒泉衛星發射中心,酒泉 735000)
Chisel是加州大學伯克利分校研究人員設計并發布的一種開源的硬件設計語言,已成功用于實現多種處理器,在介紹Chisel基本概念的基礎上,通過一個組合電路設計示例和一個時序電路設計示例,說明應用Chisel進行數字電路設計的主要流程。
Chisel; Scala;開源硬件
Chisel(Constructing Hardware In a Scala Embedded Language)是由加州大學伯克利分校設計并發布的一種開源硬件描述語言,建立在Scala語言之上,是Scala特定領域語言的一個應用,具有高度參數化的生成器(highly parameterized generators),可以支持高級硬件設計。其產生源于加州大學伯克利分校的研究人員認為現有的硬件描述語言(如VHDL、Verilog HDL等)最初設計的目的是用來仿真的,所以有很多不可綜合的語法,此外,VHDL、Verilog HDL缺少目前高級語言具備的一些特性,比如對象、繼承等[1],所以設計了Chisel,并將其開源,而因為Chisel就是使用Scala編寫的一些類,所以在使用Chisel設計電路時,實際就是在寫Scala程序,從而可以使用到Scala的許多高級特性,比如面向對象、函數式編程、類型參數化、類型推斷等。
采用Chisel設計的電路,經過編譯,可以得到針對FPGA、ASIC的Verilog HDL代碼,還可以得到對應的時鐘精確C++模擬器,如圖1所示。

圖1 使用Chisel可以得到多種目標
目前,有多個開源項目使用Chisel作為開發語言,包括采用RISC-V架構的開源標量處理器Rocket[2]、開源亂序執行處理器BOOM(Berkeley Out-of-Order Machine)[3]等。本文首先介紹Chisel的基本概念,然后使用Chisel實現一個簡單的組合邏輯電路、一個簡單的時序邏輯電路,以此說明應用Chisel進行數字電路設計的主要流程。
1.1 數據類型
Chisel的數據類型用來指明在線上流動的信號(flowing on wire)、存儲在狀態元素(State Element)中的值的類型,對應Verilog HDL線網型、寄存器型等。雖然數字電路最終都是對二進制數字矢量進行操作,但是定義一些抽象的數據類型,有助于更加清晰地表達,同時也有利于產生更加優化的電路。具體類型如下:
① Bits:Bit的集合。
② SInt:有符號整數。
③ UInt:無符號整數。
④ Bool:布爾類型。
⑤ Bundle:一些元素的集合,每個元素都有一個變量名,類似于C語言中的結構體。
⑥ Vec:一些元素組成的向量,每個元素都有一個索引序號,類似于C語言中的數組。
上面的數據類型也對應一些類,比如SInt類型就對應SInt類,當聲明一個SInt變量時,就需要調用SInt類的構造函數,比如:
SInt(5)
在上面的定義中并沒有指明數據的寬度,但Chisel編譯器會推測數據寬度,自動選擇最小的寬度,當然也可以明確指明數據寬度,比如:
SInt(5,7) //此處明確指出寬度是7bit
Bundle是一些元素的集合,每個元素都有一個變量名,類似于C語言中的結構體。用戶可以通過定義Bundle的子類來定義一個Bundle類型的變量,如下:
class MyFloat extends Bundles{
val sign = Bool()
val exponent = UInt(width=8)
val significant = UInt(width=23)
}
val x = new MyFloat()
val xs = x.sign
Vec是一些元素組成的向量,每個元素都有一個索引序號,類似于C語言中的數組。下面是一個Vec類型的變量的定義:
//定義了一個由5個23位寬度的SInt組成的Vec
val myVec = Vec.fill(5){SInt(width = 23)}
val reg3 = myVec(3) //取出其中的一個元素
1.2 組合電路
在Chisel中,每個電路都是一些節點的集合,每個節點是一個硬件操作單元,具有0個、1個或者多個輸入,依據一個輸入驅動一個輸出。不同的節點可以通過操作符連接在一起,例如可以通過如下表達式表示一個簡單的組合邏輯電路:
val out = (a & b) | (~c & d)
在上述表達式中,“&”表示bit與,“|”表示bit或,“~”表示bit反,a、b、c、d表示一些變量。任何一個簡單的表達式都可以被轉化為一個電路樹,等式右邊的變量作為葉子節點,操作符組成中間節點,電路的最終輸出是電路樹根節點的輸出。
Chisel支持的操作符略——編者注。
1.3 函 數
Chisel支持將一些重復的邏輯定義為函數,然后再多處使用,例如下面定義了一個簡單的函數:def clb(a:UInt, b:UInt, c:UInt, d:UInt) = (a & b) | (~c&d)
函數clb有4個參數a、b、c、d,此處的def是Scala中定義的關鍵字,用來定義函數,每個參數后面跟一個冒號,然后是數據類型。在參數之后定義返回類型,也可以不定義,Chisel會自動推測,上例中就沒有定義返回類型。等號之后的就是函數體。函數定義之后,其使用方法如下:
val out = clb(a, b, c, d)
1.4 端 口
端口就是硬件單元對外的接口,需要指明方向(輸入還是輸出)。一個端口聲明的例子如下:
class Decoupled extends Bundle{
val ready = Bool(OUTPUT)
val data = UInt(INPUT, 32)
val valid = Bool(INPUT)
}
其中INPUT、OUTPUT指定方向,后面指出寬度,對于Bool類型,其寬度就是1,所以不需要明確指出。
1.5 模 塊
Chisel中的模塊與Verilog HDL中模塊的概念十分相似,都是用層次結構描述電路。Chisel中的模塊定義為一個類,其定義遵循以下幾點:繼承自Module類、有一個命名為io的端口、在其構造函數中連接子電路。如下是一個2選1選擇器的模塊定義,其中io端口是Bundle類型。
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.in1)
}
1.6 狀態單元
如果要實現時序電路,還需要狀態單元,Chisel支持的最簡單的狀態單元就是上升沿觸發的寄存器,可以使用如下方式例化:
val reg = Reg(next=in)
上述代碼形成的電路就是:將輸入賦值給輸出,但是輸出比輸入延遲一個時鐘周期。此處沒有聲明變量reg的數據類型,Chisel會自動從輸入變量in推測reg的類型。另外,也沒有聲明clock、reset信號,因為在Chisel中,clock、reset都是全局信號,不需要顯示聲明。
使用寄存器可以組成許多有用的電路,如下是一個上升沿檢測電路的代碼,輸入是一個Boolean變量,如果檢測到上升沿,那么輸出true:
def risingedge(x: Bool) = x && !Reg(next=x)
如下是一個計數器的代碼,計數到最大值,然后從0開始重新計數:
def counter(max: UInt) = {
val x = Reg(init = UInt(0, max.getWidth))
x := Mux(x === max, UInt(0), x + UInt(1))
x
}
2.1 Chisel開發環境搭建
實驗環境是Ubuntu14.04(64位),下載sbt0.13.8,解壓到一個路徑下,比如/home/riscv/riscv/sbt,將其中的bin路徑添加到環境變量PATH中,也就是打開~/.profile文件,在最后添加如下語句:
export PATH=/home/riscv/riscv/sbt/bin/:$PATH
2.2 比較器電路
設計一個比較器電路以說明Chisel進行數字電路設計的基本流程。新建一個文件夾chisel_max,在該文件夾下新建文件max2.scala、build.sbt,其中build.sbt的內容如下(注意兩行之間空一行),表示需要最新的Chisel庫,以及一些依賴庫的地址:
resolvers ++= Seq("scct-github-repository" at
"http://mtkopone.github.com/scct/maven-repo")
libraryDependencies += "edu.berkeley.cs" %% "chisel" % "latest.release"
max2.scala的內容如下,這是一個比較器,從兩個8位的輸入中,選擇一個較大的數,作為輸出:
import Chisel._
class Max2 extends Module {
val io = new Bundle {
val in0 = UInt(INPUT,8)
val in1 = UInt(INPUT,8)
val out = UInt(OUTPUT,8)
}
io.out := Mux(io.in0 > io.in1, io.in0, io.in1)
}
object Hello {
def main(args: Array[String]) : Unit={
val margs=Array("--backend","v","--compile")
chiselMain(margs, () => Module(new Max2()))
}
}
上述代碼首先引入Chisel依賴,然后定義了一個Max2類,這個電路有兩個8位輸入,一個8位輸出,輸出值是兩個輸入中較大的那一個值。在最后定義了一個方法main,其中使用了對象chiselMain,通過給chiselMain傳遞不同的參數,可以得到C++模擬器、Verilog HDL代碼等不同的結果。chiselMain支持的參數如表1所列。

表1 chiselMain支持的參數
由表2可知,因為backend的值是v,所以上述代碼會得到Verilog代碼。在終端中進入上面的chisel_max目錄,輸入sbt,會下載相關的依賴包,最后會給出>符號,此時輸入run,即可得到對應的Verilog代碼文件。得到的Verilog代碼位于文件Max2.v中,實現的正是比較器的功能,內容如下:
module Max2(
input [7:0] io_in0,
input [7:0] io_in1,
output[7:0] io_out
);
wire[7:0] T0;
wire T1;
assign io_out = T0;
assign T0 = T1 ? io_in0 : io_in1;
assign T1 = io_in1 < io_in0;
endmodule
2.3 C++模擬器的使用
使用C++模擬器的主要目的是用來做測試,所以先介紹一下相關的類,Chisel采用測試向量的機制來測試電路,測試向量所在的類是Tester的子類。測試類繼承自Tester,然后連接到指定的模塊,就可以向該模塊輸入測試向量了,具體來說,Tester提供了如下方法:
① poke用來設置端口的值;
② step用來表示執行一個時鐘;
③ peek用來讀取端口的值;
④ expect用來判斷讀取的端口值是否是預期的值。
如下是添加了測試類的比較器:
import Chisel._
class Max2 extends Module {
val io = new Bundle {
val in0 = UInt(INPUT,8)
val in1 = UInt(INPUT,8)
val out = UInt(OUTPUT,8)
}
io.out := Mux(io.in0 > io.in1, io.in0, io.in1)
}
class Max2Tests(c: Max2) extends Tester(c) {
for (i <- 0 until 10) {
// FILL THIS IN HERE
val in0 = rnd.nextInt(1 << 8)
val in1 = rnd.nextInt(1 << 8)
poke(c.io.in0, in0)
poke(c.io.in1, in1)
step(1)
expect(c.io.out, if(in0 > in1) in0 else in1)
}
}
object max2 {
def main(args: Array[String]) : Unit={
val margs=Array("--backend","c","--genHarness","--compile","--test")
chiselMainTest(margs, () => Module(new Max2())){c => new Max2Tests(c)}
}
}
與第2.2節比較,有如下幾點不同:
① 增加了一個Max2Tests類,繼承自Tester,其中使用poke方法給Max2電路的輸入賦值,此處隨機產生了10組數據,使用expect判斷Max2電路的輸出是否正確,是否符合預期。

圖2 計數器對應的電路
② 在main函數中將chiselMain替換為chiselMainTest,目的是進行測試。
③ chiselMianTest的參數中backend的值為c,表示得到C++代碼,此外,還添加了一個test參數。
打開終端,進入chisel_max的目錄,輸入sbt,再輸入run,可以得到下面的結果,限于篇幅,本文只給出第一組數據的測試結果,輸入的測試值為0x6b、0x99,輸出的值為0x99,是輸入值中較大的,符合預期。
……
STARTING ./Max2
RESET 5
POKE Max2.io_in0 <-0x6b
POKE Max2.io_in1 <-0x99
STEP 1 -> 1
PEEK Max2.io_out -> 0x99
EXPECT Max2.io_out <- 153 == 153 PASS
……
本節通過一個同步計數器電路設計,說明Chisel時序電路的設計要點,這個電路有一個使能輸入,當使能輸入為1的時候,計數器才計數,并且是在時鐘上升沿計數。電路設計如下:
import Chisel._
class Counter extends Module {
val io = new Bundle {
val enb = Bool(INPUT)
val out = UInt(OUTPUT, 4)
}
val counter1 = Reg(UInt(width = 4), UInt(0))
when ( io.enb ) { counter1 := counter1 + UInt(1)}
io.out := counter1
}
object counter_obj {
def main(args: Array[String]) : Unit={
……
}
Counter類中定義的是同步計數器,具有一個使能輸入enb、一個計數結果輸出out。在Counter的構造函數中,定義了一個寄存器counter1,其中有隱含的時鐘輸入,接著是一個條件判斷,enb為真的時候,counter1的值才加1,最后將counter1的值作為輸出。編譯上述代碼得到對應的Verilog HDL代碼,然后在QuartusII中編譯,可以得到如圖2所示電路。從電路結構分析,實現的正是計數器。
Chisel基于Scala,充分利用了Scala的一些高級特性,在本文示例電路的實現中也體現了這一點,Chisel仍然在持續改進中,目前已成功使用Chisel編寫實現了數種開源處理器。此外,Chisel的開源特性,也有助于用戶了解一種硬件設計語言的內部實現機理,并在此基礎上進行特定的優化、改進。


Lei Silei
(Jiuquan Satellite Launch Center,Jiuquan 735000,China)
Chisel is an open source hardware design language,which is designed and published by researchers of university of California at Berkeley.It has been successfully used to achieve a variety of processors.In the paper,the basic concept of Chisel is introduced.The main processes of Chisel digital circuit design is illustrated through a combination of circuit design example and a sequential circuit design example.
Chisel; Scala;open source
TP368.1
A