Ultra96ボードを用いた、VerilogHDLによるSpace Invadersゲームの作成
動機
FPGA (Xilinx Zynq UltraScale+ ™ MPSoC) を用いたADASプロジェクト2件に参加する機会があり、FPGAの勉強のため、タイトルのゲームを作成した経験を記します。ADASは乗用車の運転支援システムですが、このプロジェクトは、2件とも同じXilinx FPGAを使用していました。FPGAを採用した理由は、画像認識にはDeep Learning含め、かなり複雑な回路が必要ですが、専用ASICを開発すると、ゲート1つの修正でも数か月と億単位の金が飛ぶためだと思います。
チープラーニング
テーマはゲームとしました。ゲームには絵、音、動きがあるため、ゲームの開発は非常に教育的です。ところが、最近のゲームのような3Dフルカラーでは一人では作り切れません。そこで、8bitの(チープな)ゲームを選択しました。これはほのぼのしていて、規模的に全てに目が届きます。これをチープラーニングと呼びます。
得られた知識
ASIC設計者としてVerilog HDLはかなり経験があったので、今回、初めてCからの高位合成も考えましたが、初めてのFPGAで初めての高位合成にトライするとハマリ箇所が多くなると思い、HDLからのトライアルとしました。
VerilogRTLにより、スペースインベーダーという複雑なステートマシンを設計した結果、ロジック的にはハードもソフトもそう変わらないことを実感しました。作業効率等はだいぶ低いかもしれませんが。
結果としてXilinxツール(主にVivado)の知識を得ました。さらに、ゲームを動作させるためにPCB開発が必要となり、PCB設計ツール(Eagle)の知識を得ました。さらに、格安でPCBを作成する方法がわかりました。従来はユニバーサル基板にハンダ付けやワイヤラップで配線しましたが、なんと数百円でオリジナルPCBが作成できてしまう時代になりました。
価格破壊といえば、論理合成、配置配線ツールも、昔ASICを開発していた時はそれぞれ数千万円しており、論理シミュレータも数百万円でしたが、XilinxのWebpakを使用すれば、全く無償でシミュレーションまでできてしまうのは感激です。FPGAボードも3万円程度なので、個人でも手が届く範囲でしょう。
こんな良い時代になったので、あとはアイデアとやる気だけですね。
必要なもの
Ultra96ボード
対象となるFPGAは前述のようにXilinx Zynq UltraScale+ ™ MPSoCで、これを搭載した評価ボードは20万円ほどしていましたが、Ultra96という3万円くらいのボードがAvnetから昨年発売されました。今は改版(V2)の出荷待ち状態で、近日中に再発売の予定です。
Ultra96 USB-to-JTAG/UART
PCからビットストリームをダウンロードする際に使うUSBからJTAGへのインタフェースボードです。これ用のUSBケーブルも必要です。ボードは5,000円です。
Ultra96toPMODボード
今回開発したボードで、Ultra96からオーディオやビデオ(VGA)、ジョイスティック、スイッチとインタフェースするためのPMOD変換ボードです。作成枚数にもよりますが、Seeed Fusion PCBで10枚製造して送料込みで7.9USDでした。ただし、部品代は別で、基板だけの価格です。
ジョイスティック&スイッチ
インベーダでおなじみの、自機左右移動スイッチと、スタートボタン、弾発射ボタンです。国内では少々高いため、中国から輸入しました。が、送料は結構かかります。写真は100均ショップで購入したケースに取り付けた完成形で、費用は1,000円くらいです。
ケーブルの先のボードも今回作成したもので、ジョイスティックとスイッチをPMODとインタフェースするためのボードです。
PMOD VGAインタフェース
Digilent製PMOD仕様のVGAインタフェースボードで8.99 USDです。ただしVGAモニタが最近は見当たらなくなっているため、写真のようにVGAからHDMIへの変換ケーブルを通して、その先にHDMIケーブルとHDMIモニタを接続しています。
PMOD オーディオインタフェース
Digilent製PMOD仕様のオーディオインタフェースボードです。I2Sシリアルインタフェースを持ち、シリアルDACが搭載されているものですが、廃盤になったようです。オーディオケーブルで、アンプ搭載のマイクロスピーカを接続します。
仕様
Ultra96ボード
以下に本ボードの低速拡張コネクタ信号表を示します。
問題点としては、黄色で示すように汎用GPIOが16本しかなく、グラフィック系だけでも各色4bit×3原色=12bitあり、他にサウンド系4bit、スイッチ系で4bitとトータルでは本数が少々オーバーします。幸いグラフィクスは各色4bitも使用していないので、今回は各色1bit×3原色=3bitとして本数を減らすことにします。
Ultra96toPMODボード
Ultra96にはPMODインタフェースが無いため、PMODインタフェースを持つVGAインタフェースボードとI2Sインタフェースボードを接続するためのインタフェースボードを作成します。
Ultra96にPMOD仕様カードを接続するために、PMODコネクタだけでなく、レベル変換ICを搭載します。PMODは3.3V電源が標準であるのに、Ultra96の低速インタフェースは1.8V電源であるためです。さらにUltraZedボードにも接続可能なように設計します。もともとUltraZedはPMODインタフェースが搭載されているので、論理的な意味は無いものの、PMODカードが複数あり、抜き差しする手間を減らすために、共用にしてみました。
【追記begin】
UltraZedボードインタフェースを削除して簡略化したボードデータを以下の場所に置きました。動作に問題無いことを評価済みです。
【追記end】
-
回路図
完成した回路図を次に示します。EAGLEにより作図しました。UltraZedボードとUltra96ボードの兼用であるため、ショットキーバリアダイオードで電源のORを取っています。どちらの電源がONしているかを2色LEDの色で表示しています。初版からリセットSWとDIP SWを追加しました。
-
BOM
初版は試作のため、全て手ハンダで実装しました。改版後は顕微鏡下での手ハンダが大変なので、SMD部品2個のみを実装してもらいました。Gerberデータの他にBOMのエクセルファイルをベンダーのサイトにアップロードします。PCBアセンブリ費用29.83 USDの内訳は、部品費6.64 USD、セットアップ費20.00 USD、組み立て費0.92 USD、サービス費2.27 USDとのことでした。2枚SMD実装してもらって29.83 USDなら苦労してハンダリフローや手付ハンダをしなくても良いかもしれません。
-
サプライヤ
プロトタイプボードを作成する場合には、これもひと昔前はワイアラッピングやハンダ付けで作成したものですが、最近では10ドル以下で5枚程度のPCBを作成できる工場が現れてきました。これだとユニバーサル基板で作成したほうが高くつくくらいです。
●PCBWay (https://www.pcbway.com/) ⇒基板10枚で5USD
●FusionPCB (https://www.fusionpcb.jp/) ⇒基板10枚で4.9USD
●BokTech (https://www.boktech.cc/) ⇒基板5枚で1USD
●JLCPCB (https://jlcpcb.com/) ⇒基板10枚で2USD
これは送料別の金額であり、Boktechだと、5枚製造して1ドルという、ユニバーサル基板よりも安い金額です。しかしながら送料が別途20ドル程度かかるので、今回はSeeed Fusion PCBにしてみました。10枚製造して送料込みで7.9ドルです。 -
PCB設計ツール
PCB設計ツールも無償のものがあり、無償の範囲で十分実用的なPCBが設計できます。今回はEAGLEというPCB設計ツールをインタフェースボードの設計に使用してみました。
自動配線している状況です。
PCB製造業者とはGerberフォーマットのファイルでインタフェースしますが、設計が完了したレイアウトデータをビュワーにかけたものが次図です。
http://mayhewlabs.com/webGerber/ -
生基板
今回はSeeed Fusion PCBに基板を発注し、約22日で届きました。製造に13.16日、配送は最安のシンガポール郵便で9.67日となっています。
費用は送料込みで7.9 USDでした。送料込み約860円くらいで試作基板が5枚作成できるので、ユニバーサルボード一枚が200円前後することを考えると、非常にお得です。
改版時にBoktechにより5枚を製造し、2枚をSMD実装したPCBです。
-
組み立て
部品をハンダ付けにより実装していきます。試作のため手ハンダを前提としたことにより、SMDを極力避けたのですが、レベル変換ICのTXS0108EPWRはDIPの製品が存在せず、SMDとなってしまいました。
SMDのアマチュア的ハンダ付けには、手ハンダ、ホットプレート法、トースター法等がありますが、今回は手ハンダでやってみます。手付けでなんとかきれいに付けることができました。次図にTXS0108EPWRの手ハンダ実装後の顕微鏡写真を示します。
手ハンダについては以下の動画を参考にさせて頂きました。
https://www.youtube.com/watch?v=5uiroWBkdFY
ジョイスティックとスイッチ入力装置
ジョイスティックとスイッチの入力装置を作成します。ジョイスティックもプッシュスイッチも単なるマイクロスイッチです。ただし、問題は遠いところにあることです。例えば線長が1m程度もあります。そのままだと反射が激しくなり、伝送線路の終端が必要になります。今回の場合終端しなければ、しばしば50MHz程度の定常波が発生しました。
写真のジョイスティックやプッシュスイッチはパーツショップで1,000円程度で購入できます。またそれらを取り付ける台は100円ショップで半透明のPPケースを購入しました。以下に、ボード以外で必要な部品表を示します。
品名 | 単価 | 数量 | 備考 |
---|---|---|---|
ジョイスティック | 980円 | 1 | http://akizukidenshi.com/catalog/g/gP-10992/ |
プッシュスイッチ | 150円 | 2 | http://akizukidenshi.com/catalog/g/gP-07248/ |
PPケース | 100円 | 1 | https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplehome/20170303/20170303152651.jpg |
ネジM4x6mm | 5円 | 4 | http://akizukidenshi.com/catalog/g/gP-07326/ (100個入り500円) |
ナットM4 | 10円 | 4 | http://akizukidenshi.com/catalog/g/gP-11730/ (50個入り500円) |
フラットケーブル5芯 | 79円 | 1.0m | https://www.sengoku.co.jp/mod/sgk_cart/detail.php?code=EEHD-04N5 (158円) |
配線材料 | - | 少々 | http://akizukidenshi.com/catalog/g/gP-11641/ (1mx10本入り300円) |
PMODスイッチインタフェース
Ultra96対応だけであれば変換ボードに吸収しても良いのですが、 スイッチ類をUltraZedボードに差し替えて使用する場合には、前述のとおりインタフェースが必要になります。その理由は、遠い場所にあるスイッチは単なるロジックではなく、伝送線路扱いとしなければ反射が起き、正常に動作しないためです。
この反射を抑えるために、次図のような回路を設計しました。
このボードを作成しました。手ハンダでも作成できるようにSMDは使用しないようにしています。
非常に小さい基板で、以下に基板の100円硬貨との比較写真を示します。基板業者は前回と同じSeeed Fusion PCBで、今回はOSCという速達配送業者を利用して9.9USDでした。基板のみで製造に7.94日、OSCによる配送にシンセンから日本まで3.13日かかっています。シンガポール郵便よりはだいぶ早いですね。9.9USDの内訳は、7.9USDが基板制作代、2.0USDがOSCの配送料のようです。
前回はソルダーレジストを白で製造しましたが、今回は黒にしてみました。
これに部品を実装したものを次図に示します。
Vivadoの準備
最初にFPGAを勉強するのに、いきなりPCBを作るのはもちろんハードルが高いので、まずLEDチカチカを行いました。それができるまでにもハードルがあり、まずVivadoのインストールがあります。それができてもボード定義ファイル(bdf) という、Ultra96ボード専用の設定はVivadoに入っていなかったため、Githubからダウンロードし、適切な位置、例えばC:\Xilinx\Vivado\2018.3\data\boards\board_filesに配置する必要があります。
グラフィックディスプレイコントローラ(GDC)の設計
高機能なFPGAボードを使って、Lチカでは、パイプクリーニングの意味はあってもそれ自体は面白くありません。何か面白いものを作成しようと思い、まずVGAのコントローラを設計しました。
グラフィックディスプレイの解像度は様々なものがあり、タイミング設計の際に参考になるサイトをご紹介します。
VGAタイミングのサイトにより、タイミングを決定しました。VGAはアナログ信号であり、あまり高解像度には向かないため、横800dot、縦600dotのSVGAとしました。以下の表にSVGAタイミングを先のサイトから引用します。
基本的には、横800dot×縦600dot×色深度、例えば8色であれば3bitの情報を記憶するメモリが中心となります。これをVRAM (Video RAM)やGRAM(Graphic RAM)と呼びます。
少々問題になるのは、VRAMは2つのバスマスタによりアクセスされることです。第1にグラフィックディスプレイコントローラ(GDC)、第2に描画マスタであるCPU。ただし、描画マスタはCPUとは限らず、ハードウェアでも構いません。問題は、両者が同時に同じ領域にアクセスすると、VRAMのポートがひとつしかない場合には待たされることになります。GDCはディスプレイのタイミングに合わせて画像を読み出す必要があるため、待たすことができません。従ってCPUを待たすことになります。
一方、VRAMのポートを増やして2つにすれば、この問題は解決することができます。これをデュアルポートメモリと呼びます。以下の図は非同期のデュアルポートメモリを用いたディスプレイ表示装置の例で、ブルーのクロックドメインとグリーンのクロックドメインの周波数は特に関係がありません。グリーンは前記のようにSVGAのタイミングで動作します。
FPGAには真のデュアルポートメモリはありません。従って、シングルポートメモリを2つ並列に実装し、書き込みの際には同じデータを両方のメモリに書き、読み出しは片方から読むことで、ひとつのメモリとしては1R1Wのシングルポートとなります。ただし、これはVivadoがやってくれるので意識する必要はありません。
タイミング設計
まず、ピクセルクロック(ドットクロックとも呼ばれる)は先の表より40.0MHzとします。これがタイミングの最小単位で、水平表示期間(HD)の800dotはこのクロックが800クロック分となります。さらに水平期間はこれだけではなく、表示期間(HD)以外に3つの期間があり、それぞれフロントポーチ(FP)、同期パルス(SP)、バックポーチ(BP)に分かれます。
水平タイミングを例にとれば、それぞれHFP, HSP, HBPとなります。トータルでは表示期間であるHDをすべて加えて、HD+HFP+HSP+HBP=40+128+88+800=1056が水平期間(26.4us)となり、これをラインと呼びます。水平周波数は37.878787KHzとなります。
垂直タイミングはこのラインがひとつの単位となります。表示期間はVD=600、その他非表示期間はVFP=1、VSP=4、VBP=23となり、トータルでは628ラインとなります。ラインが集まり一つのフレームを構成し、その周期は16.5792msec、垂直周波数は60.3165412083Hzとなります。
先の表と一致していることを確認してください。
縦横タイミングを組み合わせ、論理画面と物理画面を並べると以下の図のようになります。
論理画面が256x256と小さいので、縦横2倍の拡大をしています。ちなみに、2倍の拡大は難しくなく、カウンタをメモリアドレスに出力する際に1ビット右シフトしているだけです。そうすれば縦横共同じ情報が出力されるため、2倍の拡大となるわけです。
Vivadoブロックエディタにより作成したものを以下の図に示します。
シミュレーションを駆使し、動作の確認を行っています。
ただ、このままでは描画マスタを設計していないため、静止画を表示することしかできません。静止画を表示するには、Bitstream中に初期VRAMデータを埋め込みます。ブロック図でデュアルポートメモリをダブルクリックすると、以下の画面が現れ、"Other Options"の"Memory Initialization"にファイル名を入力します。
ここでは、dual port memoryの初期データファイルとして、スペースインベーダーの画像を取り込むことにします。データは以下のようなフォーマットで構成します。Vivadoにはデータがフォーマットに沿っているかの確認を行うコマンドがあり、validateを実行し、正しいデータかどうか確認した後にwrite bitstreamを行います。
以下にGDCを起動したところの図を示します。残念ながら、描画マスタを設計しておらず、表示マスタのみが動作しているため、画像は静止しています。
描画マスタの設計
通常はデュアルポートメモリの片側(クロックドメイン図の左側)はCPUを用いて、プログラムから読み書きすることになります。言うまでもなくソフトウェアのほうが、複雑なシーケンスを制御しやすいためです。ところがFPGAはハードウェアといえどもプログラム可能なハードウェアであり、ましてHDLにより設計することから、フルハードウェアで描画マスタを構成することにトライしようと思います。
テーマは今年(2018年の記述です)40周年を迎えた「スペースインベーダー」です。フルハードウェア(ソフトウェアを一切使用しない)でこれが動けば、たいていのものはハードウェアでできるのではないでしょうか。
描画マスタをハードウェアで構成したものを以下の図に示します。
Ultra96ではありませんが、同じFPGA搭載のUltraZedボードにおいて、インベーダが動作しているところを、以下の図に示します。
サウンドコントローラの設計
音を出すには、まず可聴周波数帯域の適当な振幅(775mV RMS)のアナログ信号を出力します。そのために前述したPMOD-I2SというDACが必要になります。出力したアナログ信号を、任意のパワーアンプで増幅した後スピーカを駆動します。今回はUSB接続アンプ付きスピーカを用意しました。
オーディオDAC仕様
- 製品データシート: https://reference.digilentinc.com/_media/reference/pmod/pmodi2s/pmodi2s_rm.pdf
- 使用DACデータシート: https://www.mouser.com/ds/2/76/CS4344-45-48_F2-472818.pdf
このデータシートによればステレオの24ビットオーディオDACとのことで、ゲームサウンドには高級すぎますが、それほど高価ではないのでこれを使うことにします。インタフェースはI2Sというシリアルデータです。 マニュアルに掲載されているシリアルデータフォーマットを次の図に示します。
16bitデータ2ch(L, R)の32bitデータをシリアルでDACに供給しますが、注意点は図のように1ビットズレていることです。
waveデータ仕様
入力としてwaveファイルを受信し、デコードし、I2Sシリアルデータを出力するようなモジュールを作成します。以下に入力のwaveフォーマットを示します。 http://sky.geocities.jp/kmaedam/directx9/waveform.htmlの魚拓
このwaveをデコードするFSMを設計します。以下にサンプルのwaveフォーマットを示します。
サウンドは、ここから入手しました。入手したwavファイルのサンプリング周波数とデータ精度がバラバラだとハードウェアで扱うには厳しいので、全て11.025KHz、8bit、Mono、メタデータ無しに変換しておきます。そのコマンドは以下のとおりです。
$ ffmpeg -i input.wav -ac 1 -ar 11025 -acodec pcm_u8 -fflags +bitexact -flags:v +bitexact -flags:a +bitexact output.wav
設計制約
設計制約をリストアップします。
LRCLKの1周期に対し、L=16bit, R=16bitの32bitのシリアルデータが必要であり、シリアルデータはSCKでシフトされるので、SCK=LRCLKx32。ただしこのSCKは、DACに供給しなければDAC内部で発生されます。
データフォーマットより、サンプリングレートは16進数で16'h2b11=11025、すなわち11.025KHz。LRCLK=Fs(サンプリング周波数)とのことで、LRCLK=11.025KHzとしたいところですが、下図において、LRCLK=11.025KHzが存在しないので、データのほうを4倍のインターポレーションすることにし、LRCLK=44.1KHzとします。
上記関係式よりSCK=1.4112MHz。
マスタークロック(MCLK)は下図のように、256, 384, 512, 768, 1024倍等の任意性がありますが、ここでは256倍の11.2896MHzを使用します。
タイミングチャート
クロックに関する設計制約がだいたい解決したので、タイミングチャートを書いていきます。基本的にハードウェアベースのサウンド出力であるため、FSMによるフォーマット解析を行います。タイミングチャートは以下のとおりです。
ステートマシンのクロックFSMCLKは上図のように、ステートアドレス、ステートデータ、データアドレス、データデータの4クロックで1サウンドデータの読み出しになることから、4xFSMCLK=2x16xSCLK、これよりFSMCLK=176.4KHz。
DACが要求する16bitデータ×2ch(L, R)の32bitデータについては、データソースが8bitモノラルであるため、8bitデータをMSB側に詰め、残りは0詰めし、16bitとします。LとRには同じデータを供給します。
4倍インタポレーションのやり方は線形補完する方法と、同じデータを繰り返す方法がありますが、もともと16bit@44.1KHz表現可能なDACに対して8bit@11.025KHzという荒い音質であることから、同じ32bitデータを4回繰り返すことにします。
以上で設計制約から来るクロック周波数とタイミングチャートが確定したので、これに基づいてFSMを設計します。
設計指針
設計指針を記述していきます。
- サウンドデータはwaveフォーマットでROMに8bit単位で格納する。フォーマットは非圧縮PCM、8bit、モノラル、11.025KHzサンプリング
- FSMはFSMCLKで動作する。FSMCLK=176.4KHz(=1/8xSCLK)。
- 8bitサウンドデータの読み出しは、FSMからアドレスを与えることで行う。
- FSMから並列直列変換(以降パラシリ)にROMデータを供給する。
- パラシリでは8bitデータをDACの要求する16bitデータに伸長する。具体的にはROMデータを上部8bitに詰め、下部8bitはゼロを詰める。
- パラシリのシフトはSCLK(=BCLK)で行う。SCK=1.4112MHz。
- パラシリの1フレームはLRCLKの立下りで開始する。LRCLK=44.1KHz(=1/32xSCLK)。
- 変換されたシリアルデータは1SCLK遅らせてDACに出力する。
アーキテクチャ設計
以上の指針からブロック図を書くと以下のようになります。
詳細設計
上記ブロック図(アーキテクチャ設計)をもとに、ブロック内部をRTLで記述したものを下図に示します。 SCKはシリアルクロック(=ビットクロック)ですが、内部クロックモードを使用するために、コンスタント出力としています。基本的にZynqを除き、ブロック図と一致しています。
データ変換
ROMに入れるデータをVivadoで読めるCOEフォーマットに変換します。以下はwaveの16進ダンプファイルをCOEのデータ部に変換するコマンドです。これにヘッダとトレーラを付ける修正を行いROMデータとします。
$ od -An -t x1 -v input.wav|sed -e 's/ /,/g' >output.coe
シミュレーション
詳細設計に基づきシミュレーションを実施し、波形を確認し、不具合を修正するというループに入ります。 Vivadoはアナログ波形も表示可能であり、シミュレーションでアナログ波形を確認します。
もちろん全体だけでなく、細かいクロック毎の動きを見ていきます。ステートマシンが期待通りのステート遷移をしているか、LRCLKとシリアルビットデータの並びはDAC仕様に合っているかなどを見ていきます。
実験
FPGAにビットファイルをダウンロードし、実験します。STARTスイッチを押すと、それっぽい音が出ますが、元のファイルをPCで再生した音と異なるようです。破裂音が混ざっているようです。元の音はインベーダが破壊された時の音なので、破裂音が混ざっていても構いませんが、もし音楽だったら聴くに堪えないでしょう。
破裂音原因解析
シミュレーション波形を確認すると、アナログ波形がなんだか尖っています。アナログとデジタルの一致性が必要で、そのためにはサンプリング周波数の1/2以上の周波数をカットする必要があります(標本化定理)。
周波数帯域を制限するには22.05KHz以下をカットするロウパスフィルターを入れる必要がありますが、まずDACチップのデータシートの参考回路図を確認します。
上図のとおり、アナログ出力にRとCで構成されるロウパスフィルターが入っています。
次にそれがどのように実装されているかを、PMOD DACモジュールのデータシートで確認します。
出力のロウパスフィルターが入っていないことがわかりました。 サンプリング周波数は44.1[KHz]であったので、ロウパスフィルターの時定数を決定します。 Cを求める式は、
となることから、アナログ出力に40nFのキャパシタ(0.04μFのセラコン等)を挿入すれば良いことになります。
ここでアナログ波形をDSOで取得してみます。下図のような波形が取得されました。青がシリアルDACデータ、黄色がDACの出力のアナログ波形です。
一方で、サウンドデータをAudacityで開いてみると、下図のような低音の波形となっています。これはインベーダの進行音です。
シミュレーション波形やwaveデータ波形は上図のようであり、アナログ波形はこのようでなければならないはずなのに、DSO取得図ではある閾値以上と以下で波形が折り返されているようです。ここで思いつくのがMSBが反転しているのではないかということです。ここまではwaveデータをそのままDACに入力すれば良いと思っていたので、データ構造を調べてみます。すると、以下の事がわかりました。
- 8bitPCMデータは符号なし
- 16bitPCMデータは符号付き
従って、パラシリ部で8bitから16bitへ伸長する際にLSBへのゼロ詰めだけではなく、符号なし⇒符号付き変換を実施しなければなりません。RTLにMSBを反転する修正を加えたところ、次の図のような正しいアナログ波形が得られました。
デジタル的に出力すればOKのグラフィック系と異なり、オーディオ系は正しく動作させるのに案外苦労が必要でした。サウンドの重畳、リアルタイム性、ノイズ防止の考慮等を含めるとさらに大変です。
出来上がったサウンド系階層のブロック図を次に示します。これはソフト階層(中が見える階層であり、IPインテグレータで作成)で、中にステートマシン、サウンド格納用ROM、パラシリ変換ユニットから構成されます。
基本的に左側のステートマシンが、外部から与えられた音色コードに従い、右上のサウンド格納用ROMから8bitPCM wave情報を読み出し、それを右下のパラシリ変換ユニットを用いてDAC用シリアルデータに変換します。
サウンド優先度の解決
サウンドの難しい点は演奏終了以前に割込みが入ったらどうする等の、時間的な仕様を定義するところです。本来は同時発声チャネルを複数用意し、サウンドを重ね合わせれば良いのですが、今回は優先順位表(次図)を作成し、優先度の高いサウンドが、演奏中の優先度の低いサウンド演奏を中断させる仕様としてみました。優先度の高いサウンドを待たせるとおかしくなるためです。例えば、インベーダの移動音が弾の発射音を中断させると違和感を感じますが、逆は自然です。実際に、音のマスキング効果により、重畳しなくても特に違和感がないことが判明しました。
この優先順位表は、現在出力されているサウンドを横軸で表示し、その時に縦軸の割込みサウンドが来た場合にどちらを優先するかを判断するものです。
ゲームアルゴリズム
ゲームはプログラミングと同じように、FSMのステートをひとつずつ書いていきました。オリジナルのゲームソフトウエアが1/60秒で1ループを回っているとのことで、そのループ内で、自機の移動、自機の弾の移動及び衝突判定、インベーダの移動、インベーダの弾の移動及び衝突判定、UFOの発生及び移動、スコア表示、ゲーム終了判定等を実行します。
ハードウェアの場合は非常に速く実行されるので、1/60secの間、かなり待たせることになります(約98%がウエイト)。ちなみに、インベーダ数が減ってくると動作が早くなるのは、以下のような理由です。まず、1ループは1/60秒でその中でインベーダ1匹の処理、自弾の処理、自機の処理等を行います。つまり初期ではインベーダ全体は55/60秒かかって移動します。インベーダ数が減るとその分の処理がカットされ、インベーダの全体としては移動が速くなるわけです。一方で、自機や弾のスピードは速くなりません。
共通シーケンス
次の図に共通シーケンスの例を示します。パターンROMからいろいろな形、例えばインベーダ、自機、ベース、UFO等をVRAMに矩形転送するシーケンスは、多用されますが、それぞれにシーケンサを用意していると大変なロジック量になります。これをサブルーチンのように、{ソースX座標、ソースY座標、横幅、縦幅、デスティネーションX座標、デスティネーションY座標}の6個を入力パラメータとして、シーケンスを呼びだすことでシーケンスの共通化が可能です。ソフトウエアではサブルーチンと呼ばれますが、ハードウェアなので共通シーケンスと呼びます。共通シーケンスを呼び出す前には戻り先ステートをリターンレジスタに退避してから呼び出します。呼び出し先で戻るときにはリターンレジスタをステートに入れて戻ります。
本来はプロセッサのソフトウェアで実行する、十分複雑なシーケンス制御をハードウェアで実装してみて分かったのは、まさにプロセッサの進化のリバース(逆行)だということです。FSMシーケンスを共用化する目的で、前述のようにリターンレジスタを実装しましたが、これはプロセッサのリンクレジスタと同じです。言うまでもなく、ステートレジスタはPCに相当します。
水平マイクロ
今ではCPUの設計上、マイクロプログラム処理をあまり見なくなったのは論理合成の発達のためだと思います。論理合成が無い時代はハードウェア論理を変更するのは大変な作業でした。それを簡単にするために、FSMではなくマイクロプログラム処理を考え、ROMの内容を書き換えることで、処理の変更を行ったわけです。今回のFSMシーケンス記述は、いわば、インベーダ処理プロセッサの水平マイクロを書いているようなものです。ちなみに水平マイクロには、普通プロセッサレベルのISAには存在しない、マルチウェイ分岐等の強力な命令があります。
FSMのcall, returnメカニズム
FSMのcall, returnメカニズムとして、returnレジスタを設置し、caller側で戻り先ステート(通常は現ステートの次)をreturnに格納しておいて、calleeを呼ぶ機があります。単純な場合はこれで動作しますが、callが2段以上になると、returnレジスタが1段であるため、returnレジスタの退避等が必要になります。言うまでもなくプロセッサではスタックがこの自動化を行うわけで、プロセッサの場合の疑似コードを書くと次のようになります。
call CALEE;
:
CALEE:
:
return;
コール、リターンの動作的は、32bitプロセッサの場合、
rs[--sp] ← PC + 4;
PC ← CALEE;
:
CALEE:
:
PC←rs[sp++];
となります。
FSMの場合はPCはstateレジスタに相当しますが、それを退避するRS(リターンスタック)配列を設置し、SP(スタックポインタ)も設置します。とはいえ、ハードウェアでありリカーシブコールがあるわけでもないため、4段程度の簡素なスタックとします。2bitのSPも実装します。 実際にはreturnレジスタを除いて2段で済んだので(トータルで3段)、SPは1bitとしました。
さらに、上記のようにプリディクリメント、ポストインクリメントの処理とせず、ポスト処理のみとします。その理由はサイクルを増やせばプリ処理も可能なものの、Verilogが遅延代入であることから、簡単のためポスト処理のみとしています。
また、通常のスタックがpush down(積むと上から下にスタックが伸びる)であるのに比べて、実装するスタックは本当のスタック(地面に積むため、下から上に伸びる)にします。これはどちらでもコストは同等です。
以上のような設計だと、FSMのコールは、次図のような疑似コードとなります(caller saved)。
これは全ての場合に動作しますが、リーフコールが多い(ほとんど)のにも関わらず、リーフではスタックは不要です。従って、call先を見分ける必要はありますが、コードの効率化を考えて呼び出しをリーフ(緑)コールと非リーフ(赤)コールに分け、リーフをコールする際は以下の図のようなコードを用います。
これは、元から使用していたコードと同じものとなります。returnを破壊する一般FSMを赤、returnを破壊しないリーフFSMを緑で表しています。
今回はcaller savedで実装しましたが、callee savedのほうが若干容易かもしれません。caller savedでは呼ぶ前にcalleeがreturnを壊すか壊さないかを調べてからcallしなければならないのに比べて、callee savedであれば、全てreturnレジスタに入れてcallし、callee側でさらに呼ぶときは(つまりreturnを壊す場合は)スタックに入れるという流れであるためです。
FSMのダイレクトリターン
さて、非リーフFSMコールの後、リーフコールがある場合は、単純に前記シーケンスの組み合わせとなります。図で示すと次図のようになります。
ここで結構見られるのが、リーフを呼んで戻った後、何もせずにさらにcallerに戻るパターンです。これを実現するより効率的な方法として、ダイレクトリターン手法があります。これを次図に示すと、リーフをコールする前にスタックから戻り番地を取り出しreturnに入れ、リーフへジャンプします。リーフからは知らずにリターンすると、ダイレクトリターンが行われます。
特殊な場合として、上記のようにパラメータ設定だけしてリーフにジャンプするような場合は、そもそもリーフであるため、以下の図のような呼び出しとなります。
例として、残機表示サブFSMは、(リーフである)copyAreaを呼び出すので、returnを破壊するため非リーフですが、その後のシーケンスでcopyAreaを呼び出しダイレクトリターンを行います。その際には上図のダイレクトリターン手法は取れないため、その上の図のダイレクトリターン手法を取っています。
ESDAツール
最近ではSDSoCなりVivado-HLSなりが使えるので、Cで書こうかとも思いましたが、今回はVerilogで記述してみました。VerilogはCに対するアセンブラとも言われますが、抽象度的にもコード量的にも、Cとアセンブラほどの差はありません。ESDAツールでステートマシンの図を書いて設計する方法もありますが、高位合成とどちらが良いか試してみたいと思っています。
結果
GIF:
YouTube:サウンド付きはこちら
出典:
続きはこちら
Discussion