12 |
RISC-Vの調査 (4) |
シミュレーションモデルの波形を観測してみます。テストプログラムは、\$80000000から始まっていますが、シミュレーションは\$1000から始まっています。これはBootROMのようです。BootROMのブロックインが19サイクルかかっています。
図275.1はGtkwaveによるシミュレーション波形で、pcをオレンジ色で塗っています。
12 |
RISC-Vの調査 (4) |
シミュレーションモデルの波形を観測してみます。テストプログラムは、\$80000000から始まっていますが、シミュレーションは\$1000から始まっています。これはBootROMのようです。BootROMのブロックインが19サイクルかかっています。
図275.1はGtkwaveによるシミュレーション波形で、pcをオレンジ色で塗っています。
11 |
RISC-Vの調査 (3) |
前々稿の記事において、シミュレーションモデルが生成できましたが、これを実行させてみます。
テストプログラムは、add命令単体テストで、
80000000 <_start>: 80000000: 04c0006f j 8000004c <reset_vector> 8000004c <reset_vector>: 8000004c: f1402573 csrr a0,mhartid 80000050: 00051063 bnez a0,80000050 <reset_vector+0x4> 80000054: 00000297 auipc t0,0x0 80000058: 01028293 addi t0,t0,16 # 80000064 <reset_vector+0x18> 8000005c: 30529073 csrw mtvec,t0 80000060: 18005073 csrwi satp,0 : 80000100 <test_2>: 80000100: 00000093 li ra,0 80000104: 00000113 li sp,0 80000108: 00208f33 add t5,ra,sp 8000010c: 00000e93 li t4,0 80000110: 00200193 li gp,2 80000114: 4ddf1663 bne t5,t4,800005e0 <fail> 80000118 <test_3>: 80000118: 00100093 li ra,1 8000011c: 00100113 li sp,1 80000120: 00208f33 add t5,ra,sp 80000124: 00200e93 li t4,2 80000128: 00300193 li gp,3 8000012c: 4bdf1a63 bne t5,t4,800005e0 <fail> :
のように構成されています。Bsimによるシミュレーションは、+v1で命令トレースが、+v2でパイプラインステージの内容を表示させるようになっています。+v2で実行させた結果をgrepにより計数してみると、表274.1のようになりました。パイプラインの各ステージの出力ステータスの意味は、以下のとおりです。
状態 | StageF | StageD | Stage1 | Stage2 | Stage3 |
---|---|---|---|---|---|
BUSY | 1,118 | 0 | 23 | 26 | 0 |
EMPTY | 22 | 1,094 | 959 | 1,373 | 1,409 |
PIPE処理 | 743 | 789 | 901 | 484 | 474 |
合計 | 1,883 |
これだけ見るとかなり効率が悪そうですが、対象がテストプログラムでループが無いため、基本的にキャッシュミスが頻発します。一般のアプリケーションのようにループがあれば、ずっと効率が向上するはずです。
10 |
RISC-Vの調査 (2) |
CPUの階層構造を図273.1に示します。5段のパイプラインは図273.1に示すように、"F"(命令Fetchステージ), "D"(命令デコードステージ), "1"(命令実行ステージ), "2"(メモリアクセス、長レインテンシステージ), "3"(ライトバックステージ)とステージ名が付けられています。なぜステージ"1”,"2",...,"5"でないかと言えば、3段パイプラインのPiccoloとステージを共用しているため書かれていました。Piccoloではパイプラインステージは”1", "2", "3"と名付けられており、Fluteでは、Piccoloの"1"を"F", ”D”, "1"に分解したようです。
各モジュールがmk〇〇と名付けられているのはBSVのお作法です。モジュールとそのインスタンスは一対nの関係にあり、モジュール名はいわばテンプレート名を意味するため、モジュールからインスタンスがmakeされるということを表しています。例えば上図のmkMMU_Cacheはどちらも同じモジュールですが、それぞれ命令用とデータ用に2個インスタンシエートしています。
キャッシュは上記のように、パラメタライズ可能な命令キャッシュ、データキャッシュの2つがあります。その他に、分岐予測器があります。
9 |
RISC-Vの調査 |
BluespecのBSVが読めるようになったところで、引き続いてFluteプロセッサの調査を行います。FluteはBluespecの開発したRISC-Vアーキテクチャの5段パイプラインRISC CPUです。それだけでなく、仮想記憶をサポートしているため、Linuxが動作します。(ページ)テーブルウォークはMMU内のハードウェアが実行します。ソースではステートベース設計のFSMで実装されていました。
$ git clone https://github.com/bluespec/Flute.git
としてGithubからダウンロードします。
アーキテクチャには各種ありますが、比較的軽いもの、例えば32bit、Floating無しのマイクロアーキテクチャを選択します。
$ cd builds/RV32ACIMU_Flute_bluesim
として、ターゲットディレクトリに移行します。
$ make all
と実行すると、
: Simulation shared library created: exe_HW_sim.so Simulation executable created: ./exe_HW_sim INFO: linked bsc-compiled objects into Bluesim executable $
このように、bsvで書かれたソースファイルがbscによりコンパイルされ、シミュレーションモデルであるexe_HW_simが生成されます。
$ make test
により、出来上がったシミュレーションモデルが、RISC-Vのテストスイートによりテストされます。
$ make test make -C ../../Tests/elf_to_hex make[1]: Entering directory '/home/sakurai/src/bsv/riscv/Flute/Tests/elf_to_hex' make[1]: 'elf_to_hex' is up to date. make[1]: Leaving directory '/home/sakurai/src/bsv/riscv/Flute/Tests/elf_to_hex' ../../Tests/elf_to_hex/elf_to_hex ../../Tests/isa/rv32ui-p-add Mem.hex c_mem_load_elf: ../../Tests/isa/rv32ui-p-add is a 32-bit ELF file Section .text.init : addr 80000000 to addr 80000604; size 0x 604 (= 1540) bytes Section .tohost : addr 80001000 to addr 80001048; size 0x 48 (= 72) bytes Section .riscv.attributes: Ignored Section .symtab : Searching for addresses of '_start', 'exit' and 'tohost' symbols Writing symbols to: symbol_table.txt No 'exit' label found Section .strtab : Ignored Section .shstrtab : Ignored Min addr: 80000000 (hex) Max addr: 80001047 (hex) : ================================================================ Bluespec RISC-V standalone system simulation v1.2 Copyright (c) 2017-2019 Bluespec, Inc. All Rights Reserved. ================================================================ INFO: watch_tohost = 1, tohost_addr = 0x80001000 1: top.soc_top.mem0_controller_axi4_deburster::AXI4_Deburster.rl_reset 2:top.soc_top.rl_reset_start_initial ... 3: Core.rl_cpu_hart0_reset_from_soc_start ================================================================ CPU: Bluespec RISC-V Flute v3.0 (RV32) Copyright (c) 2016-2020 Bluespec, Inc. All Rights Reserved. ================================================================ 6: D_MMU_Cache: cache size 8 KB, associativity 2, line size 32 bytes (= 8 XLEN words) 6: I_MMU_Cache: cache size 8 KB, associativity 2, line size 32 bytes (= 8 XLEN words) 512: top.soc_top.core.cpu.rl_reset_complete: restart at PC = 0x1000 514: Near_Mem_IO_AXI4.set_addr_map: addr_base 0x2000000 addr_lim 0x200c000 514: Core.rl_cpu_hart0_reset_complete 515: Mem_Controller.set_addr_map: addr_base 0x80000000 addr_lim 0x90000000 515:top.soc_top.rl_reset_complete_initial instret:0 PC:0x1000 instr:0x297 priv:3 instret:1 PC:0x1004 instr:0x2028593 priv:3 instret:2 PC:0x1008 instr:0xf1402573 priv:3 : instret:471 PC:0x80000044 instr:0xfc3f2023 priv:3 instret:472 PC:0x80000048 instr:0xff9ff06f priv:3 instret:473 PC:0x80000040 instr:0x1f17 priv:3 2396: Mem_Controller.rl_process_wr_req: addr 0x80001000 (<tohost>) data 0x1 PASS 2397: top:.rl_terminate: soc_top status is 0x1 (= 0d1) Simulation speed: 2396 cycles, 60915008 nsecs = 39333 cycles/sec
テストスイートのelfを、elf_to_hexにより、asciiのメモリイメージファイルに変換しています。
これはadd命令単体のテストですが、全てのテストを行うには、以下のように実行します。
$ make isa_tests
と実行すると、
: Worker 1: Test: rv32um-p-mul PASS [So far: total 67, executed 34, PASS 34, FAIL 0] Worker 0 executed 33 tests, of which 33 passed Worker 1 executed 34 tests, of which 34 passed Total tests: 67 tests Executed: 67 tests PASS: 67 tests FAIL: 0 tests Finished running regressions; saved logs in Logs/
のように出力され、67個全てのテストが実行され、全てパスしたことが表示されます。
5 |
ゲームFSMとサウンドFSMの連携 |
元々Verilog版では、コマンドバッファに書き込むだけで特に何もしなくても動作していました。今回BSVで再設計する際に、サウンドを4chとし、取りこぼしを避けるために考えたのがサウンドキュー(FIFO)でした。
これはVivadoのFIFOジェネレータで作成したため、最小段数でもかなり深く1024段程度となっています。 実験したところ、確かに取りこぼしは無いのですが、一方、サウンドがゲームとズレて行き、まるでサウンドレコーダのような動作になってしまいました。そのため、FIFOを1段に修正しました。FIFOジェネレータでは1段のFIFOは作成できないのでVerilogで記述しました。1段のためFIFOと呼ぶのはおかしいのでコマンドバッファと呼ぶことにします。
コマンドバッファには、ゲームFSMからコマンドが来たことを示すフラグemptyを設け、書き込むと!emptyとなるようにします。サウンドFSMからは!emptyの時に新たにコマンドが来たと判断し、コマンドを読んだ後にemptyに変更します。
Ultra96ではこれで動作していたのですが、Artyボードに移植後に自機増加音が無視されることに気づきました。サウンドFSMが取りこぼしているのだと推測し、再考すると、ゲームFSMが不必要に速いことに気づきました。DSOを接続して調べたところ、96.4%がウェイトだと判明したので、これを1MHzに落としたところ、動作するようでした。
ところが実験すると、依然として自機増加音(コマンドNo.9)が無視されるようです。そこで、ILAを接続して、
を観測しました。最後の内部フラグfNO9は自機増加音がプリエンプトされないように割込みを禁止するためのフラグで、コマンドNo.9を受け付けた際にTrueになる信号です。
従って、コマンドの書き込みの際にemptyである場合のみ書き込み、!emptyの場合は捨てる処理を行います(図271.4のマゼンタ矢印の処理を追加)。
通常ではVivadoからPROGRAM AND DEBUG⇒Open Hardware Manager⇒Open Target⇒Auto Connectとし、Program DeviceによりJTAG経由でFPGAにビットストリームを焼きこみます。しかしながらこれだと電源断によりFPGAのSRAM内容が消えてしまいます。また、FPGAプログラミング用のPCが常に必要です。オンボードFlashにデータを焼きこめばPCを持ち運ぶ必要がなく、電源onでアプリケーションが立ち上がるため、Flashのプログラミングを行います。
最初にFlashへ書き込むデータファイルであるbinファイルを用意します。これは、Tools⇒Setting(歯車マーク)⇒Project Settings⇒Bitstream画面で行います。この画面を開くと複数のチェックボックスが表示されます。その中の、-bin_file*のチェックボックスにチェックします。
これを行ってから、通常どおりPROGRAM AND DEBUG⇒Generate Bitstreamを実施するとWrite Bitstreamが完了しますが、同時にbinファイルが生成されています。場所はbitファイルと同じところで'プロジェクト/プロジェクト.runs/impl_1/'です。
binファイルができたら、Add Configuration Memoryにより、Add Configuration Memory Device画面が開きます。Flashデバイスの選択が可能なので、この中で"s25fl128sxxxxx0"を選択します。Search窓にs25fl128を入力すれば、候補が3つ現れますがその真ん中です。
リファレンスマニュアルにはJP1でプログラミングモードが決まるとあります。JP1の位置がどちらでもJTAGからは書き込めるとのことです。初期状態はJP1はショートで、SPI-FLASHのモードとなっており、そのまま電源のOFF⇒ONでSpace Invadersが立ち上がりました。
DigilentからArty A7-35ボード(魚拓)を購入しました。このボードはUltra96と比べて本体が約半額と安いだけでなく、(弊社開発の)PMOD変換ボードも不要なので、最も安くSpace Invadersを動かすことができます。
Space Invadersを動作させるには、Artyボードの他に必要なものは以下のとおりです。
FPGAの世代や遅延、容量は違うものの、基本的には同様に動作するはずです。ところが、一部動作がおかしかったので修正しました。まず、除算器にバグがあるようなので引き算方式に修正しました。スコアを表示する箇所において、各桁表示のため1000、100、10で割る場合がありますが、1000で割った商を誤ることがあるようです。除算をやめ、引けなくなるまで1000、100、10を引く方式に変更したところ、回路規模も小さくなり正常に動作するようになりました。
ゲームFSMクロックを10MHzで設計し、96.4%がウェイトだと判明したので、FSMクロックを1MHzに落としました。自機増加音が無視されることがあるので、クロックを落としたのですが、原因は異なっていました(後述)。
この修正により、FSMの待ち時間が影響を受けます。1tick=60HzのタイミングをとるのにFSMクロック数を数えていましたが、FSMクロックの周波数が変わるため、外部から60Hzを入力するように修正します。60Hzクロックは、上記FSMクロックである1MHzクロックをバイナリカウンタで\$411B回カウントすることで生成します。さらにFSM内での60Hzクロックとの同期は以下のように行います。countはtick(=16.67msec)の何倍待たせるかを示す引数です。
repeat(pack(extend(count))) seq await(tick == 0); await(tick == 1); endseq
60Hzクロックの"L"を待ち、もし"L"であれば次に"H"を待つようにします。これにより60Hzの立ち上がりに同期して動作することになります。
このように変更した結果、FSMの処理時間は10倍の約5msecに増加し、60Hzの周期16.67msecの約30%になりました。図269.2の黄線が60Hzクロック、青線がそれによる実行(Hでウエイト中、Lで実行中)を示します。
過去ブログの、BSVによるスペースインベーダーの再設計の記事#234~#239, #254をまとめてQiitaに投稿しました。さらに考察を加えています。
BSV (Bluespec SystemVerilog)によるスペースインベーダーの再設計
過去ブログ記事でUltra96ボードを用いた、VerilogHDLによるSpace Invadersゲームの作成を投稿しましたが、その続きです。
11 |
BSVの設計トライアル (21) |
トライアルの結果、BSVによるゲームFSMが完成しました。過去記事のステートベースのサウンドステートマシンと異なり、ステート分解をしていないため、rule文を一切使用していません。全てbsc(Bluespec Compiler)の、StmtFSMライブラリにステート管理を任せました。
基本的にはCで記述するようにゲームが記述できることが分かりました。例えば、弾の移動及び衝突判定、衝突処理(爆発マーク)、爆発マーク消去等のアルゴリズムを考えると、自弾、敵弾共にアルゴリズムは共通で、疑似コードで書けば、
if (弾爆発タイマ >= 1) { // 弾爆発中 弾爆発タイマ++; if (弾爆発タイマ == MAX) { 弾削除; // 論理的な消去 弾爆発マーク消去; // 物理的な消去 弾爆発タイマ停止; } } else { if (弾が出ていない and 弾生成条件) { 弾生成処理; 弾発射音; // 自弾のみ } if (弾存在) { 衝突判定; if (対象物) { // 自弾の場合はインベーダ及びUFO、敵弾の場合は自機 弾削除; // 論理的な消去 対象物ステート <= 爆発; 対象物爆発タイマ <= 0; } else if (上下ハズレ || ベース || 弾) { // 弾:自弾の場合は敵弾、敵弾の場合は自弾 弾マーク消去; 弾爆発マーク; 弾爆発タイマ <= 1; } else { // 衝突していない場合 弾を進める; } } }
一方、対象物は、
if (対象物ステート == 爆発) { if (対象物爆発タイマ==0) { 対象物爆発タイマ <= 1; 対象物爆発音; 対象物爆発マーク; } else { 対象物爆発タイマ++; if (対象物爆発タイマ == MAX) { 対象物削除; // 論理的な消去 対象物爆発マーク消去; // 物理的な消去 } } }
のようになりますが、StmtFSMを使うと、このようなシーケンスをクロック毎のステートに分解しなくて記述できます。
某所で質問があったので、タイミングについて解説します。基本の1 tickは1/60秒で、その中で、インベーダ1匹、敵弾全弾、自機、自弾、UFO、スコア等の処理を行います。以下は実際のBSVのメインループのコードです。
while (game_flag) seq // メインループ for (noy <= 0; noy < `Inv_TateS; noy <= noy + 1) seq // インベーダの行処理 for (nox <= 0; nox < `Inv_YokoS; nox <= nox + 1) seq // インベーダの列処理 if (inv_s[nox][noy]) seq // インベーダが生きてれば ivader; // インベーダ処理 gun; // 自機処理 bullet; // 自弾処理 for (idx <= 0; idx < extend(max); idx <= idx + 1) seq invBullet(idx); // 敵弾全弾処理 endseq ufo; // UFO処理 scores; // スコア表示 endJudge; // 終了判定 counter <= counter + 1; // tickカウンタ++ wait_timer; // インナーループを1/60secにするウエイト endseq endseq endseq endseq gameOver; // ゲームオーバー表示
1tick=1/60secの間に、インベーダ1匹(2ピクセル移動)の処理に対して、自機(1ピクセル移動)、敵弾(1ピクセル移動)、自弾(4ピクセル移動)の処理が行われます。インベーダは初期に55匹存在するので、1/55倍のスピードで始まりますが、最終的に1倍のスピードになります。従って、インベーダを倒すたびにインベーダ全体は速くなり、一方その他の速度は変わらないわけです。
FPGAでの実装では1 tick内にインベーダ全体を移動することは可能ですし、そのような実装も見ますが、ゲーム性が変わってしまいます。具体的には、インベーダ全体の速度が次第に速くならなかったり、後ろのインベーダを撃つことができなくなります。
例えば、インベーダゲームのレインボーは、後ろのインベーダを撃つことにより出現します。インベーダは残りが一匹になると左へは2ピクセルずつ右には3ピクセルずつ移動します。下2段のインベーダは、左右2ピクセルまでの移動では跡が残らない図形になっていますが、3ピクセルだと跡が消えずに残ります。もちろん今回の実装でもレインボーを体験できます。
図254.1は、BSVで再設計したゲームFSMにより動作する、インベーダーゲームの動画です。過去記事に書いたように、サウンドが4ch同時発声と高品質になりました。
4 |
BSVの設計トライアル (20) |
次は、ファンクションの中にシーケンスを組み込み、ゲームFSMの設計トライアルを行います。 シーケンスを人手で分解することは、なるべくしたくありません。ファンクションでシーケンスが定義できれば、インベーダ動作、自機動作等のファンクションを作成し、順番にそれらを呼び出せば良いはずです。
import StmtFSM::*; interface TestFSM_ifc; method Action inp(UInt#(8) inx); endinterface (* synthesize *) module mkTestFSM(TestFSM_ifc); Reg#(UInt#(8)) i <- mkRegU; Reg#(UInt#(8)) x <- mkRegU; function Stmt test1; return (seq $display("%3d 1-1", $time); delay(5); $display("%3d 1-2", $time); endseq); endfunction function Stmt test2(UInt#(8) xx); return (seq $display("%3d 2-1", $time); for (i <= 0; i < xx; i <= i + 1) $display("%3d 2-loop-%1d", $time, i); $display("%3d 2-2", $time); endseq); endfunction Stmt main = seq $display("%3d fsm1.start", $time); test1; $display("%3d fsm2.start", $time); test2(x); endseq; mkAutoFSM(main); method Action inp(UInt#(8) inx); x <= inx; endmethod endmodule: mkTestFSM
このためのテストベンチを示します。あえてモジュール外部からループ回数を入れているのは、ループ回数がダイナミックに(実行時に)決定できるかを確認するためです。ファンクションのループを8回呼び出してみます。
import StmtFSM::*; import TestFSM::*; (* synthesize, always_ready, always_enabled *) module mkTb (Empty); TestFSM_ifc test <- mkTestFSM(); Reg#(UInt#(8)) count <- mkReg(8); Stmt main = seq test.inp(count); repeat(40) noAction; endseq; mkAutoFSM(main); endmodule
実行結果を示します。test1の次にtest2が呼び出され、ループが8回回ったことを示しています。
20 fsm1.start 30 1-1 90 1-2 100 fsm2.start 110 2-1 130 2-loop-0 150 2-loop-1 170 2-loop-2 190 2-loop-3 210 2-loop-4 230 2-loop-5 250 2-loop-6 270 2-loop-7 290 2-2