Chisel
Scala
Digital Design
Programming
For Chisel and Scala
Case Study - RISC-V CPU Design
一開始在學 Chisel,不管是課程的範例或是 The Chisel Book 上面的範例,都是用 chisel.util.Enum
這個方式來宣告 states,但後來看 Chisel 官方文件的時候,發現都是用下一小節提到的 ChiselEnum
的方法,目前還不太清楚差別在哪,個人認為應該 chisel.util.Enum
是比較舊的寫法 (The Chisel Book 2ed 就出現了),而 ChiselNnum
是目前官方文件上的推薦用法,所以應該是比較新的寫法,但還不太確定就是了。
峻豪
我覺得 chisel.util.Enum
應該就類似 C/C++ 的 Enum,他就是單純把你宣告的變數名稱 encode 成對應的整數,所以 chisel.util.Enum
沒有辦法被放進 IO/Bundle 裡面,但是 ChiselEnum
就可以,彈性比較大,可以讓 code 的可讀性更高。
Use val ... = Enum()
Use ChiselEnum
Note: the is statement can take multiple conditions e.g. is (sTwo1s, sOne1) { … }.
Chisel
ChiselEnum
引述官方的文件敘述
The ChiselEnum type can be used to reduce the chance of error when encoding mux selectors, opcodes, and functional unit operations. In contrast with Chisel.util.Enum, ChiselEnum are subclasses of Data, which means that they can be used to define fields in Bundles, including in IOs.
Chisel
從上面敘述最後一段看起來,ChiselEnum
應該是一個可以取代 Chisel.util.Enum
的存在,因為它的限制更少 (e.g. 可以包在 Bundle, IO 裡面)
我認為現在看到 ChiselEnum
很好用的地方應該在於定義 Opcode 或是 Mux Seletor,以下有兩個 example,一個是針對 MUX,一個是針對 RISC-V OPCODE
因為 ChiselEnum
是 Data
的 subclass,所以可以被直接包在 Bundle 內,利用這樣的方式來定義 OPCODE 我覺得會比之前利用 val
的方式來定義還要來得容易看懂電路描述的邏輯
Example of Mux
Example of Defining RV32I Opcode
在 Chisel 中,常用的電路行為可以直接定義成一個 function,以下引述自文件
We can define functions to factor out a repeated piece of logic that we later reuse multiple times in a design.
Chisel
Example
在 scala 中,函數的最後一個 expression 會被 return(這是 Functional Programming 的特性),所以呼叫這個 clb()
function 時,就會 return (a & b) | (~c & d)
的結果。
在 scala 中,object 本來就有一個 pre-existing 的 function(method) 叫做 apply()
,當呼叫一個定義好的 object 時,會一併呼叫 apply()
,所以可以透過這個方法,來把一個普通的 Circuit Module 變成 Functional Module (我自己的理解是讓這個 module 可以像是 function 一樣被呼叫,然後有 return value)。以下引述自官方文件
Objects in Scala have a pre-existing creation function (method) called apply. When an object is used as value in an expression (which basically means that the constructor was called), this method determines the returned value. When dealing with hardware modules, one would expect the module output to be representative of the hardware module’s functionality. Therefore, we would sometimes like the module output to be the value returned when using the object as a value in an expression. Since hardware modules are represented as Scala objects, this can be done by defining the object’s apply method to return the module’s output. This can be referred to as creating a functional interface for module construction.
Chisel
Mux2
,並且定義 Mux2
object 的 apply()
function
Mux4
Mux4
,可以讓 code 簡潔很多
以下引述自官方文件
Chisel now supports a DontCare element, which may be connected to an output signal, indicating that that signal is intentionally not driven. Unless a signal is driven by hardware or connected to a DontCare, Firrtl will complain with a “not fully initialized” error.
Chisel
以前的 FIRRTL Compiler 不支援沒有被驅動的 output signal/wires,所以一旦有 output signal 或是 wires 沒有被初始化,就會出現 “not fully initialized” error,但是後來的 Chisel 支援了 DontCare
這個關鍵字,可以用在那些不會被驅動的信號,讓 compiler 不會出現錯誤。
Example 1
Example 2 - Use bunk connection
以下引述自官方文件
Chisel contains two connection operators,
:=
and<>
. This document provides a deeper explanation of the differences of the two and when to use one or the other. The differences are demonstrated with experiments using Scastie examples which useDecoupledIO
.
Chisel
直接說結論
<>
is Commutative:=
means assign ALL LHS signals from the RHS, regardless of the direction on the LHS.:=
to assign DontCare to Wires<>
or :=
to assign DontCare to directioned things (IOs<>
works between things with at least one known flow (An IO or child’s IO).<>
and :=
connect signals by field name.實驗的過程可以參考:[Deep Dive into Connection Operators](https://www.chisel-lang.org/chisel3/docs/explanations/connection-operators.html
在 Chisel 中,有一種特殊的用法可以讓 Bundle 可以被參數化,通常是用在 bit width 的參數化。
在 Chisel 中,可以利用一些語法,配合 scala 的特性,來讓一個 Reg 包含不同的 data type,以下舉兩個例子,並且同時示範如何做 Partially reset(initialization)。
Example 1
透過 Bundle + .Lit()
的語法來初始化 aggregate register
Example 2
透過 Wire 和 initial value 的方式
Example 3
另外一種寫法,單純透過 Wire 的宣告
有時候一個 module 中的某些 IO ports 可能是為了 debug 用的,當 debug 完成之後,我們就不會想要在 verilog code 中生成這些 ports,如何在 Chisel 中表達這些 optional IO ports?以下舉一個例子
Example
利用傳入 Boolean
參數,來決定某個 ports 是否要被生成,利用 if (param) Some_port else None
這個語法
特別注意上面的 io.out2.get
這個用法,這個語法代表,如果 io.out2
有被定義的話,那麼 get()
就會 return io.out2
,然後就可以進行 assgin,反之,get()
會 return None
。這方面的 API 是關於 scala 的 Options,詳細可以參考 Scala - Options
Example - if an entire IO
is optional
上面的 getOrElse(false.B)
代表如果 io.in
有被定義的話,那就會傳回 io.in
,反之,就會傳回 false.B
,所以括號內的 value 代表 default value。
有些 ports 可能包含雙向的 signals,像是被 Decoupled()
包起來的 port,有時候我們可能會遇到下面的使用場景
但是這樣做,FIRRTL Compiler 會報錯
因此,可以透過 chiselTypeOf()
這個 API 解決這個問題,僅僅拿到 signal 的 type 就好,不要管它的方向
之前有一個比較舊的框架叫做 PeekPokeTester
,但後來 UC Berkeley 又推出一個新的框架來取代這個舊框架,叫做 ChiselTest
。兩著的用法雖然類似,但是 ChiselTest
是基於 ScalaTest
的一個測試框架,所以可以利用很多 ScalaTest
的強大功能,因此還是推薦使用 ChiselTest
而非 PeekPokeTester
。
這篇筆記中,我們會先簡單介紹 ScalaTest
的用法,再帶入 ChiselTest
的用法。以下引述自 The Chisel Book
ChiselTest is the new standard testing tool for Chisel modules based on the ScalaTest tool for Scala and Java, which we can use to run Chisel tests.
The Chisel Book
chiseltest now provides a compatibility layer that makes it possible to re-use old PeekPokeTester based tests with little to no changes to the code.
FIRRTL/Chisel
以下是 ScalaTest
的簡單用法,你可以寫一段類似敘述或是 spec 的語句來描述目前這個 test 要做哪些事情,應該要有哪些行為產生,像是下面的 "Integers" should "add" in {...}
,而花括號 {...}
裡面則為 test codes。
而要執行 test script,則可以直接在 terminal 輸入 $ sbt test
,他會執行全部的測試檔案,如果不想要執行全部的測試的話,則可以輸入像是 $ sbt "testOnly ExampleTest"
來執行單一測試。
而上面的測試的輸出結果如下
使用 ChiselTest
和使用 ScalaTest
的方式差不多,只是多了一些由 Chisel 提供的 API 而已。以下為一個簡單的例子
Run your testbench
simulation results in terminal
在上面的例子,我們是用 dut.io.poke()
加上 toString()
的方式去得到 port value,還有另外一種方式,是利用 expect()
Testing result in terminal
如果要產生 .vcd
檔案,基本上有兩種方式可以選擇。
Option 1 - Add commands in terminal
加上 -DwriteVcd=1
這種方式會一次產生所有 test cases 的波形,如果不想要一次產生所有波形可以使用 option 2。
Option 2 - Use ChiselTest
API
利用 withAnnotations (Seq( WriteVcdAnnotation ))
printf
Debugging在 debug 的時候也可以利用 printf()
的方式印出想要看到的 values,而 printf()
可以寫在 module declaration 內的任何地方,他會在每次 rising edge 的時候把東西印出來。下面引述自官方 doc
The printing happens at the rising edge of the clock. A printf statement can be inserted just anywhere in the module definition, as shown in the printf debugging version of the DUT.
The Chisel Book
而 printf()
的寫法又有分兩種 style,一種是 Scala Style,一種是 C Style。其中 Scala Style 感覺很類似 Python 裡面的 f string。
在 Chisel 的子計畫中,有一個叫做 diagrammer 的 project,他是基於 GraphViz
這個繪圖框架,可以根據 .fir
檔案自動生成電路相關的 block diagram 或是整個 module 的 hierarchy diagram。
先安裝 diagrammer
Clone repo
如果沒有安裝 GraphViz
,則需要一併安裝,因為 diagrammer
是基於 GraphViz
的 tool
Hwo to use
Step#1: 你需要先生成電路的 .fir
檔案
Setp#2: 來移動到 diagrammer
的資料夾下面,利用 diagram.sh
生成 .svg
檔案
如果你沒有指定路徑的話,那預設會在 diagrammer
的資料夾底下,所以這個 tool 有提供一些 options 可以使用,像是利用 -t
指定生成圖檔要放在哪個路徑之下。
open
, set to empty to tell it not to do open