tags: Chisel Scala Digital Design Programming

Chisel HDL 筆記

Introduction

  • 峻豪
    這邊文件的重點主要在於記錄一些學習資源、useful links,和一些筆記
    不過,筆記的部分會相對零散,較沒有主題性。因為我主要想記錄一些我自己認為特別或是好用,但是平常沒有用過的方法 & 語法,所以筆記應該是對 Chisel 有基本概念的人比較適合閱讀

Concepts and Advanced Syntax

Finite-State-Machine 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()

    ​​​​import chisel3._ ​​​​import chisel3.util._ ​​​​class SimpleFsm extends Module { ​​​​ val io = IO(new Bundle{ ​​​​ val badEvent = Input(Bool ()) ​​​​ val clear = Input(Bool ()) ​​​​ val ringBell = Output(Bool ()) ​​​​ }) ​​​​ // The three states ​​​​ val green :: orange :: red :: Nil = Enum (3) ​​​​ // The state register ​​​​ val stateReg = RegInit(green) ​​​​ // Next state logic ​​​​ switch (stateReg) { ​​​​ is (green) { ​​​​ when(io.badEvent ) { ​​​​ stateReg := orange ​​​​ } ​​​​ } ​​​​ is (orange) { ​​​​ when(io.badEvent ) { ​​​​ stateReg := red ​​​​ } .elsewhen(io.clear) { ​​​​ stateReg := green ​​​​ } ​​​​ } ​​​​ is (red) { ​​​​ when (io.clear) { ​​​​ stateReg := green ​​​​ } ​​​​ } ​​​​} ​​​​ // Output logic ​​​​ io. ringBell := stateReg === red ​​​​}
  • Use ChiselEnum

    ​​​​import chisel3._ ​​​​import chisel3.util.{switch, is} ​​​​import chisel3.experimental.ChiselEnum ​​​​object DetectTwoOnes { ​​​​ object State extends ChiselEnum { ​​​​ val sNone, sOne1, sTwo1s = Value ​​​​ } ​​​​} ​​​​/* This FSM detects two 1's one after the other */ ​​​​class DetectTwoOnes extends Module { ​​​​ import DetectTwoOnes.State ​​​​ import DetectTwoOnes.State._ ​​​​ val io = IO(new Bundle { ​​​​ val in = Input(Bool()) ​​​​ val out = Output(Bool()) ​​​​ val state = Output(State()) ​​​​ }) ​​​​ val state = RegInit(sNone) ​​​​ io.out := (state === sTwo1s) ​​​​ io.state := state ​​​​ 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 ​​​​ } ​​​​ } ​​​​ } ​​​​}

    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
因為 ChiselEnumData 的 subclass,所以可以被直接包在 Bundle 內,利用這樣的方式來定義 OPCODE 我覺得會比之前利用 val 的方式來定義還要來得容易看懂電路描述的邏輯

  • Example of Mux

    ​​​​// package CPUTypes { ​​​​object AluMux1Sel extends ChiselEnum { ​​​​ val selectRS1, selectPC = Value ​​​​} ​​​​// We can see the mapping by printing each Value ​​​​AluMux1Sel.all.foreach(println) ​​​​// AluMux1Sel(0=selectRS1) ​​​​// AluMux1Sel(1=selectPC) ​​​​class AluMux1Bundle extends Bundle { ​​​​ val aluMux1Sel = Input(AluMux1Sel()) ​​​​ val rs1Out = Input(Bits(32.W)) ​​​​ val pcOut = Input(Bits(32.W)) ​​​​ val aluMux1Out = Output(Bits(32.W)) ​​​​} ​​​​class AluMux1File extends Module { ​​​​ val io = IO(new AluMux1Bundle) ​​​​ // Default value for aluMux1Out ​​​​ io.aluMux1Out := 0.U ​​​​ switch (io.aluMux1Sel) { ​​​​ is (selectRS1) { ​​​​ io.aluMux1Out := io.rs1Out ​​​​ } ​​​​ is (selectPC) { ​​​​ io.aluMux1Out := io.pcOut ​​​​ } ​​​​ } ​​​​}
  • Example of Defining RV32I Opcode

    ​​​​object Opcode extends ChiselEnum { ​​​​ val load = Value(0x03.U) // i "load" -> 000_0011 ​​​​ val imm = Value(0x13.U) // i "imm" -> 001_0011 ​​​​ val auipc = Value(0x17.U) // u "auipc" -> 001_0111 ​​​​ val store = Value(0x23.U) // s "store" -> 010_0011 ​​​​ val reg = Value(0x33.U) // r "reg" -> 011_0011 ​​​​ val lui = Value(0x37.U) // u "lui" -> 011_0111 ​​​​ val br = Value(0x63.U) // b "br" -> 110_0011 ​​​​ val jalr = Value(0x67.U) // i "jalr" -> 110_0111 ​​​​ val jal = Value(0x6F.U) // j "jal" -> 110_1111 ​​​​} ​​​​object BranchFunct3 extends ChiselEnum { ​​​​ val beq, bne = Value ​​​​ val blt = Value(4.U) ​​​​ val bge, bltu, bgeu = Value ​​​​} ​​​​// We can see the mapping by printing each Value ​​​​BranchFunct3.all.foreach(println) ​​​​// BranchFunct3(0=beq) ​​​​// BranchFunct3(1=bne) ​​​​// BranchFunct3(4=blt) ​​​​// BranchFunct3(5=bge) ​​​​// BranchFunct3(6=bltu) ​​​​// BranchFunct3(7=bgeu)

Functional Abstraction

在 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

    ​​​​def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt = ​​​​ (a & b) | (~c & d)

    在 scala 中,函數的最後一個 expression 會被 return(這是 Functional Programming 的特性),所以呼叫這個 clb() function 時,就會 return (a & b) | (~c & d) 的結果。

    ​​​​val out = clb(a,b,c,d)

Functional Module Creation

在 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

  • Example of using 2-inputs Mux to coinstruct 4-inputs Mux
    • 先定義 Mux2,並且定義 Mux2 object 的 apply() function
      ​​​​​​​​import chisel3._ ​​​​​​​​class Mux2 extends Module { ​​​​​​​​ val io = IO(new Bundle { ​​​​​​​​ val sel = Input(Bool()) ​​​​​​​​ val in0 = Input(UInt()) ​​​​​​​​ val in1 = Input(UInt()) ​​​​​​​​ val out = Output(UInt()) ​​​​​​​​ }) ​​​​​​​​ io.out := Mux(io.sel, io.in0, io.in1) ​​​​​​​​} ​​​​​​​​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 ​​​​​​​​ } ​​​​​​​​}
    • 用一般的方法建構 Mux4
      ​​​​​​​​class Mux4 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 m0 = Module(new Mux2) ​​​​​​​​ m0.io.sel := io.sel(0) ​​​​​​​​ m0.io.in0 := io.in0 ​​​​​​​​ m0.io.in1 := io.in1 ​​​​​​​​ val m1 = Module(new Mux2) ​​​​​​​​ m1.io.sel := io.sel(0) ​​​​​​​​ m1.io.in0 := io.in2 ​​​​​​​​ m1.io.in1 := io.in3 ​​​​​​​​ val m3 = Module(new Mux2) ​​​​​​​​ m3.io.sel := io.sel(1) ​​​​​​​​ m3.io.in0 := m0.io.out ​​​​​​​​ m3.io.in1 := m1.io.out ​​​​​​​​ io.out := m3.io.out ​​​​​​​​}
    • 用 Functional Module 的方式建構 Mux4,可以讓 code 簡潔很多
      ​​​​​​​​class Mux4 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)) ​​​​​​​​ }) ​​​​​​​​ io.out := Mux2(io.sel(1), ​​​​​​​​ Mux2(io.sel(0), io.in0, io.in1), ​​​​​​​​ Mux2(io.sel(0), io.in2, io.in3)) ​​​​​​​​}

Unconnected Wires

以下引述自官方文件

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

    ​​​​class Out extends Bundle { ​​​​ val debug = Bool() ​​​​ val debugOption = Bool() ​​​​} ​​​​val io = new Bundle { val out = new Out } ​​​​// assign DontCare to io.out.debutOption ​​​​io.out.debug := true.B ​​​​io.out.debugOption := DontCare
  • Example 2 - Use bunk connection

    ​​​​import chisel3._ ​​​​class ModWithVec extends Module { ​​​​ // ... ​​​​ val nElements = 5 ​​​​ val io = IO(new Bundle { ​​​​ val outs = Output(Vec(nElements, Bool())) ​​​​ }) ​​​​ io.outs <> DontCare ​​​​ // ... ​​​​} ​​​​class TrivialInterface extends Bundle { ​​​​ val in = Input(Bool()) ​​​​ val out = Output(Bool()) ​​​​} ​​​​class ModWithTrivalInterface extends Module { ​​​​ // ... ​​​​ val io = IO(new TrivialInterface) ​​​​ io <> DontCare ​​​​ // ... ​​​​}

Deep Dive into Connection Operators

以下引述自官方文件

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 use DecoupledIO.
Chisel

  • 直接說結論

    • Concept 1: <> is Commutative
    • Concept 2: := means assign ALL LHS signals from the RHS, regardless of the direction on the LHS.
      The := operator goes field-by-field on the LHS and attempts to connect it to the same-named signal from the RHS. If something on the LHS is actually an Input, or the corresponding signal on the RHS is an Output, you will get an error
    • Concept 3: Always Use := to assign DontCare to Wires
    • Concept 4: You can use <> or := to assign DontCare to directioned things (IOs
    • Concept 5: <> works between things with at least one known flow (An IO or child’s IO).
    • Concept 6: <> and := connect signals by field name.

    實驗的過程可以參考:[Deep Dive into Connection Operators](https://www.chisel-lang.org/chisel3/docs/explanations/connection-operators.html

Paratermeterized Bundle

在 Chisel 中,有一種特殊的用法可以讓 Bundle 可以被參數化,通常是用在 bit width 的參數化。

  • Example
    ​​​​abstract class myBundle(param: MyParam) extends GenericParameterizedBundle(param) ​​​​class myBundleA(param: MyParam) extends myBundle(param){ ​​​​ val data = UInt(param.width.W) ​​​​}

Aggregate Register and Partially Reset

在 Chisel 中,可以利用一些語法,配合 scala 的特性,來讓一個 Reg 包含不同的 data type,以下舉兩個例子,並且同時示範如何做 Partially reset(initialization)。

  • Example 1
    透過 Bundle + .Lit() 的語法來初始化 aggregate register

    ​​​​import chisel3._ ​​​​import chisel3.experimental.BundleLiterals._ ​​​​class MyBundle extends Bundle { ​​​​ val foo = UInt(8.W) ​​​​ val bar = UInt(8.W) ​​​​} ​​​​class MyModule extends Module { ​​​​ // Only .foo will be reset, .bar will have no reset value ​​​​ val reg = RegInit((new MyBundle).Lit(_.foo -> 123.U)) ​​​​}
  • Example 2
    透過 Wire 和 initial value 的方式

    ​​​​class MyModule2 extends Module { ​​​​ val reg = RegInit({ ​​​​ // The wire could be constructed before the reg rather than in the RegInit scope, ​​​​ // but this style has nice lexical scoping behavior, keeping the Wire private ​​​​ val init = Wire(new MyBundle) ​​​​ init := DontCare // No fields will be reset ​​​​ init.foo := 123.U // Last connect override, .foo is reset ​​​​ init ​​​​ }) ​​​​}
  • Example 3
    另外一種寫法,單純透過 Wire 的宣告

    ​​​​val regWithValidBit = RegInit({ ​​​​ val init = Wire(Valid(UInt(bits.W))) ​​​​ init := DontCare ​​​​ init // return ​​​​ })

Optional IO port

有時候一個 module 中的某些 IO ports 可能是為了 debug 用的,當 debug 完成之後,我們就不會想要在 verilog code 中生成這些 ports,如何在 Chisel 中表達這些 optional IO ports?以下舉一個例子

  • Example
    利用傳入 Boolean 參數,來決定某個 ports 是否要被生成,利用 if (param) Some_port else None 這個語法

    ​​​​import chisel3._ ​​​​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 ​​​​ } ​​​​}

    特別注意上面的 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

    ​​​​import chisel3._ ​​​​class ModuleWithOptionalIO(flag: Boolean) extends Module { ​​​​ val in = if (flag) Some(IO(Input(Bool()))) else None ​​​​ val out = IO(Output(Bool())) ​​​​ out := in.getOrElse(false.B) ​​​​}

    上面的 getOrElse(false.B) 代表如果 io.in 有被定義的話,那就會傳回 io.in,反之,就會傳回 false.B,所以括號內的 value 代表 default value。

Strip directions from a bidirectional Bundle

有些 ports 可能包含雙向的 signals,像是被 Decoupled() 包起來的 port,有時候我們可能會遇到下面的使用場景

import chisel3.util.Decoupled class BadRegConnect extends Module { val io = IO(new Bundle { val enq = Decoupled(UInt(8.W)) }) val monitor = Reg(chiselTypeOf(io.enq)) monitor := io.enq }

但是這樣做,FIRRTL Compiler 會報錯

ChiselStage.emitVerilog(new BadRegConnect) // firrtl.passes.CheckHighFormLike$RegWithFlipException: @[cookbook.md 715:20]: [module BadRegConnect] Register monitor cannot be a bundle type with flips.

因此,可以透過 chiselTypeOf() 這個 API 解決這個問題,僅僅拿到 signal 的 type 就好,不要管它的方向

import chisel3.util.Decoupled class CoercedRegConnect extends Module { val io = IO(new Bundle { val enq = Flipped(Decoupled(UInt(8.W))) }) // Make a Reg which contains all of the bundle's signals, regardless of their directionality val monitor = Reg(Output(chiselTypeOf(io.enq))) // Even though io.enq is bidirectional, := will drive all fields of monitor with the fields of io.enq monitor := io.enq }

How to write a testbench in Chisel?

之前有一個比較舊的框架叫做 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

以下是 ScalaTest 的簡單用法,你可以寫一段類似敘述或是 spec 的語句來描述目前這個 test 要做哪些事情,應該要有哪些行為產生,像是下面的 "Integers" should "add" in {...},而花括號 {...} 裡面則為 test codes。

import org. scalatest ._ import org. scalatest .flatspec. AnyFlatSpec import org. scalatest .matchers.should. Matchers class ExampleTest extends AnyFlatSpec with Matchers { "Integers" should "add" in { val i = 2 val j = 3 i + j should be (5) } "Integers" should "multiply" in { val a = 3 val b = 4 a * b should be (12) } }

而要執行 test script,則可以直接在 terminal 輸入 $ sbt test,他會執行全部的測試檔案,如果不想要執行全部的測試的話,則可以輸入像是 $ sbt "testOnly ExampleTest" 來執行單一測試。

而上面的測試的輸出結果如下

[info] ExampleTest: [info] Integers [info] - should add [info] Integers [info] - should multiply [info] ScalaTest [info] Run completed in 119 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 2, Failed 0, Errors 0, Passed 2

Chiseltest

使用 ChiselTest 和使用 ScalaTest 的方式差不多,只是多了一些由 Chisel 提供的 API 而已。以下為一個簡單的例子

import chisel3._ import chiseltest ._ import org. scalatest .flatspec. AnyFlatSpec // define module class DeviceUnderTest extends Module { val io = IO(new Bundle { val a = Input(UInt (2.W)) val b = Input(UInt (2.W)) val out = Output(UInt (2.W)) }) io.out := io.a & io.b } // define testbench class SimpleTest extends AnyFlatSpec with ChiselScalatestTester { "DUT" should "pass" in { test(new DeviceUnderTest ) { dut => dut.io.a.poke (0.U) dut.io.b.poke (1.U) dut.clock.step () println("Result is: " + dut.io.out.peek ().toString) dut.io.a.poke (3.U) dut.io.b.poke (2.U) dut.clock.step () println("Result is: " + dut.io.out.peek ().toString) } } }

Run your testbench

# run all testbenches $ sbt test # run a specific testbench $ sbt "testOnly SimpleTest"

simulation results in terminal

... Result is: UInt<2>(0) Result is: UInt<2>(2) [info] SimpleTest: [info] DUT [info] - should pass [info] ScalaTest ...

在上面的例子,我們是用 dut.io.poke() 加上 toString() 的方式去得到 port value,還有另外一種方式,是利用 expect()

class SimpleTestExpect extends AnyFlatSpec with ChiselScalatestTester { "DUT" should "pass" in { test(new DeviceUnderTest ) { dut => dut.io.a.poke (0.U) dut.io.b.poke (1.U) dut.clock.step () dut.io.out.expect (0.U) dut.io.a.poke (3.U) dut.io.b.poke (2.U) dut.clock.step () dut.io.out.expect (2.U) } } }

Testing result in terminal

[info] SimpleTestExpect: [info] DUT [info] - should pass [info] ScalaTest [info] Run completed in 1 second, 85 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 1, Failed 0, Errors 0, Passed 1

Generate Waveforms while Testing

如果要產生 .vcd 檔案,基本上有兩種方式可以選擇。

  • Option 1 - Add commands in terminal
    加上 -DwriteVcd=1

    ​​​​sbt "testOnly SimpleTest -- -DwriteVcd=1"

    這種方式會一次產生所有 test cases 的波形,如果不想要一次產生所有波形可以使用 option 2。

  • Option 2 - Use ChiselTest API
    利用 withAnnotations (Seq( WriteVcdAnnotation ))

    ​​​​class WaveformTest extends AnyFlatSpec with ChiselScalatestTester { ​​​​ "Waveform" should "pass" in { ​​​​ test(new DeviceUnderTest) ​​​​ .withAnnotations(Seq(WriteVcdAnnotation)) { dut => ​​​​ dut.io.a.poke(0.U) ​​​​ dut.io.b.poke(0.U) ​​​​ dut.clock.step() ​​​​ dut.io.a.poke(1.U) ​​​​ dut.io.b.poke(0.U) ​​​​ dut.clock.step() ​​​​ dut.io.a.poke(0.U) ​​​​ dut.io.b.poke(1.U) ​​​​ dut.clock.step() ​​​​ dut.io.a.poke(1.U) ​​​​ dut.io.b.poke(1.U) ​​​​ dut.clock.step() ​​​​ } ​​​​ } ​​​​}

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。

  • Scala Style
    ​​​​// Chisel provides a custom string interpolator p"..." ​​​​val myUInt = 33.U ​​​​printf(p"myUInt = $myUInt") // myUInt = 33 ​​​​// Does not interpolate the second string ​​​​val myUInt = 33.U ​​​​printf("my normal string" + p"myUInt = $myUInt") ​​​​// simple formatting ​​​​val myUInt = 33.U ​​​​// Hexadecimal ​​​​printf(p"myUInt = 0x${Hexadecimal(myUInt)}") // myUInt = 0x21 // myUInt = 0x21 ​​​​// Binary ​​​​printf(p"myUInt = ${Binary(myUInt)}") // myUInt = 100001 // myUInt = 100001 ​​​​// Character ​​​​printf(p"myUInt = ${Character(myUInt)}") // myUInt = !
  • C Style
    ​​​​val myUInt = 32.U ​​​​printf("myUInt = %d", myUInt) // myUInt = 32

Create diagram of your own module

Introduction to Chisel/FIRRTL Diagramming Project

在 Chisel 的子計畫中,有一個叫做 diagrammer 的 project,他是基於 GraphViz 這個繪圖框架,可以根據 .fir 檔案自動生成電路相關的 block diagram 或是整個 module 的 hierarchy diagram。

  • Examples from official documentation
    • Top Level Example
    • Module Example

How to use

  1. 先安裝 diagrammer

    • Clone repo

      ​​​​​​​​$ git clone https://github.com/freechipsproject/diagrammer ​​​​​​​​$ cd diagrammer
    • 如果沒有安裝 GraphViz,則需要一併安裝,因為 diagrammer 是基於 GraphViz 的 tool

      ​​​​​​​​# under linux ​​​​​​​​$ sudo apt-get install graphviz ​​​​​​​​# check ​​​​​​​​$ dot -V
  2. Hwo to use

    • Step#1: 你需要先生成電路的 .fir 檔案

    • Setp#2: 來移動到 diagrammer 的資料夾下面,利用 diagram.sh 生成 .svg 檔案

      ​​​​​​​​$ ./diagram.sh -i path/to/your/fir_file

      如果你沒有指定路徑的話,那預設會在 diagrammer 的資料夾底下,所以這個 tool 有提供一些 options 可以使用,像是利用 -t 指定生成圖檔要放在哪個路徑之下。

      • Options
        • -i, firrtl-source set the source firrtl to work on
        • -t, targer-dir sets the output directory for the svg
        • -s, start-module sets the module name where the graphs will start being generated. The default is at the top
        • -o, open-program sets the open program, default is open, set to empty to tell it not to do open
        • -j, just-top-level generates just the top level diagram

Coding Style Guideline for Chisel

Naming Convention

Coding Style

Verilator - Verilog Simulator