![]() |
27 |
BSVによるSpace Invadersの変更 (9) |
![]() |
Posts Tagged with "space invaders"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
26 |
BSVによるSpace Invadersの変更 (8) |
![]() |
Y字リプレースアニメーションのソース
Y字リプレースアニメーションのソースを示します。Y字リプレースアニメーションもFボタンにより中断するため、各所でFボタンを見ています。
function Stmt replaceY;
return (seq
// from right to left
for (i <= 228; i >= 142; i <= i - 2) seq
copyArea((pack(i)[1] == 1'b1) ? 68 : 84 , 32, i, 67, 10, 8);
wait_timer(`TICK_WAIT3);
if (fbutton) break;
endseq // for
if (fbutton) break;
// from left to right
for (i <= 136; i <= 226; i <= i + 2) seq
copyArea((pack(i)[1] == 1'b1) ? 75 : 91 , 107, i, 67, 16, 8);
wait_timer(`TICK_WAIT3);
if (fbutton) break;
endseq // for
eraseArea(226, 67, 16, 8);
wait_timer(`TICK_WAIT32);
if (fbutton) break;
// from right to left
for (i <= 226; i >= 136; i <= i - 2) seq
copyArea((pack(i)[1] == 1'b1) ? 77 : 93 , 117, i, 67, 16, 8);
wait_timer(`TICK_WAIT3);
if (fbutton) break;
endseq // for
wait_timer(`TICK_WAIT32);
if (fbutton) break;
eraseArea(141, 67, 9, 8);
wait_timer(`TICK_WAIT32);
if (fbutton) break;
endseq);
endfunction
これだけでなく、タイマールーチンの中でもFボタンによる中断を見ていますが、ちょっとやり過ぎのようです。実際には多少間引いても体感に影響しないと思います。
![]() |
23 |
BSVによるSpace Invadersの変更 (7) |
![]() |
オープニングアニメーションのソース
オープニングアニメーションのソースを示します。オープニングアニメーションはFボタン(コイン投入の模擬)により中断するため、各所でFボタンを見ています。
function Stmt openingAnimation;
return (seq
// Opening Animation
foa <= True;
eraseArea( 0, 41, 255, 199); // erase screen
eraseArea(25,242, 5, 7); // erase zanki
stringS1; // PLAY ...
if (fbutton) break;
wait_timer(`TICK_WAIT64);
if (fbutton) break;
stringS2; // *SCORE ...
if (fbutton) break;
wait_timer(`TICK_WAIT32);
if (fbutton) break;
stringS3; // =? MYSTERY ...
if (fbutton) break;
wait_timer(`TICK_WAIT64);
if (fbutton) break;
replaceY; // ^ -> Y
if (fbutton) break;
wait_timer(`TICK_WAIT64);
if (fbutton) break;
foa <= False;
endseq);
endfunction
![]() |
22 |
BSVによるSpace Invadersの変更 (6) |
![]() |
オープニングアニメーションの追加
テスト用のソースを示します。コンパイル時間短縮のため、ゲーム部分をカットしています。
// メインフロー
Stmt main = seq
while (True) seq
while (!fbutton) seq
openingAnimation; // Fボタンによりブレーク
endseq // while
openingDisplay; // 表示のみ、ボタンを待たない
await(sbutton); // Sボタンによりブレーク
openingDisplay2; // タイマーによりブレーク
// game start
endseq // while
endseq;
// CREDIT 01, "PUSH ONLY 1PLAYER BUTTON"
function Stmt openingDisplay;
return (seq
eraseArea( 0, 41, 255, 199); // erase screen
stringS5; // PUSH ONLY ...
copyArea(10, 162, 217, 241, 5, 7); // CREDIT 00->01
endseq);
endfunction
// CREDIT 00, "PLAY PLAYER<1>, 00000"
function Stmt openingDisplay2;
return (seq
eraseArea( 0, 41, 255, 199); // erase screen
stringS6; // PLAY PLAYER<1>
copyArea(2, 162, 217, 241, 5, 7); // CREDIT 01->00
for (i <= 1; i < 15; i <= i + 1) seq
// erase zero
eraseArea(40, 25, 37, 7);
wait_timer(`TICK_WAIT4); // wait 66.66msec
stringS7; // 00000
wait_timer(`TICK_WAIT4); // wait 66.66msec
endseq // for
endseq);
endfunction
![]() |
21 |
BSVによるSpace Invadersの変更 (5) |
![]() |
オープニングアニメーションの追加
この動画の最初の部分を参考にして、オープニングアニメーションを作成します。
- インベーダの種類や点数の紹介、と同時に逆さYを引っ張って行き正立Yに入れ替えます。アニメーションがメインなので、これをopeningAnimationシーケンスと呼び、同名の関数により実行します。Fボタンによりコイン投入を模擬します。

図513.1 openingAnimation画面
- コインを投入すると、"PUSH ONLY 1PLAYER BUTTON"と表示され、CREDITが+1されます。アニメーションは無いため、これをopeningDisplayシーケンスと呼び、同名の関数により実行します。Sボタンを待ちます。

図513.2 openingDisplay画面
- Sボタンを押すと、"PLAY PLAYER<1>"と表示され、CREDITが-1されます。同時に得点が"00000"となり、点滅します。これをopeningDisplay2シーケンスと呼び、同名の関数により実行します。

図513.3 openingDisplay画面1

図513.4 openingDisplay画面2
- ゼロ点滅を規定回数実行すると自動的にゲームを開始します。
図513.5にSボタンとFボタンの配置を示します。

図513.5 ボタン配置図
![]() |
16 |
BSVによるSpace Invadersの変更 (4) |
![]() |
Graphic Controlerの再設計
完成したIP diagramを510.1に示します。3個のサブモジュールをまとめたので、graphic階層はなくなりました。

図510.1 BSVによるグラフィックコントローラ
BSVソース
GraphicFSM.bsv:
// グラフィックディスプレイコントローラー、BSVによる実装
import StmtFSM::*;
// 各種タイミングパラメータの定義
`define HD 800 // 水平解像度
`define HFP 16 // 水平フロントポーチ
`define HSP 80 // 水平同期期間
`define HBP 160 // 水平バックポーチ
`define HO `HFP + `HSP + `HBP // 水平オフセット
`define HL `HD + `HO // 一行当たりのピクセル数
`define VD 600 // 垂直解像度
`define VFP 1 // 垂直フロントポーチ
`define VSP 3 // 垂直同期期間
`define VBP 21 // 垂直バックポーチ
`define VO `VFP + `VSP + `VBP // 垂直オフセット
`define VL `VD + `VO // 一画面当たりの行数
// その他の定義
`define EHD 512 // 有効水平解像度
`define EVD 512 // 有効垂直解像度
// ログ幅の定義
`define HW 11 // log2(1056) = 10.04439
`define VW 10 // log2(628) = 9.294621
// アドレス型の定義
typedef Bit#(16) Addr_t;
// インターフェースの定義
interface GraphicFSM_ifc;
method Bool xhs(); // 水平同期信号出力
method Bool xvs(); // 垂直同期信号出力
method Addr_t address(); // VRAMアドレス出力
(* prefix="" *)
method Action idata(Bit#(4) indata); // VRAMデータ入力
(* prefix="" *)
method Action expl(Bool exp); // 爆発入力
method Bit#(1) rd(); // 赤出力
method Bit#(1) gd(); // 緑出力
method Bit#(1) bd(); // 青出力
endinterface
// モジュールの定義
(* synthesize,always_ready,always_enabled *)
module mkGraphicFSM(GraphicFSM_ifc);
Reg#(UInt#(`HW)) x <- mkRegU; // 水平方向のカウンタ
Reg#(UInt#(`VW)) y <- mkRegU; // 垂直方向のカウンタ
Reg#(Bool) in_xhs <- mkReg(False), // 水平同期信号フラグ
in_xvs <- mkReg(False), // 垂直同期信号フラグ
in_hdt <- mkReg(False),
in_vdt <- mkReg(False);
UInt#(`HW) ehoff = (`HD-`EHD)/2;
UInt#(`VW) evoff = (`VD-`EVD)/2;
Reg#(Bit#(4)) in_data <-mkRegU; // VRAMデータ
Reg#(Bool) in_exp <- mkReg(False); // 爆発フラグ
// メインループの定義
Stmt main = seq
while(True) seq
// for (y <= 0; y < `VL; y <= y+1) seq
y <= 0;
while (y < `VL) seq
// for (x <= 0; x < `HL; x <= x+1) action --- for consumes two cycles, then we like to use while
x <= 0;
while (x < `HL) action
if (((`HD+`HFP)<=x)&&(x<(`HD+`HFP+`HSP))) in_xhs <= False;
else in_xhs <= True;
if ((ehoff<=x)&&(x<ehoff+`EHD)) in_hdt <= True;
else in_hdt <= False;
x <= x + 1;
if (((`VD+`VFP)<=y)&&(y<(`VD+`VFP+`VSP))) in_xvs <= False;
else in_xvs <= True;
if ((evoff<=y)&&(y<evoff+`EVD)) in_vdt <= True;
else in_vdt <= False;
endaction // for -> while
y <= y + 1;
endseq // for -> while
$display("%3d %3d", y, x);
endseq // while(True)
endseq; // Stmt // xから水平オフセット(ehoff)を減算してパックし、右に1ビットシフトする。
Bit#(`HW)xx = pack(x-ehoff)>>1;
// yから垂直オフセット(evoff)を減算してパックし、右に1ビットシフトする。
Bit#(`VW)yy = pack(y-evoff)>>1;
// xxとyyを8ビットに切り詰める。
Bit#(8)xxx = truncate(xx);
Bit#(8)yyy = truncate(yy);
// xxxとyyyを合成して16ビットのアドレスを作成。
Bit#(16) in_addr = {yyy, xxx};
// 水平データタイミング(in_hdt)と垂直データタイミング(in_vdt)をANDで合成。
Bool in_dt = in_hdt && in_vdt;
// 爆発フラグ(in_exp)に基づいて赤色成分のデータを処理。
Bit#(1) in_rd = !in_exp ? in_data[2] & pack(in_dt) : (in_data[2] | in_data[1] | in_data[0]) & pack(in_dt);
// 爆発フラグ(in_exp)に基づいて緑色成分のデータを処理。
Bit#(1) in_gd = !in_exp ? in_data[1] & pack(in_dt) : 1'b0;
// 爆発フラグ(in_exp)に基づいて青色成分のデータを処理。
Bit#(1) in_bd = !in_exp ? in_data[0] & pack(in_dt) : 1'b0;
// ステートマシン生成
mkAutoFSM(main);
// メソッド定義
method Bool xhs();
return in_xhs;
endmethod
method Bool xvs();
return in_xvs;
endmethod
method Addr_t address();
return in_addr;
endmethod
method Action idata(Bit#(4) indata);
in_data <= indata;
endmethod
method Action expl(Bool exp);
in_exp <= exp;
endmethod
method Bit#(1) rd();
return in_rd;
endmethod
method Bit#(1) gd();
return in_gd;
endmethod
method Bit#(1) bd();
return in_bd;
endmethod
endmodule: mkGraphicFSM
- actionからendactionまでは1サイクル実行です。
- verilogと同様、"<="はノンブロッキング代入でDFFが、"="はブロッキング代入で組み合わせ回路がそれぞれ生成されます。
当初、水平のオフセットを表す定数EHOFFは、上記のような
UInt#(`HW) ehoff = (`HD-`EHD)/2;
という変数ではなく、define文により
`define EHOFF (`HD-`EHD)/2
のように定義していたのですが、defineの中でカッコや乗除算は使用できないようなので、変数としました。
ところが、生成されたVerilogを確認したところ、bscの最適化により定数となっており、レジスタは存在しませんでした。結論として、マクロで定数定義してもレジスタ宣言しても、オーバヘッドは変わりません。
![]() |
15 |
BSVによるSpace Invadersの変更 (3) |
![]() |
Graphic Controlerの再設計
引き続き、従来設計ではVerilogで設計していたものを勉強の目的からBSVに置き換えます。Graphic ControllerはVRAMへアドレスを出力し、VRAMデータを読み出し、また水平同期、垂直同期、表示期間等のタイミング信号を作成するモジュールです。
ついでにSVGAのタイミング変更をします。SVGAタイミング(魚拓)によれば、SVGA Signal 800 x 600 @ 75 Hz timingは以下のとおり。
| Screen refresh rate | 75 Hz |
|---|---|
| Vertical refresh | 46.875 kHz |
| Pixel freq. | 49.5 MHz |
ここで、Screen refresh rateは垂直同期信号周波数で、フレームのライン数(625)から自動的に決まります。Vertical refreshは水平同期信号周波数で、ラインのピクセル数(1056)から自動的に決まります。従ってここで重要なのはPixel freq.のピクセルクロック周波数のみです。
- Horizontal timing (line)
| Scanline part | Pixels | Time [µs] |
|---|---|---|
| Visible area | 800 | 16.161616161616 |
| Front porch | 16 | 0.32323232323232 |
| Sync pulse | 80 | 1.6161616161616 |
| Back porch | 160 | 3.2323232323232 |
| Whole line | 1056 | 21.333333333333 |
- Vertical timing (frame)
| Frame part | Lines | Time [ms] |
|---|---|---|
| Visible area | 600 | 12.8 |
| Front porch | 1 | 0.021333333333333 |
| Sync pulse | 3 | 0.064 |
| Back porch | 21 | 0.448 |
| Whole frame | 625 | 13.333333333333 |
Verilogによる設計(過去記事)では、水平カウンタ、垂直カウンタを別々に設け、水平のタイミングデコーダと垂直のタイミングデコーダにより同期信号等を作成していました。また、自機が破壊された場合に全画面を赤色表示にするモジュールを図414.2のように、後段に接続していました。また、VRAMデータ4bitのうちRGBを表す3bitを取り出すために、xisliceモジュールを用いていました。

図414.2 従来のグラフィックコントローラ階層図
今回BSVで再設計するにあたり、3個に分かれていたモジュール構成を1個にまとめます。
アルゴリズム説明
Graphics.bsvの中心部分:
y <= 0;
while (y < `VL) seq
// for (y <= 0; y < `VL; y <= y+1) seq
x <= 0;
// for (x <= 0; x < `HL; x <= x+1) action --- "for statement" consumes two cycles, so we like to use "while"
while (x < `HL) action
if (((`HD+`HFP)<x)&&(x<=(`HD+`HFP+`HSP))) in_xhs <= False;
else in_xhs <= True;
if ((ehoff<x)&&(x<=ehoff+`EHD)) in_hdt <= True;
else in_hdt <= False;
x <= x + 1;
if (((`VD+`VFP)<y)&&(y<=(`VD+`VFP+`VSP))) in_xvs <= False;
else in_xvs <= True;
if ((evoff<y)&&(y<=evoff+`EVD)) in_vdt <= True;
else in_vdt <= False;
endaction // for -> while
y <= y + 1;
endseq // for -> while
このように、y方向とx方向の2次元方向にドットクロックを数えます。コメントされている行のように、本来for文を2重で回したいのですが、資料事例で学ぶ BSVからの引用の図509.1に示すように、Stmt文内のfor文は2サイクルかかることに注意します。

図509.1 for文とwhile文
一方、while文は初期化に1サイクルかかるものの、インナーループでのチェックとアクションを1サイクルで実行できます。
for文のインナーループが2サイクルになるということは、2倍の周波数でFSMを駆動しなければならないことになります。現行では49.5MHzなので2倍では99MHzとなり、FPGAの上限に近くなってしまいます。
念のため99MHzで動作するfor文を用いたケースを合成し、正常動作を確認しましたが、タイミングクロージャや発熱等を考えると、回路はなるべく低速で回した方が望ましいです。
一方whileループであればインナーループが1サイクルで良いため、一旦for文で書いてから等価なwhile文に書き換えます。
![]() |
14 |
BSVによるSpace Invadersの変更 (2) |
![]() |
アルゴリズム説明
アルゴリズムの中心部分を説明します。一度に変換すると、bscによるデータサイズの推定がうまく行かなかったため、ステップに分解してデータタイプのヒントを与えています。ステップに分解しても結局組み合わせ回路が合成されるため、回路オーバヘッドはありません。
Bit#(8) xa = a[15:8];
横方向アドレスであるxaは縦方向アドレス(上位8bit)をそのまま使用します。
Int#(10) yax = 256 - signExtend(unpack(a[7:0]));
一方、縦方向アドレスyaは横方向アドレス(下位8bit)をunpackにより整数化し、符号拡張した上で256から引きます。
Bit#(8) ya = truncate(pack(yax));
その後unpackによりビットベクターとし、最後にtruncateで8bitベクターに戻します。データタイプ変換関数はここに掲載されています。
if (sel) return b;
else if (sw) return {ya, xa};
else return a; // normal state
最後にselがtrueならb(メモリダンプFSMによるアドレス)、falseでswがtrueならxとyの入れ替え&yの反転(90°回転)、falseならオリジナルのa(ゲームFSMによるアドレス)を選択します。
実行結果
図508.1に実行結果を示します。首を左に90度傾けた上で正しく実行できました。

変更後のソース
変更したMux.bsv:
typedef Bit#(16) Addr_t;
interface Mux_ifc;
(* prefix="" *)
method Addr_t outp(Bool sw, Bool sel, Addr_t a, Addr_t b);
endinterface
(* synthesize, always_ready = "outp", no_default_clock, no_default_reset *)
module mkMux(Mux_ifc);
method Addr_t outp(Bool sw, Bool sel, Addr_t a, Addr_t b);
Bit#(8) xa = a[15:8];
Int#(10) yax = 256 - signExtend(unpack(a[7:0]));
Bit#(8) ya = truncate(pack(yax));
if (sel) return b;
else if (sw) return {ya, xa};
else return a; // normal state
endmethod
endmodule
変更箇所はこのアドレスの縦横入れ替えと、VRAM初期値のデータの縦横入れ替えの2点となります。後者はプログラムでも可能ですが、csvに変換した上でexcelのコピーのオプションの行列の入れ替え機能で実施しました。
追記:改良版の記事はここ
![]() |
12 |
BSVによるSpace Invadersの変更 |
![]() |
画面の縦横の入れ替え
Landscapeのモニタを使用しているため、Space Invadersは正立していますが、本来から比べると縦方向に縮んでいます。これを修正するには画面の縦と横を入れ替えます。VRAMの読み出しアドレスのxとyを入れ替えれば良いはずです。
過去記事のVRAMのアドレス周りの図403.1を見ると、アドレスマルチプレクサが流用できそうです。これはFSMからのアドレスをメモリダンプ中にスイッチするものです。

元のMux.bsv:
typedef Bit#(16) Addr_t;
interface Mux_ifc;
(* prefix="" *)
method Addr_t outp(Bool sel, Addr_t a, Addr_t b);
endinterface
(* synthesize, always_ready = "outp", no_default_clock, no_default_reset *)
module mkMux(Mux_ifc);
method Addr_t outp(Bool sel, Addr_t a, Addr_t b);
if (sel) return b;
else return a;
endmethod
endmodule
このソースにおいて、中心部分はセレクタであり、
if (sel) return b;
else return a;
このようにselがtrueならb(メモリダンプFSMによるアドレス)、falseならa(ゲームFSMによるアドレス)を選択していました。
これに対し、図507.1のように新に1bitのスイッチを加え、スイッチがONのときにはゲームFSMによるアドレスの縦と横を入れ替えるように設計変更します。

具体的には通常時のアドレスa系のx座標アドレス(下位8bit)とy座標アドレス(上位8bit)を入れ替え、さらにy座標は上下反転します。上下反転は、256からアドレスを引くことで実現します。
$$ \begin{eqnarray} \left\{ \begin{array}{l} x&\Leftarrow&y \\ y&\Leftarrow&256 - x \end{array} \right. \end{eqnarray} $$
![]() |
9 |
BSVによるサウンドFSMの再設計 (10) |
![]() |
ページ:




