![]() |
28 |
bitmap editorの開発 |
Posts Tagged with "space invaders"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
11 |
CmodA7toPMODボードへのSpace Inavdersの移植 |
![]() |
CmodA7toPMODボード
基本的には過去記事に対してボードをCmodA7ボードに変更したものです。 DigilentからCmodA7ボードを購入しました。このボードは(弊社開発の)PMOD変換ボードは必要となりますが、総額では安くSpace Invadersを動かすことができます。

周辺インタフェースボード等
Space Invadersを動作させるには、CmodA7ボードの他に必要なものは以下のとおりです。
- 12V電源
- PMOD I2Sインタフェース(オーディオDAC)カード
- オーディオケーブル

図731.1 PMOD I2Sインタフェースとケーブル - アンプ付きミニスピーカ

図731.2 アンプ付きミニスピーカ - PMOD VGAインタフェースカード
- VGA/HDMIインタフェース

図731.3 PMOD VGAインタフェースとVGA/HDMIインタフェース - VGA/HDMIインタフェース用5V電源
- HDMIケーブル
- PMOD JOYスティック・スイッチインタフェース(弊社開発)

図731.4 PMOD JOYスティック・スイッチインタフェース - JOYスティック・スイッチ(弊社開発)

図731.5 JOYスティック・スイッチ - Micro-B USBケーブル

図731.6 Micro-B USBケーブル 図731.7に、これら周辺を全て接続したCmodA7ボードの写真を示します。
図731.7 CmodA7toPMODボードに周辺を接続
CmodA7-35ボードへの移植
Arty-35とFPGAアーキテクチャが同じであり、何も変更せずにそのままで動作しました。
![]() |
17 |
PL UARTの接続とPCでの表示 |
![]() |
PL部UARTのPCへの出力
以前Space Invadersのゲーム実行中のVRAM画面の吸出しを実施しました。過去記事ではこれ等が相当します。これは、メモリダンプモジュールをBSVにより設計し、UARTの送信機能を用いてUART/USB変換ボードを経由してPCとUSBで接続し、PC上のPuttyでメモリダンプ情報を取得するというものです。
ところが、再度実行しようとしたところ、どうしてもUARTの出力の取得ができませんでした。前回は比較的簡単にできてしまったので、記事にはpin設定であるxdcの詳細は書いていませんでした。単にUART_TXに接続するとのみと書いており、端子番号は今は定かではありません。
こうなると基本から調べなければならないので、まずUltra96V2の回路図を見ると、なんとPS部からの接続となっているようです。

UART/USB変換ボードのJ1の2 pinにUART_TXが接続されていますが、これはPS_MIO0のUART1_TXであり、PLからはPIN配置でエラーになるため、PS部であるBank 500のU4端子に接続することはできません。図605.2にUltra96-V2ハードウェアユーザーズガイドの抜粋を示しますが、やはりBank 500(PS部)のU4端子となっています。

従って、最後の手段としてボード上に配線をハンダ付けし無理やりJ1に出力することを考えます。
まず図605.3はPL部UART_TXの引き出しを示す回路図です。

このUART_TXを次のxdcによりG6端子に割り当てます。これはPL部の出力(Bank 26)です。
set_property PACKAGE_PIN G6 [get_ports {UART_TX}]
最後にUART_TXを割り着けた端子G6とJ1の2 pinの間に配線をハンダ付けし、ショートしてやります。レベル的に本来はTrのD側ではなくS側に接続すべきですが、3.3Vにpull upされているため、これでもVOHは満足しているようです。
このようにすることで921,600bpsにより、VRAM内容の送信がうまく動作しました。が、以前設定だけでできた理由は不明のままです。
PCへの出力結果の確認
以下に、得られたlogファイルを画像に変換するフィルタを再掲します。
log2ppm.c
#include <stdio.h>
void main() {
char line[4096];
char ch;
printf("P3\n256 256\n255\n");
for(int y = 0; y <= 255; y++) {
fgets(line, sizeof(line), stdin);
for(int x = 0; x <= 255; x++) {
ch = line[x] - 0x30;
if ((ch & 0x4) != 0) printf("255 "); // R
else printf("0 ");
if ((ch & 0x2) != 0) printf("255 "); // G
else printf("0 ");
if ((ch & 0x1) != 0) printf("255 "); // B
else printf("0 ");
}
printf("\n");
}
}
以下のコマンドによりフィルタを作成します。
$ gcc -O log2ppm.c -o log2ppm
これを下記のようにフィルターとして実行し、ログデータを画像ファイルに変換します。
$ ./log2ppm <putty.log >putty.ppm
生成されたppm図形を図605.4に示します。

![]() |
2 |
画面の縦横の入れ替え |
![]() |
画面の縦横の入れ替えの変更
過去記事において、本来VRAMの読み出しアドレスのxとyを入れ替えるつもりが、たまたまwrite側のアドレスマルチプレクサが目につき、それを流用しました。が、動作的な欠点が目立ちました。具体的には、画面書き換えの途中で切り替えると、縦図形と横図形が混在したり、初期画面を動的に準備する負担が大きくなっていました。
これを本来のread側アドレスマルチプレクサに変更します。ただし、write側マルチプレクサに追加してread側マルチプレクサの増設が必要になります。
write側アドレスマルチプレクサは名前をWriteMuxに変更しましたが、元の単なるセレクタに戻しました。新たにReadMuxモジュールを設置し、読み出し側アドレスのxとyを入れ替えます。実際にはyはx(a[7:0])をそのまま用い、xはy(a[15:8])を256から引いたものを用います。
$$ \begin{eqnarray} \left\{ \begin{array}{l} x&\Leftarrow&256-y \\ y&\Leftarrow&x \end{array} \right. \end{eqnarray} $$
図561.1に改造後のブロック図を示します。

以下にBSVソースを示します。処理はwrite側で実施した入れ替えとほとんど同様です。
ReadMux.bsv:
// アドレス型の定義
typedef Bit#(16) Addr_t;
// マルチプレクサのインタフェース定義
interface ReadMux_ifc;
(* prefix="" *)
// 出力アドレスを生成するメソッド
method Addr_t outp(Bool sel, Addr_t a);
endinterface
// マルチプレクサの実装
(* synthesize, always_ready = "outp", no_default_clock, no_default_reset *)
module mkReadMux(ReadMux_ifc);
// 出力アドレスを生成するメソッド
method Addr_t outp(Bool sel, Addr_t a);
// xa: aの下位8ビット
Bit#(8) xa = a[7:0];
// yax: aの上位8ビットから算出
Int#(10) yax = 256 - signExtend(unpack(a[15:8]));
// ya: yaxを8ビットにトリミング
Bit#(8) ya = truncate(pack(yax));
// selがTrueなら加工したアドレスを返す
if (sel) return {xa, ya};
// selがFalseなら元のアドレスをそのまま返す
else return a;
endmethod
endmodule
この変更により、ボード上のdip swを切り替えることで、リアルタイムに画面の縦横変換ができるようになりました。
![]() |
27 |
BSVによるSpace Invadersの変更 (9) |
![]() |
![]() |
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ボタンの配置を示します。

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

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 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の最適化により定数となっており、レジスタは存在しませんでした。結論として、マクロで定数定義してもレジスタ宣言しても、オーバヘッドは変わりません。
ページ:



