ソフト屋のための SpinalHDL FPGA 設計入門(その2)

投稿者: | 2020/01/01

How to begin a simple SpinalHDL project and simulate it.

本年もよろしくお願いいたします。

前回は SpinalHDL でデジタル PWM 回路を設計してみました。今回は、それをシミュレーションで動作させ、期待通りの波形が出力されるか見てみましょう。

前回までの流れ

  1. 初めての SpinalHDL: デジタル PWM を SpinalHDL で書いてみよう

SpinalHDL プロジェクトファイルの準備

前回、SpinalHDL のコードを示しましたが、具体的にどのようにして新規プロジェクトファイルを作るかについては説明しませんでした。まず、そこから説明しましょう。

SpinalHDL では sbt と呼ばれるビルドツールを使ってプロジェクトを作成することになっています。sbt は主に Scala や Java のソフトウェアプロジェクトを、コマンドラインで設計するためのプロジェクトフレームワークです。sbt ではビルドのために build.sbt というファイル(make でいうところの makefile みたいなもの)を用意する必要がありますが、SpinalHDL 用にこれを一から用意するのは面倒ですので、SpinalHDL の GitHub ではテンプレートプロジェクトを配布しています。これを利用しましょう。ここからダウンロード(git clone)できます。

git clone すると、次のようなディレクトリとファイルが作成されます。(今回はコミット 13cd788 を使用)

.
├── README.md
├── build.sbt
├── project
│   ├── build.properties
│   └── plugins.sbt
└── src
    └── main
        └── scala
            └── mylib
                ├── MyTopLevel.scala
                └── MyTopLevelSim.scala

実際にわれわれがいじることになるのは、src/main/scala より下のファイルです。余裕があったら、MyTopLevel.scala と MyTopLevelSim.scala を覗いてみると参考になるでしょう。

通常 Scala 言語のプロジェクトでは、main() 関数を持つクラスを一つだけ定義し、その場合は上記ディレクトリのトップで sbt run コマンドを実行するとソフトウェアがビルド、および実行されます。(Scala のチュートリアルを参考ください。) しかし、SpinalHDL で設計をしていると多くの main() 関数を書くことがあり、その場合はクラス名を指定して実行することになります。そのやり方は後ほど説明しましょう。

Pwm クラスの記述と Verilog HDL への変換

それでは、前回示した Pwm クラスのコードをプロジェクトに納めてみましょう。以下の内容をコピーし、src/main/scala/PwmLed.scala として保存します。(Pwm.scala でも良いのですが、最終的に LED ホワホワのための PWM LED を設計したいので、そのような名前にしています。)

// package com.flogics.spinal.pwmled

import spinal.core._

class Pwm(size: Int) extends Component {
  val io = new Bundle {
    val width = in UInt(size bits)
    val output = out Bool
  }

  val counter = Reg(UInt(size bits)) init (0)

  counter := counter + 1
  io.output := counter < io.width
}

object Pwm {
  def main(args: Array[String]): Unit = {
    SpinalVerilog(new Pwm(size = 10))
  }
}

最後の object Pwm {...} という部分は、main() 関数を含むオブジェクト定義です。

私は Scala は素人ですが、object の定義と class の定義は異なり、object 構文では、そこに内容を記述しただけでシングルトンオブジェクトが「必要なタイミング」で生成されるようです。このようなオブジェクト生成を Scala では「lazy」と呼ぶようです。

sbt に「オブジェクト Pwm の main() 関数を実行してね」と指示すると、この main() 関数が実行されることになります。Java に似ていますね。

少しだけ main() 関数の中身について

以下、簡単に説明します。

main() 関数の記述詳細については、SpinalHDL のドキュメントサイトを参考にしてください。(ある程度 Scala を勉強してからのほうが楽だと思いますが、同サイト中にも Scala の簡単な文法説明や使い方が記述されています。)

new Pwm(size = 10) というのは、上のほうにある class Pwm() のインスタンス生成です。ちなみに次のようにインスタンスだけを生成しようとすると JVM が例外を発生してエラーになります。

val a = new Pwm(size = 10)

どうも、このインスタンスは SpinalVerilog() というオブジェクトの中でしか評価できないようです。ちょっと難しいですね。Scala に詳しくなろうというつもりがなければ、ま、これはこういうものだ、というくらいに理解したほうが良さそうです。話を少し戻して、size = 10 というのはクラス Pwm クラスを生成するときのパラメタです。先週説明した通りです。そして最後に SpinalVerilog というオブジェクトを生成すると、自動的に Verilog への変換出力が実行されます。もし VHDL のほうがお好みであれば、SpinalVerilog を SpinalVhdl に変更してみてください。

最後に、main() 関数定義の構文ですが、「: Unit =」は省略できるようです。私は Scala の初心者なので、よく分かっていません。ごめんなさい。

プログラムを実行してみよう

上記プログラムの実行方法には大きく 2つがあります。一つ目は毎回 sbt コマンドを起動するというものです。やってみましょう。最初のシェルコマンド sbt "runMain Pwm" を実行します。(最初は少し時間がかかります。)

$ sbt "runMain Pwm"
[info] Loading settings for project spinaltemplatesbt-build from plugins.sbt ...
[info] Loading project definition from /somedir/SpinalTemplateSbt/project
[info] Updating ProjectRef(uri("file:/somedir/SpinalTemplateSbt/project/"), "spinaltemplatesbt-build")...
[info] Done updating.
[info] Loading settings for project spinaltemplatesbt from build.sbt ...
[info] Set current project to SpinalTemplateSbt (in build file:/somedir/SpinalTemplateSbt/)
[info] Updating ...
[info] Done updating.
[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
[info] Compiling 3 Scala sources to /somedir/SpinalTemplateSbt/target/scala-2.11/classes ...
[info] Done compiling.
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Packaging /somedir/SpinalTemplateSbt/target/scala-2.11/spinaltemplatesbt_2.11-1.0.jar ...
[info] Done packaging.
[info] Running (fork) Pwm 
[info] [Runtime] SpinalHDL v1.3.5    git head : f0505d24810c8661a24530409359554b7cfa271a
[info] [Runtime] JVM max memory : 5461.5MiB
[info] [Runtime] Current date : 2020.01.01 19:23:08
[info] [Progress] at 0.000 : Elaborate components
[info] [Progress] at 0.083 : Checks and transforms
[info] [Progress] at 0.137 : Generate Verilog
[info] [Progress] at 0.140 :   emit Pwm
[info] [Info] Number of registers : 10
[info] [Done] at 0.174
[success] Total time: 6 s, completed 2020/01/01 19:23:08

プロジェクトのトップディレクトリに Pwm.v というファイルが生成されていたら成功です。

やってみると分かるのですが、ちょっとこれは時間がかかりますよね。Python などのインタープリタ形式の開発に慣れていると、この待ち時間が気になります。もう一つの方法は、常に sbt コマンドを実行しておき、その中(sbt シェル?)の中でコマンドを実行するというものです。そのためには、もう一つシェル用のターミナルを開いておくと良いでしょう。

そちらの中で、sbt を起動します。以下、太字の部分が手で実行するコマンドです。

$ sbt
[info] Loading settings for project spinaltemplatesbt-build from plugins.sbt ...
[info] Loading project definition from /Users/yokoyama/Dropbox/monthly/202001/fpga/spinalhdl/SpinalTemplateSbt/project
[info] Loading settings for project spinaltemplatesbt from build.sbt ...
[info] Set current project to SpinalTemplateSbt (in build file:/Users/yokoyama/Dropbox/monthly/202001/fpga/spinalhdl/SpinalTemplateSbt/)
[info] sbt server started at local:///Users/yokoyama/.sbt/1.0/server/af6bc6bd9cba82a12f26/sock
sbt:SpinalTemplateSbt>

「>」で終わるプロンプトが表示されたら、runMain Pwm を実行します。

sbt:SpinalTemplateSbt> runMain Pwm
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Running (fork) Pwm 
[info] [Runtime] SpinalHDL v1.3.5    git head : f0505d24810c8661a24530409359554b7cfa271a
[info] [Runtime] JVM max memory : 5461.5MiB
[info] [Runtime] Current date : 2020.01.01 19:28:20
[info] [Progress] at 0.000 : Elaborate components
[info] [Progress] at 0.079 : Checks and transforms
[info] [Progress] at 0.131 : Generate Verilog
[info] [Progress] at 0.134 :   emit Pwm
[info] [Info] Number of registers : 10
[info] [Done] at 0.166
[success] Total time: 2 s, completed 2020/01/01 19:28:21
sbt:SpinalTemplateSbt> 

先ほどより少し速いでしょうか。sbt を抜けるには exit コマンドを実行します。

なおもう一つ技(?)があります。コマンド実行時に、行頭にチルダ(~)を付けるのです。そうすると、sbt は常にファイル変更を監視し、ファイルがセーブされると自動的に runMain してくれます。Scala の文法に慣れず初歩的な文法エラーばかり繰り返しているときは逆にイライラするかも知れませんが、慣れると便利かも知れません。やってみましょう。

sbt:SpinalTemplateSbt> ~runMain Pwm
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Running (fork) Pwm 
[info] [Runtime] SpinalHDL v1.3.5    git head : f0505d24810c8661a24530409359554b7cfa271a
[info] [Runtime] JVM max memory : 5461.5MiB
[info] [Runtime] Current date : 2020.01.01 19:30:57
[info] [Progress] at 0.000 : Elaborate components
[info] [Progress] at 0.078 : Checks and transforms
[info] [Progress] at 0.127 : Generate Verilog
[info] [Progress] at 0.130 :   emit Pwm
[info] [Info] Number of registers : 10
[info] [Done] at 0.163
[success] Total time: 1 s, completed 2020/01/01 19:30:57
1. Waiting for source changes in project spinaltemplatesbt... (press enter to interrupt)
(ここでファイル Pwm.scala をセーブした)
[warn] Multiple main classes detected.  Run 'show discoveredMainClasses' to see the list
[info] Running (fork) Pwm 
[info] [Runtime] SpinalHDL v1.3.5    git head : f0505d24810c8661a24530409359554b7cfa271a
[info] [Runtime] JVM max memory : 5461.5MiB
[info] [Runtime] Current date : 2020.01.01 19:31:05
[info] [Progress] at 0.000 : Elaborate components
[info] [Progress] at 0.080 : Checks and transforms
[info] [Progress] at 0.131 : Generate Verilog
[info] [Progress] at 0.134 :   emit Pwm
[info] [Info] Number of registers : 10
[info] [Done] at 0.168
[success] Total time: 1 s, completed 2020/01/01 19:31:05
2. Waiting for source changes in project spinaltemplatesbt... (press enter to interrupt)

なお、待機中にリターンキーを押すと終了します。

生成されたファイルを見ておこう

先週も確認しましたが、生成された Verilog ファイルをちょっと覗いてみましょう。

module Pwm (
      input  [9:0] io_width,
      output  io_output,
      input   clk,
      input   reset);
  reg [9:0] counter;
  assign io_output = (counter < io_width);
  always @ (posedge clk or posedge reset) begin
    if (reset) begin
      counter <= (10'b0000000000);
    end else begin
      counter <= (counter + (10'b0000000001));
    end
  end

endmodule

Verilog を御存知の方はお気づきかと思いますが、レジスタ counter の初期化を明示的に、reset 信号(それも非同期 posedge)で実行してますね。。。FPGA を使うときは、reg [9:0] counter の定義時に初期値を(initial 等で)明示的に記述したいんだがなあ、と思われる方もあるかと思います。あるいは、非同期リセットではなくて同期リセットにしたい、という場合もあるかも知れません。これらは、今後説明していくクロックドメイン(ClockDomain)というものを使うと変更できます。それまでお待ちください。

シミュレーションしてみる

それでは続いて、上記コードをシミュレーションしてみましょう。通常の HDL 設計ではテストベンチというコードを別途作成することが多いと思いますが、SpinalHDL は「シミュレーションのやり方」も理解しているので、これを簡単に実現することができます。

なお、ここでは Verilator という Verilog シミュレータが必要です。インストールしてから先に進んでください。

まず最初に、先ほどの PwmLed.scala と同ディレクトリに PwmLedSim.scala というファイルを作成しましょう。中身は次の通りです。

// package com.flogics.spinal.pwmled

import spinal.sim._
import spinal.core.sim._

object PwmSim {
  def main(args: Array[String]): Unit = {
    SimConfig.withWave.compile(new Pwm(size = 4)).doSim{ dut =>
      dut.clockDomain.forkStimulus(period = 10)
      dut.io.width #= 5
      for (i <- 0 until 100) {
        dut.clockDomain.waitSampling()
      }
    }
  }
}

詳しくは、上述の MyTopLevelSim.scala あるいはこちらの説明を御参考ください。ちょっと難しいですね。以下に、私の解釈を示します。

SimConfig.withWave... という行では、テスト用のモジュール(クラス)をインスタンス化しています。なお、size が大きすぎるとグラフで閲覧するのが大変ですし、シミュレーションの時間も増加しますので、今回は 4(4ビットカウンタの PWM)としています。dut というのは design under test の略ですね。withWave というのは、シミュレーション結果の波形を波形としてセーブする、という意味のようです。結果のファイルは ./simWorkspace/Pwm/test.vcd という場所に保管されます。

HDL シミュレーションをする際は通常、クロックをパタパタさせながら dut に送り込む訳ですが、SpinalHDL ではこれを一行で実行するための関数が定義されています。それが dut.clockDomain.forkStimulus() という部分です。これを記述すると、period = 10 [ns] で自動的にクロックをパタパタさせてくれます。

次の dut.io.width #= 5 は、回路への信号入力です。#= というのは、Pwm モジュールの入力 width に 5(decimal)を入力しなさいという意味です。詳しい説明はこちらにあります。

次は、ループ処理です。クロックをパタパタさせながら 100回(100クロック)動作させましょうという意味です。この構文は Scala そのものだと思います。中では、クロックのパタパタが完了するのを毎クロック待ち合わせているようです。(実はシミュレータは別スレッドで動作しているようです。) このループの中に Scala で記述すれば、必要なタイミングで回路に値を入力したり、回路中の信号を読み出したりすることができます。つまり、テストベンチを記述することができる訳ですね。先ほどの MyTopLevelSim.scala を読んでみると参考になりそうです。

それでは sbt で動かしてみましょう。先ほどは sbt で runMain Pwm としましたが、今度は runMain PwmSim です。実行してしばらくすると、

[info] [Done] Simulation done in 20.744 ms

のような出力があり、ファイル ./simWorkspace/Pwm/test.vcd が生成されます。これを GTKwave という閲覧ツールで開いてみましょう。使い方については、あちこちに説明があると思いますので省略します。Xilinx 社等の似たようなツールを御経験の方は、あちこちをクリックしたりダブルクリックしたりすれば、すぐに使い方が分かるでしょう。

うまく閲覧できましたでしょうか。カウンタが 4ビット(0〜15)で動作し、width とカウンタを比較した結果、カウンタ値が width 未満のところだけ io_output が True になっていれば OK です。興味ある方は、先ほどのテストベンチ中で、ループ中に width を適宜変更してみると面白いでしょう。例えば 300ns(30クロック)経過した段階で width を 10 にするとどうなるでしょうか。これは皆さんへの宿題としておきます。

今日はここまで。次回は PwmLed クラスを設計してみましょう。おつかれさまでした。

御質問などありましたら、お気軽に!

御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。なお、Facebook ページ(https://www.facebook.com/flogics/)でも御連絡頂けます。