![]() |
1 |
QSPI Flashへの書き込み |
![]() |
Posts Tagged with "Design"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
31 |
Space Invadersの構成と物量 |
![]() |
ブロック図
ブロック図をIP Integratorで示します。

リソース使用量
各階層(ソフトブロック)のリソース使用量を図409.2に示します。

表409.1に示すように、BRAMの割合がかなり大きいです。全部で50個中、39.5個を使用しています。
リソース | 割合[%] |
---|---|
MMCM | 20 |
BUFG | 25 |
I/O | 16 |
BRAM | 79 |
FF | 6 |
LUT | 36 |
モジュール配置
各階層の配置状況を図409.3に示します。おもしろいことに、サウンドが4つのまとまりに分かれていますが、図409.4のように4つのステートマシン毎に固まっていました。


![]() |
28 |
BSVによる歩行音の改善 |
![]() |
インベーダの歩行音
インベーダの歩行音は、現在は隊列に同期して出力されます。インベーダ一匹の歩行処理は60Hzで実施されるため、初期状態では55/60secに一度、最後の一匹では1/60secに一度歩行音が出力されます。
最後の一匹の時に、どうも歩行音が速すぎると思っていたら、過去記事のとおり、インベーダの数に応じた間隔で歩行音が出力され、必ずしも隊列の動作とは同期していないことが判明しました。アニメーションは、インベーダーの数に比例して徐々に速くなるのに比べ、サウンドは段階的に速くなるのは、どのような意図があるのでしょうか?
なるべくオリジナルに近づけるという方針から、今回はこれを実装します。
歩行音間隔タイマの実装
まず、過去記事の表を実装します。タイマ初期値の最大値は52なので6bitのレジスタが55個必要です。1から55までを使用しており、0を含めた56個を定義しています。
UInt#(6) inv_init_stimer[56] = {5, 5, 7, 9,11,12,13,14,16,16,19,19,19,21,21,21,21,24,24,24,
24,24,28,28,28,28,28,28,34,34,34,34,34,34,34,34,39,39,39,39,
39,39,39,46,46,46,46,46,46,46,52,52,52,52,52,52};
過去記事の歩行音間隔タイマの仕様に示すように、これはタイムアウトした際に、インベーダ数に応じた値でタイマを初期化するための表です。次に、歩行音間隔タイマ本体を実装します。
Reg#(UInt#(6)) inv_stimer <- mkRegU;
次に、サウンドパターンは4から7を繰り返すため、2bitのパターンレジスタを用意します。
Reg#(UInt#(2)) inv_spattern <- mkRegU;
初期化部分です。タイマの初期値は52とします。パターンの初期値は0とします。
inv_stimer <= 52;
inv_spattern <= 0;
隊の先頭で実施していた次のサウンド出力処理を削除します。
// if (inv_ido != END_STATE) seq
// sound(extend(inv_pattern) + 4); // from 4 to 7
// endseq
その代わり、 全てのインベーダについて次の処理を追加します。
// 全てのインベーダについて処理
if (inv_stimer == 0) seq
sound(extend(inv_spattern) + 4); // from 4 to 7
inv_spattern <= inv_spattern + 1;
inv_stimer <= inv_init_stimer[inv_no];
endseq else seq
inv_stimer <= inv_stimer - 1;
endseq
これにより過去記事のような動作を行うはずです。⇒実施したところ、正しく動作したようです。
![]() |
27 |
BSVによるメモリダンプモジュールの設計 (10) |
![]() |
受信したデータは以下の図に示すように、一文字4bitのデータが連続する、VRAM内容を示すログデータです(右側を一部省略)。

VRAMデータ4bitの意味は以下のとおりです。
- bit3: バリケード(シールド)=非画像情報
- bit2: R=画像情報
- bit1: G=画像情報
- bit0: B=画像情報
従って、非画像情報を無視し、次のコードにより画像フォーマットであるPPMに変換します。
log2ppm.c
#include
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.ppm
生成されたファイルを画像処理ツールであるgimp2で開くと以下のように正常に受信されています。

以上で、ゲームのメモリダンプ機能がひとおおり完成しました。ゲームの状態を吸い出したのは、これをオートエンコーダによりCNNに認識させるのを目的としています。
![]() |
26 |
BSVによるメモリダンプモジュールの設計 (9) |
![]() |
出来上がったモジュールの図を図406.1に示します。

シリアルデータ出力はUART_TXに接続します。これによりFPGAボード(正確にはアダプタボードであるAES-ACC-U96-JTAGボード)上でUSBに変換され、ケーブルを経由してPCのPuttyで受信します。今回は230,400 bps、COMは6番だったので、以下のようにPuttyの受信パラメータ及びログの場所を設定します。


ゲームをスタートさせて、FPGAボード上のダンプスイッチを押すと、ゲームが一時停止し、FPGAボードからはPuttyに対して以下のようなデータを送信して来ます。右端はチェックサムですが、この程度の通信速度ではデータ化けはしていないようです。

![]() |
25 |
BSVによるメモリダンプモジュールの設計 (8) |
![]() |
システム構成図

ハンドシェークアルゴリズム
以下に処理のハンドシェークを示します。
- ボード上のスイッチが押されstart信号が出力される。
- メモリダンパはstartに基づき、sreqを出力し、GameFSMに停止を要求。
- GameFSMは停止要求の有無に関わらず、フレームの最後で60Hzの立ち上がりを待つ。その際にcwaitを出力。
- メモリダンパはcwait(=GameFSMの停止)に基づき、以下のメモリダンプ動作をアドレス分だけ繰り返し。
- selをTrueにしてバス権(アドレス権)を取得
- アドレスを出力
- データを取得
- アスキー化してシリアルデータとして出力
- メモリダンパは終了時にsreqをネゲート。
- GameFSMはsreqがネゲートされるのを待ち、cwaitをネゲートしフレーム先頭から再開。
ゲームFSM側のBSVコードの修正
修正したGameFSM.bsvのウエイトルーチンを示します。
GameFSM.bsv
// 時間待ち
function Stmt wait_timer(
UInt#(12) count
);
return (seq
testOut <= True;
repeat(pack(extend(count))) seq
await(tic == 0);
await(tic == 1 && sreq == 0);
endseq
testOut <= False;
endseq);
endfunction
元々のtest出力信号(wait時を示す)testOut信号をそのままcwait(ゲームFSMのwaitを示す)として使用します。
元々は、60Hzの立下りを
await(tic == 0);
このように待った後、立ち上がりに同期して
await(tic == 1);
このように、ウエイトをリリースする(ウエイトルーチンから抜ける)仕様でした。今回それに加えて、メモリダンプからの停止要求がリリースされていることを
await(tic == 1 && sreq == 0);
このようにAND条件で加えました。これにより、ウエイトしている状態は元々testOut(=cwait)として出力されていたため、それを用いてメモリダンプの開始信号としています。
GameFSMのインタフェースにメモリダンパからの停止要求信号sreqを加えます。
GameFSM.bsv
(* prefix="" *)
method Action sreqm(Bit#(1) in_sreq);
次にワイヤ定義を記述します。
GameFSM.bsv
Wire#(Bit#(1)) sreq <- mkWire;
最後にメソッド定義を示します。
GameFSM.bsv
method Action sreqm(Bit#(1) in_sreq);
sreq <= in_sreq;
endmethod
![]() |
24 |
BSVによるメモリダンプモジュールの設計 (7) |
![]() |
メモリダンプモジュールを組み込むにあたり、前回までテストベンチ(=最上位)であった階層をモジュール化します。外部インタフェースは図404.1のとおりです。

- start (入力): ボード上のスイッチであり、画像ダンプの起動スイッチです。
- sreq (出力): GameFSMに対して(デュアルポートメモリに対して)バス権を要求する信号です。
- cwait (入力): GameFSMが60Hzの同期待ち状態にある信号です。これはバス権を放棄している信号でもあるので、流用します。
- addr (出力): デュアルポートメモリアドレスです。Muxを介してデュアルポートメモリに接続します。
- data (入力): デュアルポートメモリからの4bitデータです。
- sel (出力): Muxの制御信号であり、Trueでデュアルポートメモリのアドレスがメモリダンプモジュール側であることを示します。
- read (出力): シリアルデータ出力です。
入力
(* prefix="" *) // method名を削除するため
method Action startm(Bool newstart);
(* prefix="" *)
method Action datam(Data newdata);
(* prefix="" *)
method Action cwaitm(Bool newcwait);
(* synthesize, always_enabled="startm, datam, cwaitm" *) // EN_xxxを削除するため
出力
method Bool sreqm();
method Addr_t addrm();
method Bool selm();
method Bit#(1) readm();
(* synthesize, always_ready="sreqm, addrm, selm, readm" *) // RDY_xxxを削除するため
入出力は上記のとおりメソッドで定義し、入力はmethod Action、出力はmethodで定義します。
![]() |
21 |
BSVによるメモリダンプモジュールの設計 (6) |
![]() |
![]() |
20 |
BSVによるメモリダンプモジュールの設計 (5) |
![]() |
RAMアドレスマルチプレクサの設計
VRAMアクセスするマスタに、FSM、CRTCに加えてメモリダンパが加わりました。しかしながらBRAMのポートが2つまでなので、FSM側のアドレスバスをシェアします。CRTCは常にアクセスしているのに比べて、メモリダンパは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
出力のハンドシェーク端子は不要であるため、
(* synthesize, always_ready = "outp" *)
を指定して削除しています。さらに、組み合わせ回路であるため、clock, resetを使用していないので、それらポートを削除するために、
(* no_default_clock, no_default_reset *)
を指定しています。また、入力ピン名が、メソッド名_変数名、例えばoutp_a等のように複雑になるのを防止するため、
(* prefix="" *)
を指定してメソッド名を消しています。
これを合成すると以下のようなVerilogになります。
mkMux.v
//
// Generated by Bluespec Compiler (build 38534dc)
//
// On Thu May 20 14:21:23 JST 2021
//
//
// Ports:
// Name I/O size props
// outp O 16
// sel I 1
// a I 16
// b I 16
//
// Combinational paths from inputs to outputs:
// (sel, a, b) -> outp
//
//
`ifdef BSV_ASSIGNMENT_DELAY
`else
`define BSV_ASSIGNMENT_DELAY
`endif
`ifdef BSV_POSITIVE_RESET
`define BSV_RESET_VALUE 1'b1
`define BSV_RESET_EDGE posedge
`else
`define BSV_RESET_VALUE 1'b0
`define BSV_RESET_EDGE negedge
`endif
module mkMux(sel,
a,
b,
outp);
// value method outp
input sel;
input [15 : 0] a;
input [15 : 0] b;
output [15 : 0] outp;
// signals for module outputs
wire [15 : 0] outp;
// value method outp
assign outp = sel ? b : a ;
endmodule // mkMux
わずか、 assign outp = sel ? b : a ;
という一行のverilogを得るためにいろいろと記述していますが、これはBSVの練習のためでもあります。
![]() |
13 |
BSVによるメモリダンプモジュールの設計 (4) |
![]() |
以下のスクリプトでVerilogシミュレーションを実行します。
$ bsc -verilog -u Tb.bsv
cp top-original.v top.v
emacs -nw top.v
// emacsでautomodeにより、top.vを生成
iverilog -y /usr/local/lib/Bluespec/Verilog/ top.v mkTb.v mkUart.v -o mkTb.exe
./mkTb.exe
gtkwave -A verilog.vcd

図はちょうど1行を送信したところで、横方向のxが256(xは255までだがチェックサム出力の際に256となる)から0に戻り、縦方向のyが0から1になった時点の波形です。データを0x33, 0x66, 0x0a, 0x30, 0x64と送信しています。
ストップビットを1ビットに削ったところ、1,582,090サイクルとなりました。1アスキーバイトあたり12サイクルなので、8bitの他、スタートが1bit、ストップが3bit相当となっています。送信時間は
- 115,200bpsでは6.9秒
- 230,400bpsでは3.4秒
- 460,800bpsでは1.7秒
- 921,600bpsでは0.9秒
となります。バイナリだとこれの半分の時間となるはずですが、デバッグの都合上アスキーコードの転送とします。
ページ: