FPGA開発日記

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

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ファイルを生成してみる。