/home/tnishinaga/TechMEMO

日々行ったこと、面白かったことを書き留めます。

Chisel + PYNQでLチカ(お正月FPGAあそび)

お正月なので普段やらないことをやろうと思い、買ってから2年ほど放置してしまっていたPYNQ-Z1を使ってFPGAを触ってみました。

RISC-Vで遊びたい」という気持ちと「FPGAをさわってみたい」という気持ちがあったので、「PYNQでRISC-Vを動かしてみる」ことを最終目標にしました。 しかし、これではお正月休みだけでは終わらないので、このお休みの間は「PYNQでChiselを使ってLチカする」のをゴールとしてやってみました。

今回はそこで何やったかのメモ的な記事です。

なお、私はFPGAScalaもChiselもほぼ初挑戦です。

Vivado setup

PYNQはDigilent社の作ったFPGAボードです。

www.pynq.io

PYNQにはXilinx社のZynqというFPGAが乗っているので、Xilinx社のVivadoという開発環境で開発を行います。 以下のサイトからアカウントを作ってインストールします。WebPackライセンスを選択すればいろいろ制限はありますが無料で使えます。

japan.xilinx.com

インストールが終わったら以下からPYNQ向けのボードファイルをもらってきてインストールします。

https://github.com/cathalmccabe/pynq-z1_board_files/raw/master/pynq-z1.zip

$ wget https://github.com/cathalmccabe/pynq-z1_board_files/raw/master/pynq-z1.zip
$ unzip pynq-z1.zip
$ sudo cp -r pynq-z1 /tools/Xilinx/Vivado/2018.3/data/boards/board_files

PS + PL でLチカ

環境設定が終わったので、組み込み版HelloWorldであるLチカをやってみます。

PYNQボード向けの入門ドキュメントは少ないので、だいたい同じ構成のZyboというボード向けの初心者用ドキュメントを参考に、一部読み替えつつLチカをしてみます。

Getting Started with Zynq [Reference.Digilentinc]

PYNQボードに乗っているZynqというFPGAチップには、ARMのCPUコア(PS: Processing System)とFPGA(PL: Programmable Logic)が1つのチップに乗っています。 今回参考にするドキュメントの例では、PL部に用意したGPIOをPS部のプログラムから制御して、スライドスイッチの状態に応じてLEDを点灯させるというものになります。

Zybo向けに書かれたこのドキュメントをPYNQで用いるための変更点としては、PYNQにはスライドスイッチが2つしか無いので、手順4.3で設定するスイッチをswts_2bitsに設定する必要があります。

PL部のみでLチカ

次にPYNQをPS部を用いず普通のFPGAボードとして利用する方法が知りたいので、以下のサイトを参考にPL部のみでLチカをしてみたいと思います。

qiita.com

こちらのドキュメントもZYNQ向けなので、「ピンアサインをする」のところでクロックなどが接続されているピンがPYNQと異なります。そのため、以下のマニュアルを参考に変更を加えます。

https://reference.digilentinc.com/_media/reference/programmable-logic/pynq-z1/pynq-rm.pdf

「11 Clock Sources」と「12 Basic I/O」を読むと、クロックソースとLEDのピン配置が以下の表のようになっていることがわかるので、設定します。

Parts FPGA Pin
125MHz Clock H16
LED LD0 R14
LED LD1 P14
LED LD2 N16
LED LD3 M14

回路がうまく動けば、1秒ごとにLEDが4つとも点滅します。

Chiselを使ったLチカ

RISC-VのコアはChiselという、ScalaDSLとして実装された言語を用いて書かれています。 このChiselの書き方を学ぶため、まずはこのChiselを使って先程のPL部だけで行うLチカの回路を作ってみたいと思います。

まずChiselから実際のボード上で動く回路を作るまでの流れが知りたかったので調べてみます。 Chiselのコードをビルドすると、中間表現を経由してVerilogのコードが得られます。実際のボード上で動かすためにはこの生成したVerilogをvivadoに取り込んでピンアサインなどを設定してビルドする必要が有るようです。

詳細についてはmskysphinzさんのブログ記事が参考になりました(ありがとうございます)。

msyksphinz.hatenablog.com

次にプロジェクトの開始の際には、chisel-templateというリポジトリをベースに行うのが良いようです。

github.com

chisel-remplateにはGCDを求める回路が最初から書かれています。 最初は簡単のためメソッド名等はそのままに中身だけ書き換えてLチカのコードを書いていきます。

src/main/scala/gcd/GCD.scala

Verilogで書いていたLチカのコードをChiselで書き直します。 Clockは暗黙の引数として有るようなので、書かなくて良いようです。 onoff変数の値をLED4つに反映させる方法がわからなかったので、LED0にだけonoffの値を入れるようにしています。

// src/main/scala/gcd/GCD.scala

package gcd

import chisel3._

/**
  * Compute GCD using subtraction method.
  * Subtracts the smaller from the larger until register y is zero.
  * value in register x is then the GCD
  */
class GCD extends Module {
  val io = IO(new Bundle {
    val outputLED     = Output(UInt(4.W))
  })
  //  parameter CNT_1SEC = 27'd124999999;  // 125MHz clk for 1sec
  val CNT_1SEC = RegInit(124999999.U(27.W))
  //  reg [26:0] cnt = 27'd0;
  val cnt  = RegInit(0.U(27.W))
  // reg onoff = 1'd0;
  val onoff  = RegInit(false.B)

  // if (cnt == CNT_1SEC) begin
  when(cnt === CNT_1SEC) {
    // cnt <= 27'd0;
    // onoff <= ~onoff;
    cnt := 0.U
    onoff := ~onoff
  } .otherwise {
    // cnt <= cnt + 27'd1;
    cnt := cnt + 1.U
  }

  // assign LED = {onoff, onoff, onoff, onoff};
  io.outputLED := onoff
}

src/test/scala/gcd/GCDUnitTest.scala

回路のテストコードを書きます。

サンプルではpokeを使って回路に入力を設定しているものがよく見られますが、この回路は入力がクロックしか無いので、pokeで入力設定を行わなくても良いようです。

stepを進めるとクロックが入力されてカウンタが上がっていきます。 今回125MHzでLEDの出力を反転させるので、stepで125,000,000進めた後にexpectを使って出力が変わっているかをチェックします。

class GCDUnitTester(c: GCD) extends PeekPokeTester(c) {
  private val gcd = c

  // return 0 when clk counter == 0
  expect(gcd.io.outputLED, 0)

  // return 0 when clk counter == 1
  step(1)
  expect(gcd.io.outputLED, 0)

  // return 1 when clk counter == 124999999
  step(125000000 - 1)
  expect(gcd.io.outputLED, 1)

  // return 1 when clk counter == 0
  step(1)
  expect(gcd.io.outputLED, 1)

  // return 0 when clk counter == 124999999
  step(125000000 - 1)
  expect(gcd.io.outputLED, 0)
}

src/test/scala/gcd/GCDMain.scala

そのままもテストはできますがVerilogファイルの出力ができなかったので、chisel-wikiを参考にMainメソッドに1行追加してVerilogのコードをビルドできるようにします。 以下のドキュメントを参考に、出力のための処理を追記します。

Frequently Asked Questions · freechipsproject/chisel3 Wiki · GitHub

object GCDMain extends App {
  iotesters.Driver.execute(args, () => new GCD) {
    c => new GCDUnitTester(c)
  }

  // 追記
  chisel3.Driver.execute(args, () => new GCD)
}

テスト

tutorialのREADME.mdに書いてあるとおりに以下のコマンドを実行するとテストが走ります。

$ sbt 'testOnly gcd.GCDTester -- -z Basic'

うまくいけばこんな感じのログが出てきます。

$ sbt 'testOnly gcd.GCDTester -- -z Basic'                                                                                                                                                                                                                                                                                                                                                                                  
[info] Loading settings from plugins.sbt ...
[info] Loading project definition from /home/tnishinaga/projects/chisel/chisel_ledblink/project
[info] Loading settings from build.sbt ...
[info] Set current project to chisel-ledblink (in build file:/home/tnishinaga/projects/chisel/chisel_ledblink/)
[info] Compiling 1 Scala source to /home/tnishinaga/projects/chisel/chisel_ledblink/target/scala-2.11/classes ...
[warn] there was one feature warning; re-run with -feature for details
[warn] one warning found
[info] Done compiling.
[info] [0.005] Elaborating design...
[info] [2.191] Done elaborating.
Total FIRRTL Compile Time: 512.2 ms
Total FIRRTL Compile Time: 201.5 ms
file loaded in 0.388108123 seconds, 13 symbols, 9 statements
[info] [0.002] SEED 1546794998929
test GCD Success: 5 tests passed in 250000005 cycles in 118.643097 seconds 2107160.15 Hz
[info] [118.613] RAN 250000000 CYCLES PASSED
[info] GCDTester:
[info] GCD
[info] Basic test using Driver.execute
[info] - should be used as an alternative way to run specification
[info] using --backend-name verilator
[info] running with --is-verbose
[info] running with --generate-vcd-output on
[info] running with --generate-vcd-output off
[info] ScalaTest
[info] Run completed in 2 minutes, 3 seconds.
[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
[success] Total time: 135 s, completed Jan 7, 2019 00:01:42 AM

Verilogコードの生成

Verilogのコード生成は以下のコマンドで行えました。 このコマンドがスマートな方法かはわかりませんが、動いたので良しとしています。どなたか良い方法をご存知の方は教えてください。

$ sbt 'test:runMain gcd.GCDMain --target-dir buildstuff --top-name GCDMain'

うまくビルドが終わればbuildstuffディレクトリ以下にGCDMain.vというファイルができているはずです。 機械が生成したコードなので、先程の人間の作ったコードに比べるとちと読みづらいです。

module GCD( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  output [3:0] io_outputLED // @[:@6.4]
);
  reg [26:0] cnt; // @[GCD.scala 19:21:@9.4]
  reg [31:0] _RAND_0;
  reg  onoff; // @[GCD.scala 21:23:@10.4]
  reg [31:0] _RAND_1;
  wire  _T_13; // @[GCD.scala 25:12:@11.4]
  wire  _T_15; // @[GCD.scala 27:14:@14.6]
  wire [27:0] _T_17; // @[GCD.scala 29:16:@18.6]
  wire [26:0] _T_18; // @[GCD.scala 29:16:@19.6]
  wire [26:0] _GEN_0; // @[GCD.scala 25:26:@12.4]
  wire  _GEN_1; // @[GCD.scala 25:26:@12.4]
  assign _T_13 = cnt == 27'h773593f; // @[GCD.scala 25:12:@11.4]
  assign _T_15 = ~ onoff; // @[GCD.scala 27:14:@14.6]
  assign _T_17 = cnt + 27'h1; // @[GCD.scala 29:16:@18.6]
  assign _T_18 = cnt + 27'h1; // @[GCD.scala 29:16:@19.6]
  assign _GEN_0 = _T_13 ? 27'h0 : _T_18; // @[GCD.scala 25:26:@12.4]
  assign _GEN_1 = _T_13 ? _T_15 : onoff; // @[GCD.scala 25:26:@12.4]
  assign io_outputLED = {{3'd0}, onoff}; // @[GCD.scala 33:16:@22.4]
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE
  integer initvar;
  initial begin
    `ifdef INIT_RANDOM
      `INIT_RANDOM
    `endif
    `ifndef VERILATOR
      #0.002 begin end
    `endif
  `ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{`RANDOM}};
  cnt = _RAND_0[26:0];
  `endif // RANDOMIZE_REG_INIT
  `ifdef RANDOMIZE_REG_INIT
  _RAND_1 = {1{`RANDOM}};
  onoff = _RAND_1[0:0];
  `endif // RANDOMIZE_REG_INIT
  end
`endif // RANDOMIZE
  always @(posedge clock) begin
    if (reset) begin
      cnt <= 27'h0;
    end else begin
      if (_T_13) begin
        cnt <= 27'h0;
      end else begin
        cnt <= _T_18;
      end
    end
    if (reset) begin
      onoff <= 1'h0;
    end else begin
      if (_T_13) begin
        onoff <= _T_15;
      end
    end
  end
endmodule

FPGAでの動作確認

PL部でのLチカを参考に、VerilogのコードをコピペしてVivadoでビルドを行います。 Chiselはreset用のpinも欲しがるので、私はとりあえずスライドスイッチのついているM20を設定しました。

PYNQにうまく書き込めれば、LEDのLD0だけがチカチカするはずです。

おもったこと

最後に、やってる途中に思ったことをつらつらと。

  • ChiselのまえにScala(基本文法とsbtつかったビルド方法)を勉強したほうが良さそう
  • Verilogを使った普通のFPGA開発も本一冊分くらいはやったほうが良さそう
  • RISC-Vの実機が触りたいなら10万出してhifive unleashed買うか、マイコンで良ければArty FPGAボードかHiFive1を買ったほうがよさそう