NES on FPGA 2024/01/04

NES on FPGA feat. TangNano series

Gowin社 のFPGAを搭載した Sipeed社 の Tang Nano シリーズに NES on FPGA を移植したメモ。

目標は、基本的な『スーパーマリオブラザーズ』から、 ROMサイズの大きい『星のカービィ』『沙羅曼蛇』も動かしたい。

そしてNESの次は…!

Tang Nano シリーズ

Sipeed から販売されている Gowin のFPGAを使用したボードたち。 なお Tang Nano 1K についてはLUT数が少なくNES入らないので割愛。

今回はWindows 10 22H2 にて、Gowin EDA FPGA Designer ver. 1.9.9 (64-bit) を使用。

NES on FPGA 見積もり

Gowin EDA での合成結果では、約3,600LUTsとなった。 また必要なBlockSRAMは、WorkRAMが2kByteとVRAMが2kByteに加え+αでラインバッファ等がある。 ソフトはPRG-ROM(プログラム)とCHR-ROM(キャラクタ)からなり、 これをどうやってメモリに配置するか、 そしてCPUからのPRG-ROMアクセスと、PPUからのCHR-ROMアクセスをどう捌くかが実装のカギとなる。 ちなみに、PRG-ROMアクセスは約560ns間隔で、CHR-ROMアクセスは約372ns間隔で発生するので、 メモリアクセスはこの時間の間に収めなければならない。

NES on FPGA feat. TangNano4K

価格約2,000円
デバイスLittleBee GW1NSR-LV4CQN48PC6/I5 (55nm LP embedded flash)
リソース4,608 LUTs、BlockSRAM 約22kByte
メモリHyperRAM 8MByte
参考 Sipeed wiki Tang Nano 4K

NES on FPGA feat. TangNano4K

使用率93%でMapper 0のみ実装して、マリオやレッキングクルーをUSB-JTAG経由で送信して動かせました。4KでNESは誰もやってなかったので移植してみたけど、LED1個でデバグつら。 pic.twitter.com/2aKNpUjpHM

— かんな丸⁧!!⁨ (@pgate1) 2024年1月4日

メモリマッピング

PRG-ROMとCHR-ROMをHyperRAMに共存させようとしても、 HyperRAMのリードアクセス時間が長いため不可。 このため、PRG-ROMのみHyperRAMに配置し、CHR-ROMは8kByteのBlockSRAMを使用する。 よって動かせるソフトはCHR-ROM(もしくはCHR-RAM)が8kByteまでのものに限られる。

BlockSRAM推論

要件としては、1クロックでWriteできることと、 Readした次のクロックおよび永続的にデータを参照できること。 SFL+の記述は従来通りで変更はなく、ここから生成される VerilogHDLでSDPB(Semi Dual Port Block SRAM)に推論される。
ram_8x8k.sflp
circuit ram_8x8k
{
	input adrs<13>, din<8>;
	instrin write(adrs, din);
	instrin read(adrs);
	output dout<8>;

	mem ram[8192]<8>;

	reg dout_reg<8>; // 出力レジスタ使用
	if(write) ram[adrs] := din;
	if(read) dout_reg := ram[adrs];
	dout = dout_reg;
}

HyperRAM

Gowin EDAのHyperRAM IPで最短のバースト長16で使用しても、 リードアクセス時間は686nsとなり遅すぎて使えない。 そこで、バースト無しメモリアクセスコントローラを使用する。 以下のPSRAM用コントローラを少し書き換えて使用させて頂いた。

参考: psram-tang-nano-9k

これならリードアクセスが282nsで間に合う。 ちなみにGowinのIPは 643 LUTs、今回のコントローラは 118 LUTs で済むのも嬉しい。

最初、HyperRAMが動かなくて困っていたけど、 サンプル通りのポート宣言に修正したらポートが正常に割り当てられて動いた。

NES_top.v
    // OK こちらで動作した
    output wire [0:0] O_hpram_ck,
    output wire [0:0] O_hpram_ck_n,
    output wire [0:0] O_hpram_cs_n,
    output wire [0:0] O_hpram_reset_n,
    inout  wire [7:0] IO_hpram_dq,
    inout  wire [0:0] IO_hpram_rwds
    /*
    // NG ピンが正常に割り当てられない
    output wire O_hpram_ck,
    output wire O_hpram_ck_n,
    output wire O_hpram_cs_n,
    output wire O_hpram_reset_n,
    inout  wire [7:0] IO_hpram_dq,
    inout  wire IO_hpram_rwds
    */

USB-JTAG UART

ソフトROMをFPGA内メモリに配置するには、USB-JTAG経由でのUARTを使用するのが手っ取り早い。 USB-UARTには以下を参考にデータ受信UARTモジュールを作成した。

参考: Tang-Nano-4Kとパソコンとの、JTAG経由の直接通信

UARTにはJTAGピンをユーザピンとして使用している。 このため、FPGAデータを書き込むと Gowin Programmer からは見えなくなるが、 電源を入れなおせば再度FPGA内にデフォルトデータが書き込まれるので問題ない。 ただしデバグの度にUSB電源とHDMIケーブルを抜き差しするのが非常に面倒だった。

注意点として、FPGAデータは SRAM Program を使用すること。 Flash Program で書き込んでしまうと電源を入れなおしてもそれがFPGAに書き込まれてしまうため Gowin Programmer から認識されなくなってしまう。 対処法としては、JTAGSEL(ピン8)をGNDに接続した状態で、JTAGピンをユーザIOとして使用していないデザインを書き込むといいらしい。

PCからはFTDIドライバを使用してFT_Writeでデータを送信する。 データ送信が速いとHyperRAMへのWiteが間に合わなかったので、1バイトずつFT_Writeしている。 最大65535Byteまで送信できるので、マリオの約40kByteは問題なし。

ftdi_send.cpp
	// ポートオープン
	FT_HANDLE ftHandle;
	FT_OpenEx((PVOID)"USB Debugger A", FT_OPEN_BY_DESCRIPTION, &ftHandle);

	int send_size = 16 + 32768 + 8192 + 2 + 3;

	// Sendサイズセット
	uint8 init[3];
	init[0] = 0x31; // Writeコマンド
	init[1] = (uint8)send_size;
	init[2] = (uint8)(send_size >> 8);
	uint32 write_size;
	FT_Write(ftHandle, init, 3, &write_size); // Writeサイズセット(初期化)

	// ROMのロード
	FILE *fp = fopen("mario.nes", "rb");
	uint8 *rom = new uint8[16+32768+8192];
	fread(rom, 1, 16+32768+8192, fp);
	fclose(fp);

	uint8 begin[1];
	begin[0] = 0x00; // Mapper 0
	FT_Write(ftHandle, begin, 1, &write_size);
	FT_Write(ftHandle, begin, 1, &write_size);
	// ヘッダー send
	for(i=0; i<16; i++){
		FT_Write(ftHandle, rom+i, 1, &write_size);
	}
	// PRG-ROM send
	for(i=16; i<16+32768; i++){
		FT_Write(ftHandle, rom+i, 1, &write_size);
	}
	// CHR-ROM send
	for(i=16+32768; i<16+32768+8192; i++){
		FT_Write(ftHandle, rom+i, 1, &write_size);
	}
	uint8 end[1];
	FT_Write(ftHandle, end, 1, &write_size);
	FT_Write(ftHandle, end, 1, &write_size);
	FT_Write(ftHandle, end, 1, &write_size);

	delete[] rom;
	FT_Close(ftHandle);

DVI出力

Gowin EDA で生成できる DVI_TX のIPを使用する。

最初、NES解像度を倍にした 512 x 448 で表示させてみたところ、 HDMI-USBキャプチャではスケーリング表示できたが、例のモニタでは表示できなかった。 他のモニタでも確実に表示されるよう、 640 x 480 で出力し 512 x 448 はスケーリングせずに中央に描画させた。

サウンド出力

一応APUもFPGAに入るかの確認のため、サウンド出力は1bitΔΣを通してLEDに接続してる。 IOピンに接続するとノイズになってしまうので、今のところLEDに接続。 音がモヨモヨ光る様子が見て取れる。

クロック

NESの実装見積もりで Fmax 30MHz 程度だったことも考慮し、 DVI_TXクロックを 124.875 MHz とし、NESコア用クロックは5分の1の 24.975 MHz とした。 HyperRAMクロックはNESコアの2倍の 49.95 MHz を使用。

NESコアは 25 MHz もしくは 50 MHz をベースに実装しているので、 クロックを 25.2 MHz にした場合、PPUの描画がHsyncに間に合わず、画面が下に流れてしまう現象が発生してしまう。 よって今回は、25 MHz 以下で設定する必要があった。

動作確認

実装はMapper0のみ、『スパーマリオブラザーズ』『レッキングクルー』等が動作した。

LED1個でデバグするのは非常につらいので、Gowin Analizer Oscilloscopeを活用した。

レポート

リソース使用率4264/4608 LUTs (93%)
BlockSRAM使用率9/10 (90%)
NESコアFmax30.7 MHz
論理合成・配置配線時間40 sec
FPGAデータ・コード Tang Nano 4K 用FPGAデータ(GitHub)

試しにいくつかのMapperを有効化して合成してみたところ、 LUT使用率ほぼ100%になった。 それでも合成・配置配線時間は変わらず、Fmaxも達成している。 Gowin EDA やるじゃん。

NES on FPGA feat. TangNano9K

価格約2,300円
デバイスLittleBee GW1NR-LV9QN88PC6/I5 (55nm LP embedded flash)
リソース8640 LUTs、BlockSRAM 約58kByte
メモリPSRAM 64Mbit (32Mbit 2ch)
参考 Sipeed wiki Tang Nano 9K

NES on FPGA feat. TangNano9K

PSRAMが2つ使えてそれぞれPRG-ROM・CHR-ROMとして実装できるので移植難易度は低いですね。使用率は61%程度なのでMapperも複数実装できる。容量の大きいマリオ3や沙羅曼蛇も難なく動きました。 pic.twitter.com/8AEa3zZ0UF

— かんな丸⁧!!⁨ (@pgate1) 2024年1月6日

メモリマッピング

PSRAMを2ch使えるので、それぞれPRG-ROMとCHR-ROMとして使用する。 一番楽かもしれない。

PSRAM

バースト無しアクセスを使用したいので、 以下のPSRAM用コントローラを使用させて頂いた。

参考: psram-tang-nano-9k

microSDカード

いわゆるTFカードスロットが付いているので、ROMはここから読み込む。 回路図を見るに、配線はSPIモードの接続のみ。 他FPGAプロジェクトから、SDカードSPIアクセスモジュールとFAT16モジュールを流用する。 FAT16なので、microSDカードは最大2GBまでのものをたくさん用意した。

サウンド出力

HDMI Audio を使えばHDMIから出力できそうで、そのためのLUTも余裕はあるけど、 今のところちょっと難しそうなのでパス。 1bitΔΣを通してLEDに接続してる。

クロック

NESの実装見積もりで Fmax 30MHz 程度だったことも考慮し、 DVI_TXクロック 124.875 MHz、NESコアクロックは5分の1の 24.975 MHz とした。 PSRAMクロックはNESコアの倍の 49.95 MHz とした。

入力クロック 27 MHz から、49.5 MHz および 24.75 MHz を作るより、 まずDVI用のクロックを作って割った方が 25 MHz に近いのはたまたまなのか、 それとも高クロックからクロックツリーを設計すべきなのか。

また、CHR-ROMリードが間に合ってなかったので、 1クロック早くリードリクエストを出せるようにちょっと修正。

動作確認

実装はMapper0,1,2,3,4,73 を実装してみた。 『スパーマリオブラザーズ3』『星のカービィ』『沙羅曼蛇』等が動作した。

レポート

リソース使用率5190/8640 LUTs (61%)
BlockSRAM使用率10/26 (39%)
NESコアFmax34.7 MHz
論理合成・配置配線時間50 sec
FPGAデータ・コード Tang Nano 9K 用FPGAデータ(GitHub)

DVI出力は Tang Nano 4K 同様の実装とした。

NES on FPGA feat. TangNano20K

価格約4,000円
デバイスArora GW2AR-LV18QN88C8/I7 (55nm LP SRAM)
リソース20,736 LUTs、BlockSRAM 約103kByte
メモリSDR SDRAM 8MByte
参考 Sipeed wiki Tang Nano 20K

NES on FPGA feat. TangNano20K

1chipMSXに移植した時と同様に、SDRAM1個をPRG-ROMとCHR-ROMで共有してます。9Kよりも動作周波数上がって、NESコアは約50MHzでいけた。 pic.twitter.com/KKi8Dzhvzm

— かんな丸⁧!!⁨ (@pgate1) 2024年1月7日

メモリマッピング

SDRAMが1個なので、PRG-ROMとCHR-ROMを共存させる。 CPUとPPUからのメモリアクセスアービトレーションは必要。

SDRAM

他のFPGAプロジェクトから、バースト無しSDRAMコントローラを流用する。 もちろんPRG-ROMアクセスとCHR-ROMアクセスをアービトレーションする仕組みは必要だけど、 NES on FPGA feat. 1chipMSX での実装と同様の構成で動いた。

クロック

NESの実装見積もりで Fmax 70MHz いけたので、 DVI_TXクロック 124.875 MHz を 5分の2 してNESコアクロックは 49.95 MHz とした。 SDRAMクロックもNESコアと同じ 49.95 MHz を使用。

動作確認

実装はMapper0,1,2,3,4,73 を実装してみた。 『スパーマリオブラザーズ3』『星のカービィ』『沙羅曼蛇』等が動作した。 他のマッパーも余裕で追加できる程度にはリソースが余っている。

レポート

リソース使用率5240/20736 LUTs (26%)
BlockSRAM使用率10/46 (22%)
NESコアFmax68.1 MHz
論理合成・配置配線時間45 sec
FPGAデータ・コード Tang Nano 20K 用FPGAデータ(GitHub)

microSDカードアクセスとDVI出力とサウンド出力は Tang Nano 9K 同様の実装とした。

ちなみにTFスロット(microSDスロット)はSDモードが使用できるよう配線接続されているので、 そのうちSDモードアクセスモジュールに切り替えてROMの読み込み時間を半分にしたい (NESの場合は数十kByteなので読み込み一瞬だけど、SNESだと数MByteなので読み込みに少し時間がかかる)。 あとはmicroSDHCカードとFAT32のサポートに移行したい。

2024/02/18 追記: microSDHCコントローラとFAT32モジュールを実装し移行しました。

SNES on FPGA feat. TangNano20K

Tang Nano 20K はファミコン実装には贅沢なので、ついでにスーファミも実装してみた。 LUTが足りないためサウンドDSPはオミット。

SNES on FPGA feat. TangNano20K(No sound)

LUTが足りなくサウンドDSPを除外して使用率73%で実装しました。でもサウンドメモリは必要なのでBlockSRAMを使ってます。本当はSDRAM1個でWorkRAMと共存させたいけどアクセス速度がちょっと厳しくて無理だった。 pic.twitter.com/3IRMiSfHXs

— かんな丸⁧!!⁨ (@pgate1) 2024年1月8日

メモリマッピング

VRAM 64kByte は速度が要求されるためBlockSRAMで実装。

その他のCPUからアクセスするROMが最大 4MByte、WorkRAM 128kByte、SaveRAM 256kByteはSDRAMを共用する。 ただし、ROMからWorkRAMへのDMAは通常リードとライトが並列に動作するが、 ここでは同じSDRAM内のため並列動作は解除して順次動作としている。

また、SoundRAM 64kByteについてもSDRAMに共存させるつもりだったが、 CPUメモリアクセスとサウンドプロセッサメモリアクセスをアービトレーションしてもアクセスタイムオーバーのため共存は諦めた。 しかし、SNESのCPUはサウンドプロセッサと協調して動作するので、SoundRAMの実装は必須。 結果的には、SoundRAMは4分の1の 16kByte でBlockRAMとして実装した。 観測した限りではあるけど、サウンドドライバはSoundRAMの低アドレス領域(0x0000~0x3FFF)に配置しているケースが多かったので動いている模様。 サウンドDSPをオミットして音を出さないのであれば、後半(0x4000~0xFFFF)の波形メモリ領域が不要であり、このようなトリッキーな実装が可能だった。 なお、8kByteだとバハムートラグーンやクロノトリガーが動かず、 32kByteだとBlockSRAMが足りない。

2024/03/28 追記: アービトレーション回路を修正し、 SDRAMクロックを 99.9 MHz に引き上げ、 配置配線のオプションをタイミング優先にして SoundRAMもSDRAMで共存させることができた。 一応『クロノトリガー』や『バハムートラグーン』のOPデモをパス。 しかしやはりサウンドDSPが入らず音は出ない。 それに、影響ない部分の回路を変更すると動かなくなるし、 そもそもアクセスタイムぎりぎりで動かすためCDCケアしてないので この実装は実験的なものに終わった。

クロック

SNESコア実装の見積もりでFmaxが 40MHz 程度だった(速度最適化オプションありだと 46 MHz)ので、 DVI出力用に 124.875 MHz を、SNESコア用にその5分の1の 24.975 MHz を生成した。 SDRAMはSNESコアの2倍の 49.95 MHz を使用する。

できるだけSDRAMアクセス時間を短くしたいということもあり、 従来SDRAMリフレッシュはオートランさせていたが、 今回はSNESのDRAMリフレッシュタイミング中にリフレッシュを行うよう修正。 動作としてはこちらの方が実機に近いかな。

動作確認

『ロックマンX』や『FinalFantasyV』等が動作。 『バハムートラグーン』や『クロノトリガー』のオープニングデモをパスした。 セーブデータも読み込めることを確認。

SNES on FPGA feat. TangNano20K(No sound)

大好物なバハムートラグーンのOPデモもパスしました。どろどろ愛憎劇でリメイク待ってる。 pic.twitter.com/pBAiZa2ZrG

— かんな丸⁧!!⁨ (@pgate1) 2024年1月8日

SNES on FPGA feat. TangNano20K(No sound)

セーブデータも読めるようにした pic.twitter.com/jXk2MJ4XYR

— かんな丸⁧!!⁨ (@pgate1) 2024年1月9日

レポート

リソース使用率14960/20736 LUTs (73%)
BlockSRAM使用率46/46 (100%)
SNESコアFmax33.4 MHz
論理合成・配置配線時間150 sec
FPGAデータ・コード Tang Nano 20K 用FPGAデータ(GitHub)

非同期リセットか同期リセットか

FPGAベンダが出しているHDLコーディングガイド等で、 非同期リセットか同期リセットのどちらかが推奨されていたりする。 GowinEDA はどうかな?と思いドキュメントを探してみたけど見つけられず、 実際に SNES on FPGA で比較してみた。 sfl2vlの -sync_res オプションで容易に非同期・同期を切り替えられる。

以下の表は CPU,PPU,DMA 回路をリセットを変更して比較したもの。 なぜか同期リセットの方がLUT少し減ったような気もするけど、 どちらでもあまり変わりなさそう。

リセットタイプ 論理合成・配置配線時間 使用リソース Fmax
非同期リセット150 sec9237 LUTs, 4145 regs33.4 MHz
同期リセット160 sec9140 LUTs, 4150 regs35.1 MHz

おわりに

Gowin EDA については、 ファイルパスは絶対パスじゃなく相対パスで管理して欲しい。 あとFPGAデータ生成完了したら音を出して欲しい。 シンセシス途中で止めたいことがあるけど止まらない。 クリティカルパスってどうやったら見られるんだろう。

簡単なテストHDLでも、ボードによって動いたり動かなかったりするのは品質にばらつきがあるのかな? Tang Nano 60K の発売待ってますよ!Sipeedさん。

移植について

Tang Nano シリーズへの NES on FPGA の移植を通じで、その移植性、 つまりSFLから生成されたVerilogHDLの移植性の高さ、 もしくは Gowin Synthesis の十分な性能を感じることができた。 ゲーミングFPGAボードと言っていいのか分からないけど、 とりあえず SNES on FPGA も動いたのでまんぞく。

NESコアとSNESコアは、どちらもクロック1つの完全同期設計であることと、 CPUがマイクロプログラムベースではないためBlockSRAMを使わないで済むことも 移植が容易だった要因だろう。

HDMI-USBキャプチャ

DVI出力をPC画面に表示できるので、デバグするのに非常に役に立った。 DVI_DE信号を見ているようで、通常のモニタではサポートしていない低い解像度でも表示してくれて、 2,500円程度でコスパが良い。 買うならUSB3.0 1080p@60HzをサポートしているMS2130チップを搭載しているものがお勧め。

2,500円の中華製HDMI-USBキャプチャなかなか良くて小型モニタいらないね。TangNano4Kのカメラデモ普通に映ってるし。MS2130っていうチップ使ってるらしいhttps://t.co/3lgboU3VvK pic.twitter.com/YpjplBOXou

— かんな丸⁧!!⁨ (@pgate1) 2023年11月7日

©2024 pgate1