FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Spike-ISSによるRISC-V向けにコンパイルしたアプリケーション実行(1)

忙しくてずいぶんと放置してしまっていた。RISC-V向けにアプリケーションをコンパイルして、動作させてみるテスト。

いきなり大きなプログラムを実行して、HiFive1を壊してしまったりしたので、今回は慎重に生きたい。まずはISSなどであらかじめプログラムの動作の様子を見て、それからHiFive1にダウンロードするという手順を取ろう。

以前に作ったC言語で記述したMNIST学習プログラム。RISC-V向けにコンパイルしてみた。と言っても、やったことはx86用のgccを渡すべきところを、riscv64-unknown-elf-gccに置き換えただけである。

github.com

CC := riscv64-unknown-elf-gcc

include ../bp/Makefile

printfなどが入っていても、spikeを利用するときにpkオプションを指定すると、proxy kernel付きで実行してくれるようになる。proxy kernelはSyscallの代理を行うもおんで、HTIF(host target interface)の肩代わりをするため、printf()などが使えるようになるというわけだ。

  • Running simulations

Running simulations · lowRISC

以下は、MNISTの学習プログラム。5000回の学習の後に、10000個の画像を判別テストして、正答率を図っている。50000回程度学習させることによって、90%程度の正解率に持って行くシミュレーションが、Spike ISSでも実行できることを確認できた。

$ time spike pk train_twolayernet
2051
60000
28
28
2049
60000
=== TestNetwork ===
Correct = 1597
=== TestNetwork ===
Correct = 3726
=== TestNetwork ===
Correct = 6553
=== TestNetwork ===
Correct = 7882
=== TestNetwork ===
Correct = 8448
=== TestNetwork ===
Correct = 8629
=== TestNetwork ===
Correct = 8713
=== TestNetwork ===
Correct = 8858
=== TestNetwork ===
Correct = 8915
=== TestNetwork ===
Correct = 8908
=== TestNetwork ===
Correct = 8978
=== TestNetwork ===
Correct = 9035

ただし、これめちゃめちゃ遅い。これだけ動作させるのに10分程度かかった。次は、もう少しステップを踏みながら、HiFive1でMNISTが実行できるところまで持って行きたい。

f:id:msyksphinz:20170817010249p:plain

BOOM(Berkeley Out of Order Machine) version 2

UCBより、BOOM (Berkeley Out-of Order Machine) v.2 のアナウンスがあった。

BOOMは5ステージパイプラインのRocketCoreに対して、アウトオブオーダ、2-4命令同時発行など、性能方面に強化しているプロセッサだ。

アナウンスによると、BOOM v2は以下の点が強化されている。

  • Issue Windowを分割し、「整数命令」「メモリアクセス命令」「浮動小数点命令」用の3つのウィンドウに分割。これにより、命令発行ウィンドウが増え、命令発行能力が増える。
  • 物理レジスタファイルを整数レジスタ浮動小数レジスタに分割。レジスタを分割することで、同時読み書きするレジスタポートが減り、レジスタのサイズ減につながる。
  • レジスタリネームステージを2サイクルに分割。「デコード+リネーム1」と「リネーム2+ディスパッチ」に分割した。
  • コア内に分岐ターゲットバッファ(BTB)を格納。その他分岐予測に関する細々とした性能向上対策

ソースコードは以下から入手することができる。

github.com

f:id:msyksphinz:20170816222953p:plain

この画像はBOOM v1のパイプライン。BOOM v2の詳細資料求む。

AXIバス by Chisel

f:id:msyksphinz:20170806234951p:plain

Rocket-Chipは、主にTileLinkとAXIバスによって記述されており、TileLinkがRocketChipに近い方、AXIが外部バスに出ていく方として記述されているのだけれども、AXI4のバスもChiselで書かれているようだ。 とりあえず見てみたが、正直なんだか良く分からない。Scala苦手だ。。。

こういうのを購入しないとダメかしら。

Scalaスケーラブルプログラミング第3版

Scalaスケーラブルプログラミング第3版

ChiselでのAXIバスの定義

GenericParameterizedBundleというクラスを継承する形で定義されている。

  • rocket-chip/src/main/scala/util/GenericParameterizedBundle.scala
abstract class GenericParameterizedBundle[+T <: Object](val params: T) extends Bundle
{
...

例えば、AXIのAチャネルは以下のように定義されている。

  • rocket-chip/src/main/scala/amba/axi4/Bundles.scala
abstract class AXI4BundleA(params: AXI4BundleParameters) extends AXI4BundleBase(params)
{
  val id     = UInt(width = params.idBits)
  val addr   = UInt(width = params.addrBits)
  val len    = UInt(width = params.lenBits)  // number of beats - 1
  val size   = UInt(width = params.sizeBits) // bytes in beat = 2^size
  val burst  = UInt(width = params.burstBits)
  val lock   = UInt(width = params.lockBits)
  val cache  = UInt(width = params.cacheBits)
  val prot   = UInt(width = params.protBits)
  val qos    = UInt(width = params.qosBits)  // 0=no QoS, bigger = higher priority
  val user = if (params.userBits > 0) Some(UInt(width = params.userBits)) else None
  // val region = UInt(width = 4) // optional

  // Number of bytes-1 in this operation
  def bytes1(x:Int=0) = {
    val maxShift = 1 << params.sizeBits
    val tail = UInt((BigInt(1) << maxShift) - 1)
    (Cat(len, tail) << size) >> maxShift
  }
}

次に、RegisterRouterというレジスタスライスの定義を見てみる。 regmapという定義がされており、どうやらこれが信号がレジスタそのものになっているようだ。

  • rocket-chip/src/main/scala/amba/axi4/RegisterRouter.scala
  def regmap(mapping: RegField.Map*) = {
    val ar = bundleIn(0).ar
    val aw = bundleIn(0).aw
    val w  = bundleIn(0).w
    val r  = bundleIn(0).r
    val b  = bundleIn(0).b

    val params = RegMapperParams(log2Up((address.mask+1)/beatBytes), beatBytes, ar.bits.params.idBits + ar.bits.params.userBits)
    val in = Wire(Decoupled(new RegMapperInput(params)))

    // Prefer to execute reads first
    in.valid := ar.valid || (aw.valid && w.valid)
    ar.ready := in.ready
    aw.ready := in.ready && !ar.valid && w .valid
    w .ready := in.ready && !ar.valid && aw.valid

    val ar_extra = Cat(Seq(ar.bits.id) ++ ar.bits.user.toList)
    val aw_extra = Cat(Seq(aw.bits.id) ++ aw.bits.user.toList)
    val in_extra = Mux(ar.valid, ar_extra, aw_extra)
    val addr = Mux(ar.valid, ar.bits.addr, aw.bits.addr)
    val mask = MaskGen(ar.bits.addr, ar.bits.size, beatBytes)

    in.bits.read  := ar.valid
    in.bits.index := addr >> log2Ceil(beatBytes)
    in.bits.data  := w.bits.data
    in.bits.mask  := Mux(ar.valid, mask, w.bits.strb)
    in.bits.extra := in_extra

    // Invoke the register map builder and make it Irrevocable
    val out = Queue.irrevocable(
      RegMapper(beatBytes, concurrency, undefZero, in, mapping:_*),
      entries = 2)

    // No flow control needed
    out.ready := Mux(out.bits.read, r.ready, b.ready)
    r.valid := out.valid &&  out.bits.read
    b.valid := out.valid && !out.bits.read

    val out_id = if (r.bits.params.idBits == 0) UInt(0) else (out.bits.extra >> ar.bits.params.userBits)

    r.bits.id   := out_id
    r.bits.data := out.bits.data
    r.bits.last := Bool(true)
    r.bits.resp := AXI4Parameters.RESP_OKAY
    r.bits.user.foreach { _ := out.bits.extra }

    b.bits.id   := out_id
    b.bits.resp := AXI4Parameters.RESP_OKAY
    b.bits.user.foreach { _ := out.bits.extra }
  }

Ubuntu on Windows導入(1)

既に誰かがやっているとは承知の上で、Ubuntu on Windowsの上にRISC-Vの開発環境を構築しておきたくて、初めてWindows Creator’s Updateなどという謎のアップデートをインストールしたし、インストールに時間かかりすぎだし、どうにかUbuntuのインストールも完了した。

f:id:msyksphinz:20170815011239p:plain

起動してみると、なるほどUbuntuだなという感じもあるが、とりあえずインストールするまでのステップが長すぎである。 もうちょっと気楽に入れられるのかと思ったら、アップデートに時間がかかるし、なんだかよく見たらデスクトップの下の方に変なリビジョン番号とか現れるし、どうすればいいんだこりゃ。

とりあえず、RISC-VのGCCとかをインストールして使えるようにするまでが目標なので、もうちょっと様子を見たい。

dotfilesで環境を構築する

tmuxとか、emacsのinit.elとか、tmuxのtmux.confとかを導入するためにdotfilesの環境を作っているのだけれども、これらはUbuntu on Windowsでうまく動作するのかしら。

とりあえず、gitはデフォルトで入っていたので自分のbitbucketのリポジトリからdotfilesをcloneして、環境を構築してみた。 Emacsは、sudo apt install emacsで導入できるようだ。

あと、一応tmuxを導入したけど、どうもこれ使い物にならない。そもそもベースがDOSプロンプトみたいだ。Puttyみたいなインタフェースじゃないので、使いにくいし、なんかtmuxのバーが上に上がってくる。なんだか良く分からないから、あまり使う気にならないね。これ本当に正式リリースなの?

f:id:msyksphinz:20170815014859p:plain

とりあえず、Creator’s Updateをすると以下のようにVirtualBoxが立ち上がらなくなったのでマジ最悪である。

f:id:msyksphinz:20170815013626p:plain

一応、VirtualBoxを最新版にアップデートすると直ったので一件落着。焦った。

RocketChipをカスタマイズするためのチュートリアル(1)

RocketChipはChiselで記述されており、その実装はオープンになっているので、Chiselを操ることができればRocketChipを自由にカスタマイズすることができる。

さらに、RISC-VのGCCをカスタマイズすれば専用命令を追加することができ、自分の好きな命令を追加することができる。 これらのRocketChipのカスタマイズや、GCCの追加などをまとめてチュートリアルとして公開されているのが、lowRISCプロジェクトだ。

lowRISCプロジェクトはケンブリッジ大学で始まったプロジェクトで、RISC-VをベースとしたSoCを開発することを目的としている。

f:id:msyksphinz:20170813132837p:plain

lowRISC · lowRISC

lowRISC is creating a fully open-sourced, Linux-capable, RISC-V-based SoC, that can be used either directly or as the basis for a custom design. We aim to complete our SoC design this year.

このチュートリアルでは、キャッシュメモリに対してタグを挿入するためのRISC-Vカスタマイズの方法について説明してある。

このチュートリアルは2015年に記述されたものでずいぶんと古いが、RISC-Vのカスタマイズの方法を説明しているものとして貴重だ。

このチュートリアルでは、キャッシュメモリに対してタグを追加する。さらにタグをロードストアするための命令を追加する。 この追加したタグによって、キャッシュラインに対してより柔軟な属性を追加することができる。

RocketChipにタグ付きメモリを追加するためのチュートリアル

まず、本チュートリアルで何をしようとしているのか、ということだが、RocketChipに搭載されている512ビットのL1キャッシュタグに対してタグを追加する。 このタグは新規追加命令(ltag,stag)によってアクセスすることが出来る。 下記の図はチュートリアルから引用したものだが、512ビットのタグに対して64ビット毎にタグビットを追加する。 このタグ情報はメインデータメモリに対しても保存されており、タグ情報はメインデータメモリの特定の領域に保存されるようになる。 タグの転送による性能低下を防ぐために、RocketChipに対してタグキャッシュメモリを追加して、外部にタグ情報を何度も取得しに行く必要性を低減している。 タグアドレスのベースとしてはTagBaseAddrc=0x0F00_0000に設定してある。

f:id:msyksphinz:20170813134322p:plain

(※ http://www.lowrisc.org/docs/tagged-memory-v0.1/tags/ より引用)

f:id:msyksphinz:20170813134338p:plain

(※ http://www.lowrisc.org/docs/tagged-memory-v0.1/tags/ より引用)

新規命令としては、Load Tag、Store Tag命令を追加する。

# load the tag associated with DW located at rs1 + imm to register rd
ltag rd, imm(rs1)

# store the tag in rd to the DW located at rs1 + imm
stag rd, imm(rs1)

タグ付きキャッシュメモリをサポートするための命令を追加する

タグ付きキャッシュメモリをサポートするために、RocketChipへの命令追加と、GCCへの命令追加を行う。

新規命令としては、RISC-VのReservedの場所に命令を追加している。

f:id:msyksphinz:20170813135554p:plain

(※ http://www.lowrisc.org/docs/tagged-memory-v0.1/new-instructions/より引用)

チュートリアルではrocket/src/main/scala/instructions.scala となっているが、現在のRocketChipの実装では src/main/scala/rocket/Instructions.scala に移動しているようだ。

diff --git a/src/main/scala/rocket/Instructions.scala b/src/main/scala/rocket/Instructions.scala
index af61ea7..8386c19 100644
--- a/src/main/scala/rocket/Instructions.scala
+++ b/src/main/scala/rocket/Instructions.scala
@@ -214,6 +214,9 @@ object Instructions {
   def RDINSTRETH         = BitPat("b11001000001000000010?????1110011")
   def SCALL              = BitPat("b00000000000000000000000001110011")
   def SBREAK             = BitPat("b00000000000100000000000001110011")
+  def LTAG               = BitPat("b?????????????????000?????1010111")
+  def STAG               = BitPat("b?????????????????001?????1010111")
+
 }
 object Causes {
   val misaligned_fetch = 0x0

次に、制御信号を生成する記述を追加する。

diff --git a/src/main/scala/rocket/IDecode.scala b/src/main/scala/rocket/IDecode.scala
index bc88b9f..d16e498 100644
--- a/src/main/scala/rocket/IDecode.scala
+++ b/src/main/scala/rocket/IDecode.scala
@@ -325,3 +325,10 @@ class RoCCDecode(implicit val p: Parameters) extends DecodeConstants
     CUSTOM3_RD_RS1->    List(Y,N,Y,N,N,N,N,Y,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N),
     CUSTOM3_RD_RS1_RS2->List(Y,N,Y,N,N,N,Y,Y,A2_ZERO,A1_RS1, IMM_X, DW_XPR,FN_ADD,   N,M_X,        MT_X, N,N,N,N,N,Y,CSR.N,N,N,N,N))
 }
+
+class TagDecode(implicit val p: Parameters) extends DecodeConstants
+{
+  val table: Array[(BitPat, List[BitPat])] = Array(
+    LTAG->              List(Y,N,N,N,N,N,N,Y,A2_IMM, A1_RS1, IMM_I, DW_XPR,FN_ADD,   Y,M_XRD,      MT_T, N,N,Y,N,N,N,CSR.N,N,N,N,N),
+    STAG->              List(Y,N,N,N,N,N,Y,Y,A2_IMM, A1_RS1, IMM_S, DW_XPR,FN_ADD,   Y,M_XWR,      MT_T, N,N,N,N,N,N,CSR.N,N,N,N,N))
+}

この信号線の意味だが、IDecode.scala に説明が入っている。

  def default: List[BitPat] =
                //           jal                                                                 renf1             fence.i
                //   val     | jalr                                                              | renf2           |
                //   | fp_val| | renx2                                                           | | renf3         |
                //   | | rocc| | | renx1     s_alu1                          mem_val             | | | wfd         |
                //   | | | br| | | | s_alu2  |       imm    dw     alu       | mem_cmd   mem_type| | | | div       |
                //   | | | | | | | | |       |       |      |      |         | |           |     | | | | | wxd     | fence
                //   | | | | | | | | |       |       |      |      |         | |           |     | | | | | | csr   | | amo
                //   | | | | | | | | |       |       |      |      |         | |           |     | | | | | | |     | | | dp
                List(N,X,X,X,X,X,X,X,A2_X,   A1_X,   IMM_X, DW_X,  FN_X,     N,M_X,        MT_X, X,X,X,X,X,X,CSR.X,X,X,X,X)

つまり、ここでは、MT_Tというメモリアクセスタイプを追加したということである。

diff --git a/src/main/scala/rocket/Consts.scala b/src/main/scala/rocket/Consts.scala
index bde6201..13617ff 100644
--- a/src/main/scala/rocket/Consts.scala
+++ b/src/main/scala/rocket/Consts.scala
@@ -16,6 +16,7 @@ trait ScalarOpConstants {
   def MT_BU = UInt("b100")
   def MT_HU = UInt("b101")
   def MT_WU = UInt("b110")
+  def MT_T  = UInt("b111") // tag
   def mtSize(mt: UInt) = mt(MT_SZ-2, 0)
   def mtSigned(mt: UInt) = !mt(MT_SZ-1)

こんな感じだろうか。

次に、このデコードテーブルを新たに追加する。

diff --git a/src/main/scala/rocket/RocketCore.scala b/src/main/scala/rocket/RocketCore.scala
index 0940947..86ca02f 100644
--- a/src/main/scala/rocket/RocketCore.scala
+++ b/src/main/scala/rocket/RocketCore.scala
@@ -116,6 +116,7 @@ class Rocket(implicit p: Parameters) extends CoreModule()(p)
     (usingRoCC.option(new RoCCDecode)) ++:
     ((xLen > 32).option(new I64Decode)) ++:
     (usingVM.option(new SDecode)) ++:
+    Seq(new TagDecode) ++:
     (usingDebug.option(new DebugDecode)) ++:
     Seq(new IDecode)
   } flatMap(_.table)

アセンブラのアップデート

GCCアセンブリ言語のサポートを行う。まずはbinutilsによるアセンブリのアップデートから。

diff --git a/include/opcode/riscv-opc.h b/include/opcode/riscv-opc.h
index f80037b..667a80b 100644
--- a/include/opcode/riscv-opc.h
+++ b/include/opcode/riscv-opc.h
@@ -499,6 +499,11 @@
 #define MASK_CUSTOM3_RD_RS1  0x707f
 #define MATCH_CUSTOM3_RD_RS1_RS2 0x707b
 #define MASK_CUSTOM3_RD_RS1_RS2  0x707f
+#define MATCH_STAG 0x1057
+#define MASK_STAG 0x707f
+#define MATCH_LTAG 0x57
+#define MASK_LTAG 0x707f
+
 #define CSR_FFLAGS 0x1
 #define CSR_FRM 0x2
 #define CSR_FCSR 0x3
@@ -975,6 +980,9 @@ DECLARE_INSN(custom3_rs1_rs2, MATCH_CUSTOM3_RS1_RS2, MASK_CUSTOM3_RS1_RS2)
 DECLARE_INSN(custom3_rd, MATCH_CUSTOM3_RD, MASK_CUSTOM3_RD)
 DECLARE_INSN(custom3_rd_rs1, MATCH_CUSTOM3_RD_RS1, MASK_CUSTOM3_RD_RS1)
 DECLARE_INSN(custom3_rd_rs1_rs2, MATCH_CUSTOM3_RD_RS1_RS2, MASK_CUSTOM3_RD_RS1_RS2)
+
+DECLARE_INSN(stag, MATCH_STAG, MASK_STAG)
+DECLARE_INSN(ltag, MATCH_LTAG, MASK_LTAG)
 #endif
 #ifdef DECLARE_CSR
 DECLARE_CSR(fflags, CSR_FFLAGS)
diff --git a/opcodes/riscv-opc.c b/opcodes/riscv-opc.c
index 8343198..a852ae5 100644
--- a/opcodes/riscv-opc.c
+++ b/opcodes/riscv-opc.c
@@ -623,6 +623,9 @@ const struct riscv_opcode riscv_opcodes[] =
 {"sfence.vma","I",   "s,t",  MATCH_SFENCE_VMA, MASK_SFENCE_VMA, match_opcode, 0 },
 {"wfi",       "I",   "",     MATCH_WFI, MASK_WFI, match_opcode, 0 },

+{"ltag",         "I",   "d,o(s)", MATCH_LTAG, MASK_LTAG, match_opcode, 0 },
+{"stag",         "I",   "t,q(s)", MATCH_STAG, MASK_STAG, match_opcode, 0 },
+
 /* Terminate the list.  */
 {0, 0, 0, 0, 0, 0, 0}
 };

Chiselを記述して回路を作成しテストする(2)

f:id:msyksphinz:20170806234951p:plain

Chiselを使って、独自の回路を作成しテストしてみよう。前回はチュートリアルを実行してみただけだったが、次は独自の回路を作成してみる。

作ってみるのは、16個の32ビット整数を受け取り、その16要素をすべて加算し総和を求めるプログラムだ。 ツリー上に作っていけばより高速なのだが、今回は簡単化のため1要素ずつ加算していき、16サイクルかけて総和を求める構成にしてみた。以下のような回路になることを想定している。

f:id:msyksphinz:20170811234356p:plain

計算が始まるとひとつずつレジスタの要素をシフトしていき、先頭のレジスタの値を加算していく。これだけなら、超単純だ。

reduction.scala プログラムの作成

Chiselで以下のようなプログラムを記述した。

package example

import chisel3._

class reduction extends Module {
  val io = IO(new Bundle {
    val start = Input(Bool())
    val a  = Vec(16, Input(UInt(32.W)))
    val z  = Output(UInt(32.W))
  })

  val reg_a = Reg (Vec(16, UInt(32.W)))
  val reg_z = Reg (UInt(32.W))

  reg_z := reg_z + reg_a(0)
  for (idx <- 0 until 15)
    reg_a(idx) := reg_a(idx+1)
  reg_a(15) := 0.U

  when (io.start) { reg_a := io.a; reg_z := 0.U }

  io.z := reg_z
}

大体見ればわかると思う。入力は32ビットUIntが16個並んだベクトル、出力は32ビットのUIntだ。

まず、io.startがアサートされると、入力値をとりあえずreg_aに取り込む。 次に1サイクルごとに、reg_a(idx) := reg_a(idx+1) とするとことで、レジスタの要素を1つずつずらしていく。そして、レジスタの先頭の値と、現在の総和の値reg_zを加算していくというわけだ (reg_z := reg_z + reg_a(0)) 。

これを、前回紹介したchisel-templateプロジェクトの、./src/main/scala/example/ に保存した。ファイル名はreduction.scalaとした。

reduction回路のテストプログラム作成

このreduction回路をテストするためのプログラムを書こう。

前回と同様に、chisel-templateリポジトリ./src/test/scala/example/test/に、テストプログラムを作成した。ファイル名は reductionUnitTest.scala とした。

import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
import chisel3._
import example.reduction

class reductionUnitTester(c: reduction) extends PeekPokeTester(c) {
  private val reduction = c

  for(i <- 1 to 2 by 1) {
    val A = new Array[Int](16)
    for (idx <- 0 until 16) {
      poke(reduction.io.a(idx), idx)
      A(idx) = idx
    }

    poke(reduction.io.start, 1)
    step (1)
    poke(reduction.io.start, 0)

    step (16)

    val total = A.sum

    expect(reduction.io.z, total)
  }
}

class reductionTester extends ChiselFlatSpec {
  private val backendNames = Array[String]("firrtl", "verilator")
  for ( backendName <- backendNames ) {
    "reduction" should s"calculate proper greatest common denominator (with $backendName)" in {
      Driver(() => new reduction, backendName) {
        c => new reductionUnitTester(c)
      } should be (true)
    }
  }
}

とりあえず今回のテストでは、reduction回路の入力値は、順に0,1,…,15とした。これなら簡単だ。これはpokeの繰り返しで実現していることが分かる。

次に、io.start信号を1サイクルだけアサートして、計算をスタートさせる。そのとき、計算には16サイクルかかるので、step(16)として計算が終了するまで待っている。

    poke(reduction.io.start, 1)
    step (1)
    poke(reduction.io.start, 0)

    step (16)

最後に、Scalaで計算した予測値との比較をする。予測値は、単純に配列の中身を加算するだけなので、val total = A.sumで終わりだ。scala便利。

sbt testを実行するとGCDと同様にテストが開始される。

[info] [1.303] RAN 34 CYCLES PASSED
[info] reductionTester:
[info] reduction
[info] - should calculate proper greatest common denominator (with firrtl)
[info] reduction
[info] - should calculate proper greatest common denominator (with verilator)
Enabling waves..
Exit Code: 0

生成されたVerilogファイルを確認してみよう。./test_run_dir/example.test.reductionTester739090423/reduction.vに格納されている。

wc ./test_run_dir/example.test.reductionTester739090423/reduction.v
 272  819 6656 ./test_run_dir/example.test.reductionTester739090423/reduction.v

とりあえず長いVerilogファイルが生成されている。あと、さすが機械生成されただけあって、非常に読みにくい。

Chiselを記述して回路を作成しテストする(1)

前回までで、Chiselのイントロダクションを完了した。 しかし実際に使ってみなければどのように使えばよいのかは分からない。 いろいろ作って試してみよう。

Chiselを簡単に試すことのできるプロジェクトテンプレート

github.com

上記のプロジェクトを使えば、Chiselをいろいろ試すことができる。 簡単なチュートリアルも付いているので、それを見ながら試すことができる。

Chiselをコンパイルするためのsbtインストール

ChiselはScalaをベースに記述されているので、もちろんScalaのビルド環境が必要になる。 正直Scalaについてはさっぱり分からないが、Ubuntuを使ったらビルド環境は簡単に構築することができる。apt-get installするだけである。

以下のページを参考にした。

sbt Reference Manual — Linux への sbt のインストール

 echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list
 git fetch originsudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
 sudo apt-get update
 sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
 sudo apt-get update
 sudo apt-get install sbt

sbtをインストールしたら、次はChisel Project TemplateをCloneしてみる。

git clone https://github.com/ucb-bar/chisel-template.git
cd chisel-template

Chiselをプロジェクトを実行してみる。

sbt test

すると、GCDを動作させる回路が生成され、テストが実行される。Passしたようだ。

STARTING test_run_dir/example.test.GCDTester168775820/VGCD
[info] [0.000] SEED 1502420920692
Enabling waves..
Exit Code: 0
[info] [0.135] RAN 1102 CYCLES PASSED
[info] GCDTester:
[info] GCD
[info] - should calculate proper greatest common denominator (with firrtl)
[info] GCD
[info] - should calculate proper greatest common denominator (with verilator)
[info] ScalaTest
[info] Run completed in 3 seconds, 540 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
[success] Total time: 31 s, completed 2017/08/11 12:08:42

このGCDを計算するソースコードとテストプログラムは以下に置かれている。

GCDプログラム

gcdはChiselで以下のように記述されている。

class GCD extends Module {
  val io = IO(new Bundle {
    val a  = Input(UInt(16.W))
    val b  = Input(UInt(16.W))
    val e  = Input(Bool())
    val z  = Output(UInt(16.W))
    val v  = Output(Bool())
  })

  val x  = Reg(UInt())
  val y  = Reg(UInt())

  when (x > y) { x := x - y }
    .otherwise { y := y - x }

  when (io.e) { x := io.a; y := io.b }
  io.z := x
  io.v := y === 0.U
}

まずは前回までのチュートリアルを見ながら、ソースコードを読み取る。

  • 入出力ポート

GCDの回路では、入力としてa:16bit, b:16bit, e:1bitを定義している。a,bはGCDの初期値だが、eは計算開始を示すスタート制御信号である。

出力としては、GCDの計算終了したことを示すv:1bitであり、そのときの答えをz:16bit として定義している。

すぐに分かるが、レジスタとしてそれぞれxyを定義している。

  • ScalaのSInt、IntとChiselのUIntは異なる

Scalaの型とは別に、ChiselではUIntという型を定義している。 これはそのままハードウェアのwireregに変換される方と考えてよい。 基本的にScalaの整数型とUIntとは別物だ。互換性が無く、変換するためには変換用のメソッドをかませなければならない。

基本的にChiselの回路定義中は、UIntを使って書くのが望ましい。 一方で、テストプログラムや周りのサポートプログラムでは、ScalaのIntを使って記述しても構わない。

  • GCDの計算部分

入出力ポートにはio.xなどという記述でアクセスする。 io.eがアサートされた場合に、まずは初期値を内部レジスタx, yに格納する。

次に、when以下で記述されている記述でxyを計算していく。 :=は、レジスタへの代入と考えてほぼ間違いない。xをアップデートしているので、1サイクル毎に計算し、その結果をxyに格納していく。

最後に、yが0になった時点で終了という訳だ。

  • GCD回路の計算時間

つまり、この回路はGCDを計算するのに、yが0になるまでのステップ分サイクル数が必要になる。

GCDのテストプログラム

作成したテストプログラムをテストするために、以下のScalaテストプログラムが用意されている。

テストプログラムの構造

f:id:msyksphinz:20170811124208p:plain

GCD回路のテストプログラムは以下のようになっており、

  • scala自身でGCDを計算して答え合わせをするためのプログラム computeGcd
  • computeGcdを呼び出して実際にテストを実行するプログラム

に分けられている。computeGcdは答えとしてGCDの計算結果と、計算にかかったステップ数(depth)を返すような仕組みになっている。

class GCDUnitTester(c: GCD) extends PeekPokeTester(c) {
  /**
    * compute the gcd and the number of steps it should take to do it
    *
    * @param a positive integer
    * @param b positive integer
    * @return the GCD of a and b
    */
  def computeGcd(a: Int, b: Int): (Int, Int) = {
    var x = a
    var y = b
    var depth = 1
    while(y > 0 ) {
      if (x > y) {
        x -= y
      }
      else {
        y -= x
      }
      depth += 1
    }
    (x, depth)
  }

  private val gcd = c

  for(i <- 1 to 40 by 3) {
    for (j <- 1 to 40 by 7) {

      poke(gcd.io.a, i)
      poke(gcd.io.b, j)
      poke(gcd.io.e, 1)
      step(1)
      poke(gcd.io.e, 0)

      val (expected_gcd, steps) = computeGcd(i, j)

      step(steps - 1) // -1 is because we step(1) already to toggle the enable
      expect(gcd.io.z, expected_gcd)
      expect(gcd.io.v, 1)
    }
  }
}

まず、pokeというのは、Chiselで記述した回路の入力ポートに対してデータを与える。まず、

      poke(gcd.io.a, i)   // 入力ポートaに対して初期値を設定
      poke(gcd.io.b, j)   // 入力ポートbに対して初期値を設定
      poke(gcd.io.e, 1)   // 計算開始
      step(1)             // 1サイクル待つ
      poke(gcd.io.e, 0)   // 計算開始信号をdeassert

computeGcdで必要サイクル数まで計算できているので、そのサイクル数分だけ待って、(step(steps - 1)) 計算結果を確認する。

計算結果を突き合わせるのには、expect()を使う。 第一引数が回路側の答え、第2引数がテストプログラムの答えだ。 つまり第1引数の型がUInt、第2引数の型がIntであることを想定している(逆にしたら動作しなかった)。

何回かテストを行って、すべて回路の答えとテストプログラムの答えが一致したら合格、というわけだ。

さて、ここまででGCD回路をChiselで記述して、テストする方法は分かった。 次に、実際に自分で数値ベクトルの値をリダクションして加算する回路をChiselで記述して、Scalaを使ってテストし、Verilogファイルを生成してみる。