| NES on FPGA | 2025/04/28 Post | 
目標は、基本的な『スーパーマリオブラザーズ』から、 ROMサイズの大きい『星のカービィ』『沙羅曼蛇』も動かしたい。
そしてNESの次は…!
Sipeed から販売されている Gowin のFPGAを使用したボードたち。
なお Tang Nano 1K についてはLUT数が少なくNES入らないので割愛。
今回はWindows 10 22H2 にて、Gowin EDA FPGA Designer ver. 1.9.9 (64-bit) を使用。
| 価格 | 約2,000円(2023年8月時点) | 
| デバイス | 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
— かんな丸!! (@pgate1) 2024年1月4日
使用率93%でMapper 0のみ実装して、マリオやレッキングクルーをUSB-JTAG経由で送信して動かせました。4KでNESは誰もやってなかったので移植してみたけど、LED1個でデバグつら。 pic.twitter.com/2aKNpUjpHM
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が動かなくて困っていたけど、 サンプル通りのポート宣言に修正したらポートが正常に割り当てられて動いた。
NES_top.v
// 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
// 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
参考: 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);
最初、NES解像度を倍にした 512 x 448 で表示させてみたところ、 HDMI-USBキャプチャではスケーリング表示できたが、例のモニタでは表示できなかった。 他のモニタでも確実に表示されるよう、 640 x 480 で出力し 512 x 448 はスケーリングせずに中央に描画させた。
NESコアは 25 MHz もしくは 50 MHz をベースに実装しているので、 クロックを 25.2 MHz にした場合、PPUの描画がHsyncに間に合わず、画面が下に流れてしまう現象が発生してしまう。 よって今回は、25 MHz 以下で設定する必要があった。
LED1個でデバグするのは非常につらいので、Gowin Analizer Oscilloscopeを活用した。
| リソース使用率 | 4,264 / 4,608 LUTs (93%) | 
| BlockSRAM使用率 | 9 / 10 (90%) | 
| NESコアFmax | 30.7 MHz | 
| 論理合成・配置配線時間 | 40 sec | 
| FPGAデータ・コード | Tang Nano 4K 用FPGAデータ(GitHub) | 
試しにいくつかのMapperを有効化して合成してみたところ、 LUT使用率ほぼ100%になった。 それでも合成・配置配線時間は変わらず、Fmaxも達成している。 Gowin EDA やるじゃん。
| 価格 | 約2,300円(2023年8月時点) | 
| デバイス | LittleBee GW1NR-LV9QN88PC6/I5 (55nm LP embedded flash) | 
| リソース | 8,640 LUTs、BlockSRAM 約58kByte | 
| メモリ | PSRAM 64Mbit (32Mbit 2ch) | 
| 参考 | Sipeed wiki Tang Nano 9K | 
NES on FPGA feat. TangNano9K
— かんな丸!! (@pgate1) 2024年1月6日
PSRAMが2つ使えてそれぞれPRG-ROM・CHR-ROMとして実装できるので移植難易度は低いですね。使用率は61%程度なのでMapperも複数実装できる。容量の大きいマリオ3や沙羅曼蛇も難なく動きました。 pic.twitter.com/8AEa3zZ0UF
参考: hdl-util / hdmi
ただ、NES coreからのサウンド出力をそのままHDMIモジュールに接続しても、 クロックドメインが異なるためノイズが乗る。 HDMIモジュール内ではaudio_clkの立ち上がりでAudio信号をホールドしているようなので、 以下のようにaudio_clkの立ち下がりでサウンドを保持することによりノイズは消えた。
NES_top.sv
localparam CLKFRQ = 25000000;
localparam AUDIO_RATE = 32000;
localparam AUDIO_CLK_DELAY = CLKFRQ / AUDIO_RATE / 2;
reg [$clog2(AUDIO_CLK_DELAY)-1:0] audio_divider;
reg audio_clk;
wire [15:0] Audio [1:0];
reg [15:0] s_Audio [1:0];
	always @(posedge pixel_clk) begin
		if(audio_divider != AUDIO_CLK_DELAY - 1)
			audio_divider++;
		else begin
			audio_clk <= ~audio_clk;
			audio_divider <= 0;
			// クロックたち下がりでホールド
			if(audio_clk) s_Audio <= Audio;
		end
	end
あと、電源を入れた後に「ブツッ…ブツッ…」という定常的なノイズがあるのが謎(1分くらいで消える)。
他のボードでも出るからHDMI-USBキャプチャが原因か?
上記の HDMI IP に移行してもちろん映像は映るものの、 ドットクロックがIP想定25.2MHzのところ、24.975MHzのためか画面がちょっと横にずれる。 HDMIモジュールの入力にオフセット指定できるポートがあるので微調整した。
2025/04/13 追記: どうやらこの HDMI IP から出力されるピクセル座標に合わせて画面信号を入力してやる必要があるらしい。ちょっと面倒。
入力クロック 27 MHz から、49.5 MHz および 24.75 MHz を作るより、 まずDVI用のクロックを作って割った方が 25 MHz に近いのはたまたまなのか、 それとも高クロックからクロックツリーを設計すべきなのか。
また、CHR-ROMリードが間に合ってなかったので、 1クロック早くリードリクエストを出せるようにちょっと修正。
| リソース使用率 | 5,190 / 8,640 LUTs (61%) | 
| BlockSRAM使用率 | 10 / 26 (39%) | 
| NESコアFmax | 34.7 MHz | 
| 論理合成・配置配線時間 | 50 sec | 
| FPGAデータ・コード | Tang Nano 9K 用FPGAデータ(GitHub) | 
映像はHDMI-USBキャプチャでOBSに映してて、 UARTでキャプチャできればUSBキャプチャいらないのでは!?と考えたが、 1画面(256x240)は約122kByteなので1fps程度だと気づく (テーブルゲームのようなものならいけそう)。
NES on FPGA feat. TangNano9K:ROMデータをUART経由で送信するアプリ作ったので、ついでにキー操作もUARTで送信してゲームプレイできるようにした!PCにゲームパッドつなげて使えるようにすればもっと操作性良くなるかな pic.twitter.com/KMB6Hr4qTc
— かんな丸!! (@pgate1) 2024年6月11日
NSFプレイヤーにピアノロール的波形エフェクト画面を追加してみました。ちなみに3曲目のV.G.NEOは拡張音源のVRC7を使っている曲で、VRC7の本体としてvm2413を使わせて頂いています。 pic.twitter.com/kAPpSmT6zw
— かんな丸!! (@pgate1) 2022年9月22日
| 価格 | 約4,000円(2023年8月時点) | 
| デバイス | 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
— かんな丸!! (@pgate1) 2024年1月7日
1chipMSXに移植した時と同様に、SDRAM1個をPRG-ROMとCHR-ROMで共有してます。9Kよりも動作周波数上がって、NESコアは約50MHzでいけた。 pic.twitter.com/KKi8Dzhvzm
| リソース使用率 | 5,240 / 20,736 LUTs (26%) | 
| BlockSRAM使用率 | 10 / 46 (22%) | 
| NESコアFmax | 68.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モジュールを実装し移行した。
2024/05/04: プレステパッドつなげて操作できるようになった。 他のボードにつなげた時とちょっと受信信号がずれる(?)ので少し調整。 付属の似非プレステパッドもオリジナルプレステパッドとほぼ同じアクセスで使用できたが、 振動や感圧は無し。
NES on FPGA feat. TangNano20K:プレステパッドつなげて操作できるようになった。他のボードにつなげたときとちょっと受信信号がずれる(なんでだろ?)ので少し調整。これは自由に動けるようになって喜ぶ人 pic.twitter.com/7fSw8dWnoV
— かんな丸!! (@pgate1) 2024年5月4日
2024/05/06: ホーンからサウンド出せた。 アンプICのディジタル入力にはバンクIOの制約で3.3Vを使用。 低音が弱いので、三角波の音量を倍にする感じでちょうどいい。
NES on FPGA feat. TangNano20K:おとでた
— かんな丸!! (@pgate1) 2024年5月6日
ピントは永遠に合わない
HDMIにオーディオ流すのはまた時間が取れたらにしよ pic.twitter.com/Na9rb4XAyc
| 価格 | $129 販売サイト | 
| デバイス | Arora V GW5AST-LV138PG484AC1/10 (22nm SRAM) | 
| リソース | 138,240 LUTs、BlockSRAM 約765kByte | 
| 外部メモリ | SDR SDRAM 32MB×2、DDR3 DRAM 512GB×2 | 
| 参考 | Sipeed wiki Tang Retro Console | 
NES on FPGA feat. TangConsole
— かんな丸!! (@pgate1) 2025年4月13日
移植できた!LUT使用率は5%ほど
SDRAM1個で動かしてるのでメモリアクセスは Tang Nano 20K とほぼ同様の実装でok
HDMIオーディオは誤差を抑えたオーディオ用クロック作って音が途切れないようにできたので優勝 pic.twitter.com/1WOJ5vbSho
もう一方のGPIOヘッダにさらにSDRAMモジュールを追加できるようなので買うか。
NES_top.sv
// NG:audio_clk=32.010kHz 音が途切れる
localparam CLKFRQ = 25000000;
localparam AUDIO_RATE = 32000;
localparam AUDIO_CLK_DELAY = CLKFRQ / AUDIO_RATE / 2;
reg [$clog2(AUDIO_CLK_DELAY)-1:0] count;
reg audio_clk;
	always @(posedge pixel_clk or posedge sys_reset) begin
		if(sys_reset)
			count <= 0;
		else if(count != AUDIO_CLK_DELAY-1)
			count++;
		else begin
			count <= 0;
			audio_clk <= ~audio_clk;
		end
	end
// OK:audio_clk=32kHz 音の途切れなし
localparam COUNT_WIDTH = 13;
wire [COUNT_WIDTH-1:0] add;
wire [COUNT_WIDTH-1:0] max;
reg  [COUNT_WIDTH-1:0] count;
wire [COUNT_WIDTH-1:0] sa;
reg audio_clk;
	assign add = COUNT_WIDTH'('d8);
	assign max = COUNT_WIDTH'('d3125);
	assign sa = count - max;
	always @(posedge pixel_clk or posedge sys_reset) begin
		if(sys_reset)
			count <= 0;
		else if(sa[COUNT_WIDTH-1]) // count < max
			count <= count + add;
		else begin
			count <= sa + add;
			audio_clk <= ~audio_clk;
		end
	end
| リソース使用率 | 6,573 / 138,240 LUTs (5%) | 
| BlockSRAM使用率 | 13 / 340 (4%) | 
| NESコアFmax | 69.4 MHz | 
| 論理合成・配置配線時間 | 101 sec | 
| FPGAデータ・コード | Tang Console 138K 用FPGAデータ(GitHub) | 
せっかくUSBポートが2つあるのでUSBパッドをサポートできると良いな。 現状、PCからUART経由で操作できるようにしている。
SNES on FPGA feat. TangNano20K(No sound)
— かんな丸!! (@pgate1) 2024年1月8日
LUTが足りなくサウンドDSPを除外して使用率73%で実装しました。でもサウンドメモリは必要なのでBlockSRAMを使ってます。本当はSDRAM1個でWorkRAMと共存させたいけどアクセス速度がちょっと厳しくて無理だった。 pic.twitter.com/3IRMiSfHXs
その他の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ケアしてないので この実装は実験的なものに終わった。
2024/11/08 追記: SDRAMアービトレーション回路とリフレッシュ回路を修正して、 core 24.975 MHz、SDRAM 99.9 MHzで安定して動作するようになった。 SoundRAMサイズが実機同様64kByteとなったので、他のソフトも動くようになったと思う。
できるだけSDRAMアクセス時間を短くしたいということもあり、 従来SDRAMリフレッシュはオートランさせていたが、 今回はSNESのDRAMリフレッシュタイミング中にリフレッシュを行うよう修正。 動作としてはこちらの方が実機に近いかな。
2024/11/08 追記:
SNESコアのクリティカルパス箇所の修正と、不要なリセットの削減を行った。
また、リセットがLW(Long Wire、ロジックラインにも使用可能なパス)だったものを、
PRIMARY(グローバル専用ライン)に乗せた。
これによりSNESコアFmax 49.95MHz を達成した
(ただしコア 24.975MHz の方が安定して動作する…)。
リセット信号をグローバルPRIMARYラインに乗せるには2通りの方法があるようだ。
ドキュメントだとグローバルクロックラインとあるが、
リセットも乗せられるらしい。
記述例:CLOCK_LOC "sys_reset_6" BUFG = SR;
ここでsys_reset_6は論理合成で生成されるネット名のためHDLを修正すると名前が変わる可能性があるので、
レポートのHigh fanoutから判断して指定する。
記述例:DQCE dqce_inst ( .CLKIN(sys_reset), .CE(1'b1), .CLKOUT(g_sys_reset) );
DQCEプリミティブを使用することでHDLでリセット信号を指定できる。
(AroraVの場合はDCE)
SNES on FPGA feat. TangNano20K(No sound)
— かんな丸!! (@pgate1) 2024年1月8日
大好物なバハムートラグーンのOPデモもパスしました。どろどろ愛憎劇でリメイク待ってる。 pic.twitter.com/pBAiZa2ZrG
SNES on FPGA feat. TangNano20K(No sound)
— かんな丸!! (@pgate1) 2024年1月9日
セーブデータも読めるようにした pic.twitter.com/jXk2MJ4XYR
| リソース使用率 | 14,960 / 20,736 LUTs (73%) | 
| BlockSRAM使用率 | 46 / 46 (100%) | 
| SNESコアFmax | 33.4 MHz | 
| 論理合成・配置配線時間 | 150 sec | 
| FPGAデータ・コード | Tang Nano 20K 用FPGAデータ(GitHub) | 
以下の表は CPU,PPU,DMA 回路をリセットを変更して比較したもの。 なぜか同期リセットの方がLUT少し減ったような気もするけど、 どちらでもあまり変わりなさそう。
| リセットタイプ | 論理合成・配置配線時間 | 使用リソース | Fmax | 
|---|---|---|---|
| 非同期リセット | 150 sec | 9,237 LUTs, 4,145 regs | 33.4 MHz | 
| 同期リセット | 160 sec | 9,140 LUTs, 4,150 regs | 35.1 MHz | 
今回使用したのはSDRAMモジュール+アクリルカバー付きのもの。 138K LUT のFPGAモジュール(SOM)が載ってるが、60K のものもある。 Single Console はSDRAMモジュール無し(買った人によると付いてたらしいが)、 Retro Console Premium はSDRAMモジュールに加え、 アクリルカバーではなく3Dプリンタ出力したケースとTFカードも付属する。SNES on FPGA feat. TangConsole
— かんな丸!! (@pgate1) 2025年4月27日
実装できた!
TangNano20KだとサウンドDSPが入らなくて音無しだったけどこっちは138Kもあるので、スーファミ全実装が可能か不可能かで言えば、余裕です(LUT使用率19%)
はじめ動作が不安定なことがあったけど入出力遅延制約付けたら安定したし pic.twitter.com/N50P5IpN60
| リソース使用率 | 24,658 / 138,240 LUTs (18%) | 
| BlockSRAM使用率 | 145 / 340 (43%) | 
| SNESコアFmax | 50.1 MHz | 
| 論理合成・配置配線時間 | 416 sec | 
| FPGAデータ・コード | Tang Retro Console 138K 用FPGAデータ(GitHub) | 
サウンドがHDMI直なので多少のノイズがあり、フィルタ回路を実装した方が良さそう。
簡単なテストHDLでも、ボードによって動いたり動かなかったりするのは品質にばらつきがあるのかな?
Tang Nano 60K の発売待ってますよ!SipeedさんTang Console 60K/138K が出ました!ありがとうSipeedさん(読みはシペード?)。
NESコアとSNESコアは、どちらもクロック1つの完全同期設計であることと、 CPUがマイクロプログラムベースではないためBlockSRAMを使わないで済むことも 移植が容易だった要因だろう。
しかし初期実装でHDMI用のピンアサインが中途半端で変な出力してたせいか、 通常時30℃くらいなのが50℃くらいになりノイズが乗って壊れそうになったことがあったので、 そのあたり気を付けましょうという事で。2,500円の中華製HDMI-USBキャプチャなかなか良くて小型モニタいらないね。TangNano4Kのカメラデモ普通に映ってるし。MS2130っていうチップ使ってるらしいhttps://t.co/3lgboU3VvK pic.twitter.com/YpjplBOXou
— かんな丸!! (@pgate1) 2023年11月7日