![]() |
13 |
Pongの開発 (6) |
![]() |
Posts Tagged with "Design"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
10 |
Pongの開発 (5) |
![]() |
パドルコントロール
さて、Pongの実装で欲しくなるのがパドルコントロールのためのツマミです。一般には可変抵抗器で実装しているようです。幸いCmodA7にはアナログ入力があるので、可変抵抗器を接続すればよさそうです。
FPGAボード内のアナログ入力は以下のようになっています。外部の0~3.3Vの電圧を抵抗で分圧し、FPGAのADC入力は0~1Vの入力としています。入力インピーダンスが高いとは言えないので、外部回路の出力インピーダンスが高い場合問題になります。

可変抵抗器は出力インピーダンスが変化するので、設計が案外面倒です。最大と最小のみの2点だけを考えれば良いのかもしれませんが、ここではアナログ回路シミュレータであるLTSpiceを使用してみます。
まず、可変抵抗器のシミュレーションをする前に、出力インピーダンスが高い場合にどうなるかを見てみます。出力インピーダンスが3.3Kとした場合の回路とシミュレーション波形です。浮遊容量を少し付加しています。

サイン波形を入れていますが、DC特性を見る目的です。
波形から明らかなように、基準電圧$V_\text{in}$(ブルー)は3.3Vまで上昇しているにも関わらず、出力インピーダンスが後段の入力インピーダンスと同程度であるため、CmodA7ボードの入力電圧$V_\text{1}$(レッド)は期待の3.3Vまで上昇せず1.6V Maxとなり、分圧したADCの入力電圧$V_\text{out1}$(グリーン)は0.5V Maxと半分しか上がりません。
![]() |
9 |
Pongの開発 (4) |
![]() |
疑似乱数生成器
ChatGPTにLFSRのアルゴリズムを持つ疑似乱数生成器を記述してもらいました。以下の完成したモジュールはそれを手直ししたものです。
interface Randomizer_ifc;
method ActionValue#(Bit#(1)) random_01();
endinterface
//(* synthesize *) モジュールをインライン化するためコメントアウト
module mkRandomizer(Randomizer_ifc);
Reg#(Bit#(16)) lfsr <- mkReg(16'hACE1); // 適当な非ゼロの初期値
method ActionValue#(Bit#(1)) random_01();
Bit#(1) newBit = lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10];
lfsr <= {lfsr[14:0], newBit};
return lfsr[15];
endmethod
endmodule
ActionValueメソッドの呼び出し方
作成した疑似乱数生成器の呼び出しが少々難しかったのでまとめておきます。BSVにおいてはモジュールインタフェース内に記述されるメソッドの型は
- Value Method
- ActionValue Method
- Action Method
の3種類があります。それぞれ入力、入出力、出力ポートに対応しますが、ActionValueの呼び出し方に少々困難がありました。単純にメソッドを変数に入れることができないためです。
特にFSMを構成するseqブロック内で、あるレジスタwにValueメソッドの戻り値を代入するだけなら、
seq
:
w <= random_01();
:
endseq
等とすれば良いのですが、この場合のrandom関数は内部状態を持ち、それが呼び出しにより更新されるという副作用を持つため、ActionValueメソッドとして呼び出します。この呼び出し法が少々難しく、"<-"を用いてインスタンスした上で、かつ単純にseqの中で呼ぶことはできず、actionブロックを構成してその中でのみ有効な値となります。
実例を挙げると、
import Randomizer::*;
Randomizer_ifc randomizer <- mkRandomizer;
:
seq
action
:
Bit#(1) w <- randomizer.random_01();
:
endaction
endseq
のように、actionブロックの中で"<-"を用いて関数を呼び出します。特にactionブロックを構成することになかなか気づきませんでした。
追記:
この場合の変数wはaction - endactionのスコープ内でしか有効でないことが後日判明したので、それを回避する方法を追記します。action - endactionのスコープ内でコピー変数にコピーするだけです。
Reg#(Bit#(1)) ww <- mkRegU; // wをactionスコープの外で使用するコピー変数
:
seq
action
:
let w <- randomizer.random_01(); // Bit#(1)宣言でなくてletでも良い
ww <= w;
:
endaction
endseq
なお、関数がマルチサイクルの場合はこれでうまく行かない場合もあり、その場合はruleの中に入れます。
![]() |
8 |
Pongの開発 (3) |
![]() |
ブロック図
以下にPongのデモ画面が動作するステートマシンを組み込んだブロック図を示します。

これらのモジュールのうち、クロック系、デュアルポートRAMを含むグラフィクス系、1チャネルのサウンド系はほぼ流用です。新規設計はGameFSMのみであり、GameFSMとSoundFSMを連結するコマンドバッファもそのまま流用しています。
完成画面
システムが動作している画面を示します。

方向制御
パドルの縦位置はボールと同じにしてあるため、必ずボールは打ち返します。パドルでの反射は表692.1のとおり。
乱数1 | bcount | 乱数2 | dy |
---|---|---|---|
0 | 0 (45°) | 0 | (dx=1に対して)+1 |
1 | (dx=1に対して)-1 | ||
1 | 3 (18.4°) | 0 | (dx=3に対して)+1 |
1 | (dx=3に対して)-1 |
乱数1で傾きの逆数であるbcountを0または3とします。乱数2で方向がプラス+1かマイナスかを決定します。合わせると、bcount=0は右方向の場合速度ベクトルが(+1, +1)または(+1, -1)です。bcount=3の場合速度ベクトルが(+1, +1/3)または(+1, -1/3)です。
オリジナルゲームにはまっすぐ反射する反射もあったのですが、まっすぐ反射してもあまり面白くないので、カットしました。
上下の壁にボールが衝突するとy方向の速度dyの符号を反転させます。一方、左右のパドルに衝突するとx方向の速度であるdxの符号を反転させ、かつ上記の表により方向をランダムに変化させます。
![]() |
7 |
Pongの開発 (2) |
![]() |
サウンドROMデータの作成
以下のコマンドにより、Vivadoの読めるCOEファイルを作成します。
echo 'memory_initialization_radix=16;' > srom.coe
echo -n 'memory_initialization_vector=' >> srom.coe
cat s?o.wav | \od -An -t x1 -v >> srom.coe
echo ';' >> srom.coe
サウンドステートマシン
サウンドステートマシンは以前作成したものを流用します。Space Invadersの開発の際には4多重音のため、4個のサウンドステートマシンを使用しましたが、Pongは単純なので1個で十分です。そのため自分と他人のサウンドコードを見分ける必要がないため、キューへの書き込みを示す!emptyで起動します。またミキサーも無くなるため、従来後段のミキサーに入れていた符号拡張と桁調整を本モジュールに組み込みました。
以下にソースコードを示します。コメントは一部ChatGPTにより作成してもらいました。
// 波形ファイルを読み込み、オーディオDACにサウンドデータを出力するFSMの定義
import StmtFSM.*; // FSMを生成するためのユーティリティモジュールのインポート
// サウンドイベントと無音を表すマクロ定義
`define SOUND1 1 // 発射音
`define SOUND2 2 // パドルとの衝突音
`define SOUND3 3 // 壁との衝突音
`define SOUND4 4 // アウトの際の音
`define NULL 'h80 // 無音を表す値(8ビットPCMで中間値)
// 必要な型定義
typedef UInt#(13) Addr_t; // メモリアドレス用の13ビット符号なし整数
typedef UInt#(8) Data_t; // 8ビットデータ用の符号なし整数
typedef Bit#(16) Sound_t; // 16ビット符号付PCMサウンドデータ
typedef Bit#(3) Code_t; // サウンドコード
// FSMのインターフェース定義。外部からアクセスするためのメソッドが定義されています。
interface FSM_ifc;
method Action sound(Code_t code); // 音声コードを示す入力メソッド
method Action rom_data(Data_t indata); // ROMからのデータ入力メソッド
method Action sync(Bool lrclk); // 同期信号を処理するための入力メソッド
method Action empty(Bool flag); // FIFOが空を表す入力メソッド
method Addr_t rom_address(); // 現在のROMアドレスの出力メソッド
method Sound_t sdout(); // 音声出力データの出力メソッド
method Bool soundon(); // 音声が再生中かどうかを示す出力メソッド
method Bool fifo_ren(); // FIFOの読み出し要求の出力メソッド
endinterface
(* synthesize,always_ready,always_enabled *)
module mkSoundFSM(FSM_ifc);
// 内部ワイヤとレジスタの定義
Wire#(Code_t) code <- mkWire, // コードを格納するワイヤと現在のコードを保持するレジスタ
current <- mkRegU;
Wire#(Bool) lrclk <- mkWire; // 左右のクロック同期用のワイヤ
Reg#(Data_t) romdata <- mkRegU; // ROMから読み込まれたデータを保持するレジスタ
Reg#(Data_t) dout <- mkReg(`NULL); // データ出力用のレジスタ(初期値は無音)
Reg#(UInt#(32)) workd <- mkRegU; // 32ビット作業用データレジスタ
Reg#(UInt#(13)) dcount <- mkRegU; // 再生カウント用の13ビットレジスタ
Reg#(Addr_t) worka <- mkRegU, // アドレス計算用の作業用アドレスレジスタ
romaddr <- mkRegU, // ROMのアドレスレジスタ
addr <- mkRegU; // 出力用アドレスレジスタ
Reg#(UInt#(8)) ii <- mkReg(0); // ループカウンタ用の8ビットレジスタ
Reg#(Bool) son <- mkReg(False), // サウンド再生中フラグ用のレジスタ
sonEarly <- mkReg(False), // 早期サウンド開始フラグ用のレジスタ
ren <- mkReg(False), // FIFO読み込み要求フラグ用のレジスタ
emptyf <- mkReg(True); // FIFOが空かどうかを示すフラグ用のレジスタ
// subfunctions
// READ MEM サブ関数:メモリからの読み出し
// input: worka
// output: romdata;
//
function Stmt readmem;
return (seq
addr <= worka;
delay(2);
endseq);
endfunction
// READ COUNT サブ関数:カウント読み出し
// input: romaddr
// output: (romaddr,...,romaddr+3) => dcount;
// romaddr + 4 => romaddr;
//
function Stmt readcount;
return (seq
workd <= 0;
for (ii <= 0; ii <= 3; ii <= ii + 1) seq
worka <= romaddr + extend(3-ii);
readmem;
if (ii == 3) dcount <= truncate(workd<<8) | extend(romdata);
else workd <= workd<<8 | extend(romdata);
endseq
romaddr <= romaddr + 4;
endseq);
endfunction
// Mainloop メインループの定義
//
Stmt main = seq
while(True) seq
action
dout <= `NULL;
sonEarly <= False;
son <= False;
ren <= False;
endaction
await(!emptyf);
action
ren <= True; // consume 1 entry of Q
current <= code;
endaction
await(emptyf);
ren <= False;
// Sync to LRCLK
//
await(lrclk);
await(!lrclk);
delay(4);
// Format decoding
//
action
case (current)
`SOUND1: romaddr <= 0 + 16;
`SOUND2: romaddr <= 1610 + 16;
`SOUND3: romaddr <= (1610 + 900) + 16;
`SOUND4: romaddr <= (1610 + 900 + 872) +16;
endcase
endaction
readcount;
romaddr <= romaddr + extend(dcount) + 4;
readcount;
romaddr <= romaddr - 1;
// play loop
while (dcount != 0) seq
// Play 0
if (sonEarly == False) seq
// 1cycle目
readmem;
action
sonEarly <= True;
son <= False;
dout <= `NULL;
endaction
endseq else seq
// 2cycle目以降
readmem;
action
son <= True;
dout <= romdata;
endaction
endseq // if
delay(11);
action
romaddr <= romaddr + 1;
worka <= romaddr + 1;
dcount <= dcount - 1;
endaction
endseq // while(!終了条件)
endseq // while(True)
endseq; // Stmt
mkAutoFSM(main); // FSMを生成し実行
method Action sound(Code_t incode);
code <= incode;
endmethod
method Action rom_data(Data_t indata);
romdata <= indata;
endmethod
method Addr_t rom_address();
return addr;
endmethod
method Sound_t sdout();
let bdout = pack(dout); // 現在のオーディオデータ(dout)をパックし、16ビットのデータ(bdout)に変換します。
let s = ~bdout[7]; // 8ビット目(MSB)を反転させてサインビット(s)を生成します。
return {{s,s},bdout[6:0],{7'h0}}; // オーディオデータをsignedに変換します。
endmethod
method Bool soundon();
return son;
endmethod
method Action sync(Bool inlrclk);
lrclk <= inlrclk;
endmethod
method Bool fifo_ren();
return ren;
endmethod
method Action empty(Bool flag);
emptyf <= flag;
endmethod
endmodule: mkSoundFSM
![]() |
6 |
Pongの開発 |
![]() |
Pong Game
強化学習のトライアルとして、ビデオゲームを題材に取り上げます。本来はインベーダーゲームを対象としたいのですが複雑であるため、比較的単純なPongとします。
Youtubeで動作画像を探すとこのような動画が見つかりました。

これを参考にしつつ、bsvによりプログラミングを行います。
Sound
強化学習本来の目的ではサウンドは不要ですが、ゲームとしての完成度のためにサウンドも実装します。サウンドコントローラであるSoundFSMやその周りの回路は開発済みなので、サウンド実装の工数はサウンド収集加工以外にはほとんどかかりません。そこでサウンドの収集から始めます。
サウンドフォーマット等の参考にする過去記事はこれです。
まずWindows+Gによりゲームバーを呼び出し上記動画の動画をキャプチャします。次にffmpegによりwaveに変換します。変換コマンドは次のとおりです。audacityはmp4を読み込めないため、ffmpegを用います。
$ ffmpeg -i input.mp4 output.wav
これをaudacityにより編集し以下の4つのサウンドを取得します。
コード | 種類 |
---|---|
1 | 発射音 |
2 | パドル |
3 | 壁 |
4 | アウト |
ffmpegを用いるとINFO等の余分な情報が削除できずハードウエアが読み込めません。よってaudacityでサウンドデータを開き、
- 音量をノーマライズ。イフェクト⇒音量⇒ノーマライズとします。
- メタデータを全て削除
- 形式はwav (microsoft)
- チャンネルはモノラル
- サンプリングは11025 Hz
- エンコーディングはUnsigned 8-bit PCM
としてエクスポートします。
ここで各ファイルサイズを見ると、
$ ls -l s?o.wav
-rwxrwx--- 1 root vboxsf 1610 10月 29 21:10 s1o.wav
-rwxrwx--- 1 root vboxsf 900 10月 29 21:10 s2o.wav
-rwxrwx--- 1 root vboxsf 872 10月 29 21:13 s3o.wav
-rwxrwx--- 1 root vboxsf 4388 10月 29 21:11 s4o.wav
以上からROMの構成表を作成すれば、
Code | Sound | Start | Size [bytes] | Entry=Start+16 |
---|---|---|---|---|
1 | 発射音 | 0 | 1,610 | 0+16 |
2 | パドル | 1,610 | 900 | 1,610+16 |
3 | 壁 | 1,610+900 | 872 | (1,610+900)+16 |
4 | アウト | 1,610+900+872 | 4,388 | (1,610+900+872)+16 |
合計 [bytes] (16KB ROM使用率) | 7,770 (95%) |
// Format decoding
//
action
case (current)
`SOUND1: romaddr <= 0 + 16;
`SOUND2: romaddr <= 1610 + 16;
`SOUND3: romaddr <= (1610 + 900) + 16;
`SOUND4: romaddr <= (1610 + 900 + 872) +16;
endcase
endaction
![]() |
26 |
Cmod A7の利用 (3) |
![]() |
完成した回路図を図686.1に、レイアウト図を686.2に示します。ボードは仕様的に2層、80cm^2未満であるため、Eagleの無料版で設計することができました。


基板業者JLCPCBにおいて基板製造及び部品実装(PCBA)をオーダーしようと思いました。ところがSMT部品でないとアセンブリできないらしく、今回SMT部品が無いことから基板のみのサービスを利用しました。
内容 | 費用[USD] |
---|---|
基板製造費10枚 | 5.00 |
配送費(OCS) | 1.98 |
合計 | 6.98 |
という結果でした。
![]() |
25 |
Cmod A7の利用 (2) |
![]() |
Eagleライブラリの作成
同様にDC Jackを登録します。以下にその基盤図及び実体図を示します。基盤穴をグレーで塗っています。

EAGLEでこれを登録するには基盤図をfootprintに入力する必要があり、ライブラリを次のように作成します。
(1) symbolの編集画面において、
- 部品外形をLayer=94 Symbolsで記述。
- 適宜論理ピンを設定。
- textでLayer=95 Namesとして">NAME"を追加。
- textでLayer=96 Valuesとして">VALUE"を追加。
(2) footprintの編集画面において、
- textでLayer=25 tNamesとして">NAME"を追加。
- textでLayer=27 tValuesとして">VALUE"を追加。
- Layer=20 Dimensionとする。
- Widthを0とする。
- Gridをmmにする。
- ラインコマンドをクリックする。
- 入力窓に、"(x, y)"と点を打つ。
以下"(x, y)"の繰り返しで図形を描きます。"20 Dimension"は外形線ですが、外形線の囲む内部に外形線があれば、囲まれた領域が穴になるという仕様です。ただし、上図は背面から見た図なので、上面から見た図はその裏返しとなります。
さらに、表面から背面に貫通する端子を背面で+5Vと接続させるため、背面のソルダーレジストを禁止とします。そのためには
- 禁止したい領域をLayer=30 bStopとして矩形で囲む。
(3) deviceの編集画面において、
- Edit ⇒ Addで作成したsymbolの読み込み
- Edit ⇒ Packageで作成したfootprintの読み込み
- 論理ピンと物理ピン(pad)の接続
- prefixをクリックしてJと入力 (J1, J2, としたい場合)
特にここで挙げたsymbol画面とfootprint画面からのdevice画面への読み込みが直感的ではないので注意してください。
![]() |
24 |
Cmod A7の利用 |
![]() |
Cmod A7
Cmod A7とはDigilentから販売されているArtix 7シリーズのFPGAの評価ボードで、その特長は小さくて安いことにあります。最近の世界的なインフレにより各種FPGAボードが値上がりしている中で、USD 99と最安の部類に入ります。

小さくて安いFPGAボードですが、Arty A7-35ボードと同じFPGAを採用しているため、Space Invadersが動作するはずです。
CmodA7toPMOD
しかしながらArty A7ボードがPMODインタフェースを4個搭載しておりそのままPMOD-VGAやPMOD-Audioボードを接続できるのに対し、このボードはPMODが1個しかないため、PMODインタフェースを設計する必要があります。ただし、Ultra96と異なり端子電圧が3.3VであるためPMODと直接インタフェースでき、レベル変換ICが不要です。
そのためには変換ボード上に
- Cmod A7ボード
- PMODピンソケットコネクタ x 4
- ACアダプタコネクタ(MJ-179PH)
- 5V-3.3V電圧降下コンバータ(NJU7223F33)
等の部品を搭載する必要があります。
Eagleライブラリの作成
EAGLEでこれらを使用するにはシンボル図や基盤図をライブラリに登録する必要があります。例として電圧降下コンバータNJU7223F33の外形図を図684.2に示します。

このシンボルとフットプリントを作成した図を以下に示します。


![]() |
23 |
色変換回路 (5) |
![]() |
BSVフォーラムで議論したところ、以下のような回答を順次頂きました。
- 最初の一括でリターンを返すメソッドのほうが良い。が、メソッドの構造体を分割する手段は現状のBSVの文法にはない。従って手段としては2つあり、外側にverilogラッパーを設けて分割するか、BSVに新たな機能を追加するか。
- 組み合わせ回路の関数を置いたらどうか?
- clockとresetは、インタフェース外の端子を使えば消すことができる。
- RDYが1にもかかわらずエラーになる件はbscのissueとして登録した。
組み合わせ回路の関数は良い方法だと思うので、今後色変換回路を組み込む場合にはそうしようと思います。
しかしながら今回は外付けのモジュールとしたため、clockとresetを消去する方法で行きたいと思います。提示されたソースは以下のとおり。
interface ColorConverter;
(* result="RO" *)
method Bit#(4) getRO();
(* result="GO" *)
method Bit#(4) getGO();
(* result="BO" *)
method Bit#(4) getBO();
endinterface
(* synthesize, always_ready, no_default_clock, no_default_reset *)
module mkColorConverter(
(* port="RI" *) Bit#(1) r,
(* port="GI" *) Bit#(1) g,
(* port="BI" *) Bit#(1) b,
ColorConverter ifc);
method Bit#(4) getRO();
return (case ({r, g, b})
3'b000: 4'h0;
3'b001: 4'h9;
3'b010: 4'hd;
3'b011: 4'h5;
3'b100: 4'hc;
default: 4'h0;
endcase);
endmethod
method Bit#(4) getGO();
return (case ({r, g, b})
3'b000: 4'h0;
3'b001: 4'h4;
3'b010: 4'h8;
3'b011: 4'hb;
3'b100: 4'hc;
default: 4'h0;
endcase);
endmethod
method Bit#(4) getBO();
return (case ({r, g, b})
3'b000: 4'h0;
3'b001: 4'h1;
3'b010: 4'h4;
3'b011: 4'h5;
3'b100: 4'hC;
default: 4'h0;
endcase);
endmethod
endmodule
メソッドを用いない入力方法があるようです。具体的にはinterfaceはメソッドを並べるため、interface定義外にポートを定義しています。
また、カラー0は非表示画面も示すため、変換せずに0のままと変更しました。
このソースから生成されるverilogコードはヘッダや定義文を除いて次のとおり。
module mkColorConverter(RI,
GI,
BI,
RO,
GO,
BO);
input RI;
input GI;
input BI;
// value method getRO
output [3 : 0] RO;
// value method getGO
output [3 : 0] GO;
// value method getBO
output [3 : 0] BO;
// signals for module outputs
reg [3 : 0] BO, GO, RO;
// remaining internal signals
wire [2 : 0] x__h151;
// value method getRO
always@(x__h151)
begin
case (x__h151)
3'b0: RO = 4'h0;
3'b001: RO = 4'h9;
3'b010: RO = 4'hD;
3'b011: RO = 4'h5;
3'b100: RO = 4'hC;
default: RO = 4'h0;
endcase
end
// value method getGO
always@(x__h151)
begin
case (x__h151)
3'b0: GO = 4'h0;
3'b001: GO = 4'h4;
3'b010: GO = 4'h8;
3'b011: GO = 4'hB;
3'b100: GO = 4'hC;
default: GO = 4'h0;
endcase
end
// value method getBO
always@(x__h151)
begin
case (x__h151)
3'b0: BO = 4'h0;
3'b001: BO = 4'h1;
3'b010: BO = 4'h4;
3'b011: BO = 4'h5;
3'b100: BO = 4'hC;
default: BO = 4'h0;
endcase
end
// remaining internal signals
assign x__h151 = { RI, GI, BI } ;
endmodule // mkColorConverter
verilogは正しく生成されています。
ページ: