お正月なので普段やらないことをやろうと思い、買ってから2年ほど放置してしまっていたPYNQ-Z1を使ってFPGAを触ってみました。
「RISC-Vで遊びたい」という気持ちと「FPGAをさわってみたい」という気持ちがあったので、「PYNQでRISC-Vを動かしてみる」ことを最終目標にしました。 しかし、これではお正月休みだけでは終わらないので、このお休みの間は「PYNQでChiselを使ってLチカする」のをゴールとしてやってみました。
今回はそこで何やったかのメモ的な記事です。
なお、私はFPGAもScalaもChiselもほぼ初挑戦です。
Vivado setup
PYNQにはXilinx社のZynqというFPGAが乗っているので、Xilinx社のVivadoという開発環境で開発を行います。 以下のサイトからアカウントを作ってインストールします。WebPackライセンスを選択すればいろいろ制限はありますが無料で使えます。
インストールが終わったら以下から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チカをしてみたいと思います。
こちらのドキュメントも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という、ScalaのDSLとして実装された言語を用いて書かれています。 このChiselの書き方を学ぶため、まずはこのChiselを使って先程のPL部だけで行うLチカの回路を作ってみたいと思います。
まずChiselから実際のボード上で動く回路を作るまでの流れが知りたかったので調べてみます。 Chiselのコードをビルドすると、中間表現を経由してVerilogのコードが得られます。実際のボード上で動かすためにはこの生成したVerilogをvivadoに取り込んでピンアサインなどを設定してビルドする必要が有るようです。
詳細についてはmskysphinzさんのブログ記事が参考になりました(ありがとうございます)。
次にプロジェクトの開始の際には、chisel-templateというリポジトリをベースに行うのが良いようです。
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だけがチカチカするはずです。
おもったこと
最後に、やってる途中に思ったことをつらつらと。