NES on FPGA 2022/09/22

NES on FPGA feat. 1chipMSX

ファミコン(NES)を1chipMSXに実装したよっていう話。
正確に言うと、作ってたものを少し修正して移植しただけなんだけど、 メモに残しておきたくて。


Youtube : NES on FPGA feat. 1chipMSX

MSXに触れて

MSX歴。親戚の家にMSXがありゲームで遊んだのち、借りてちょっといじってみたけど、 資料がなく、簡単な図形を書いたり音を鳴らしたりしただけ。 分解はしなかった。 BASICとしては、学校にPC-98なんとかがあって3分程度のグラフィック作品を作った。 また、Z80ボードとアセンブラで作ったLEDもぐらたたきゲームが好評だった。 あと先輩がMSX持ってて3人同時プレイで弾を当て合う謎のゲームが面白くてずっとやってた。 スクウェア好きなのでMSX版ファイナルファンタジーを入手したけど遊ぶ機会はなかった。 その程度。

1chipMSXの流れ(簡易版)


私は1chipMSXの発売を待てず、 その数ヵ月前に発売されたほぼ同価格のTerasic社DE1を購入したため、 1chipMSXは見送ってしまった。 DE1にはファミコンを実装したけど、FPGAボードとしての1chipMSXはずっと気になっていた。 そしてしばらくして…。

1chipMSXを手に入れた!

2022年7月てっぺんを越えて17日、 いっとう氏(@ittou_ogami)が 「MSXなFPGAボード使わないのに捨てられない」とツイート、すぐ手を上げて譲っていただいた。 実はこの数ヵ月前に、とある方が「1chipMSX譲ります」とツイート、本当に譲り受けてもいいのか? と迷っている間に他の方が手を上げ、私はそのチャンスを逃していた。 なので迷ってる暇はない即断即決が大事だと心に刻んでいた。閑話休題。

数日後、1chipMSXが届く。 また、別途購入していた後期版DE1も到着し、2006年の再演を感じた。 これでようやく、1chipMSXにファミコンを実装する、という心の隅に置いておいたタスクを実行できそう! ムネがドキドキする。

1chipMSXと、DE1 FPGAボード(後期版)、どちらも2006年頃に発売され、本日同時にお迎えできたこと、どこか運命的ですね。MSXソフト持ってないので、1chipMSXにファミコンを実装していきたいと思います pic.twitter.com/1kiRqrU4C4

— ⁧ !!⁨かんな丸 (@pgate1) July 18, 2022

FPGAボードとして使う

FPGAボードを新しく入手した時には、基本的に以下を実施している。

マニュアルやサンプル回路はあったので問題なし。

動作確認は、背面のDIPスイッチでSDカードとVGA出力を有効化して電源投入。 1chipMSXの青い画面が出る、PS/2キーボードを接続して簡単なBASICを書いてみて動作確認。

できたできた pic.twitter.com/xyolpOyICJ

— ⁧ !!⁨かんな丸 (@pgate1) July 19, 2022

FPGA(PROM)書き換え

1chipMSXに含まれているプログラムには、SDカードからPROMを書き換える機能があるけど、 その機能がないFPGAデータに書き換えると1chipMSXが立ち上がらないので、その機能は使えなくなる(それはそう)。 なので手軽にPROMを書き換えて遊ぶにはダウンロードケーブルが必要、回路図を見るにFPGAのJTAGピンは使用されていないようで、 .pof(FPGAデータ)をPROMにダウンロードし、電源投入時にFPGAが書き換えられる。 FPGAのJTAGピンが接続されていれば、SignalTapやVirtualJTAGを使ってデバグしやすかったんだけどな。

ダウンロードケーブルとして、Intel社のUSB-Blasterは4万円以上と高価なのと、 やたら安い互換品は不具合があると面倒そうだったので、 パートナー互換品であるTerasic社 USB Blaster をDigikeyから9,600円で購入。 6,000円以上なので送料無料、10,000円未満なので消費税もかからなかった。 注文時に用途欄にはhobbyとだけ記入し、翌々日には到着。 ちなみに、Mouserだと消費税がかかり、Terasicストアは品切れ状態で納期がかかりそうだった。

Terasic USB Blaster到着!発注から2日!はや!
これで1chipMSXを書き換えられる、たぶん

今まで使ってたDEシリーズFPGAボードはUSB Blaster機能がボードに載ってたから必要なかったんだよ pic.twitter.com/QsTk6RAaHB

— ⁧ !!⁨かんな丸 (@pgate1) July 20, 2022

ダウンロードケーブルの接続のためには、1chipMSXのかっこいいクリアブルーのケースを開ける必要があり、 ちょっともったいない。 基板の右側にJTAGピン、ピン番号を間違わないように Terasic USB Blasterのサイトを参考にした。

とりあえずLチカするデザインをコンパイルするために、 初代Cycloneをサポートしている最終バージョンのQuartusII 11.0sp1 WebEditionを持っていたのでインストールする。 こんなこともあろうかとインストーラを保管しておいたのだよ。私えらい。

Lチカの回路を記述してコンパイル。 プロジェクト名に悩む、1chipMSX…OneChipMSX…OCM(ここで初めてOCMの意味を理解する)。 付属のソースコードはVHDLだけど、VHDLは複数行コメントアウトができず、 VerilogHDLのスケルトン を公開されている方がいたのでありがたく使わせていただく。

.pofができたのでいよいよ書き込み。 ところが、USB Blasterを認識しない。他のFPGAボードなんかも認識しなくなった。 おそらく古いバージョンのQuartusIIをインストールしたせいでドライバ関連が混乱したためと考えられる。 新しいバージョンのProgrammerを再インストールしたらUSB Blasterも認識された。

PROMの書き換えには、.pofを選択し、ASモードで書き込む。 10秒ほどでPROMを上書きし、無事Lチカできた。 ちょっとびっくりしたのが、8個のLEDの並びが[0:7]となっていること。[7:0]ではないんだね。 [7:0]にすべく


.LED({pLed[0],pLed[1],pLed[2],pLed[3],pLed[4],pLed[5],pLed[6],pLed[7]})

としてしまった。

USB Blaster接続とLチカのようす(滑り止めにMentorGraphicsのシート)
ちなみに、付属のemsx_top.pofをPROMに書けば、元の1chipMSXに戻ることも確認。

周辺デバイスを使用する

ベースクロックは21.47727MHzが使えるけど、そのままだとVGA出力の比率がちょっと気になる。 そもそも移植しようとしているものが50MHzをベースに作ったものなので、 タイミングを改めて計算しなおし調整するのが非常に面倒と思われたので、 PLLで50MHzを生成してこれを使用することとした。 21.47727MHzの7/3で約50.1MHz、まあいいか。 VGAもちょうどいい感じに出力できた。 ただRGB:666だと輝度が高いので、最上位ビットは0としてRGB:555で使用する。

サウンドはパラレル6bitだけど、16bitを1bitΔΣを通して、最上位ビットから出力。 一応ステレオ出力できるみたいだけど、とりあえずモノラル。 ダイソーの300円ミニスピーカーを変換プラグを介して接続。 スピーカーの電源は1chipMSXのUSB端子からとれるかな? 電力次第かな。

SDRAMコントローラも流用してビット幅を修正した程度で、 SDRAMテストもパス。

ファミコンのROMを読み込ませるためにはどうするか。 1chipMSXの2カートリッジスロットに、ファミコンカートリッジを接続する“げた”のような拡張基板を作るのも妄想されたけど、 ここはシンプルに別途吸い出したROMをSDカードから読み込ませる。

SDカードコントローラもテスト、ただ2回に一回くらいの頻度で読み込みに失敗する現象あり。 試行錯誤して、SDカードの電源投入時間確保のためにリセット時間を250ms以上としたことに加え、 そもそも回路図を見るとSD_D0がプルアップされておらず、 1回目のCMD0でエラーを返していることに対処するためにCMD0を2回実行することでSDカードも安定した。

参考: SDカードのSPIモードの初期化に関する諸々

やっぱりこれが原因っぽい
1回目のCMD0をエラー処理しないようにしたら通った https://t.co/gnC3cjeezZ

— ⁧ !!⁨かんな丸 (@pgate1) September 13, 2022
ちなみに基本はSPIモードで使用するけど、一応SDモードでも動作した。

操作系はPS/2キーボードかMSXのジョイパッド、 PS/2キーボードは1chipMSXコアでは相性のせいか動作しなかったものも動作させた。 ジョイパッドは別途入手し動作確認OK。 ジョイパッドにはスタートボタンセレクトボタンが無いので、それはキーボードで代用する。

やはりキーボードではプレイに無理があると思われたのでジョイパッドを入手したよ。想像してたよりもちっこい pic.twitter.com/1g4zotL79w

— ⁧ !!⁨かんな丸 (@pgate1) August 19, 2022

これでFPGAボードとして使用する準備が整った。

NES on FPGAの移植

プログラムROM(PRG-ROM)はSDRAMで、キャラクタROM(CHR-ROM)はFPGA内蔵RAMを8kB割り当てて、 一発でスーパーマリオが動作。 ん?あっけないぞ。 Mapper1(MMC1)を有効化してドラゴンクエスト4や Mapper4(MMC3)のファイナルファンタジー3も動いた。 Mapper73(VRC3)で沙羅曼蛇も動作。 ちなみにこれらはCHR-ROMではなく8kBのCHR-RAMを使用してるので動くわけ。 実装としてはCHR-RAMなんだけど、一応ここではCHR-ROMとして説明する。

このまま完成動画を撮ってもよかったんだけど、動かしているといくつか問題点が気になる。

SDRAM1つでなんとかする

FPGAとSDRAMとSDカードの構成なので、DE0と非常に似ている。 ということで、DE0のものを1chipMSXボード用に書き換える感じで修正作業を進める。 1chipMSXではJTAGが使えず、7セグLEDも実装されてないので観測性が悪いため、 まずDE0でデバグして1chipMSXに持っていく作戦である。 ちなみにプログラムが動作しているかは、8bitのプログラムカウンタをLEDに出力して無理やり観測できるけど、 これはスーパーマリオくらいしか分からない。

組み込み屋はプログラムカウンタをLEDに出力して光り方でプログラムがちゃんと走ってるか確認する。これは何のファミコンソフトでしょうか?? pic.twitter.com/McVwvfhCq3

— ⁧ !!⁨かんな丸 (@pgate1) August 8, 2022

一番の課題はCHR-ROMが足りないこと。 スーパーマリオ程度もしくはCHR-RAMを使用するソフトなら8kBのCHR-ROMを内蔵RAMで実装すればいいけど、 それ以上のサイズのCHR-ROMのものを動作させるには廉価版FPGAの内蔵RAM程度では足りない。

内蔵RAMをCHR-ROMとして使う
Mapper cart;
sdram_ctrl sdram; // PRG-ROMとして使用

instruct cart.prg_rom_read par{
   sdram.read(0b0000||cart.prg_rom_adrs);
}
cart.prg_rom_rdata = sdram.rdata<7:0>;

instruct cart.prg_rom_write par{
   sdram.write(0b0000||cart.prg_rom_adrs, 0x00||cart.prg_rom_wdata);
}

ram_8x8k chr_ram; // 8kB程度しか確保できない

instruct cart.chr_ram_read par{
   chr_ram.read(cart.chr_ram_adrs<12:0>);
}
cart.chr_ram_rdata = chr_ram.dout;

instruct cart.chr_ram_write par{
   chr_ram.write(cart.chr_ram_adrs<12:0>, cart.chr_ram_wdata);
}

そこで、PRG-ROMとして使用しているSDRAMをCHR-ROMとしても共有する。 共有はアービトレーション回路を組めばいいけど、 ポイントはCPUやPPUの動作に間に合うかということ。 基本的にNES on FPGAは実カートリッジをサポートするために、 可能な限りファミコン実機同様のタイミングで各モジュールを動作させるようにしている。 つまり、CPUやPPUがメモリアクセス要求を出して、既定サイクル数以内でそれを完了させる必要がある。

これまでは、例えばメモリリード時は、その規定サイクル数をカウントして、メモリからの 出力をレジスタにセットしていた。 しかし、SDRAMを共有する場合、CPUからのアクセスとPPUからのアクセスと、 SDRAMのリフレッシュ動作が入り混じるため、どのタイミングでメモリアクセスが終了するか確定しない。 そこで、アクセス要求をReq信号、アクセス終了をAck信号で管理することで、 メモリアクセスのアービトレーションを行いやすくする。 アービトレーションは、
 SDRAMリフレッシュ > PRG-ROMアクセス > CHR-ROMアクセス
の順に優先度をつけることで、 同時にリクエストが発生した場合のロジックが少し簡単になる。 またこれに伴い、グラフィック描画ユニットであるPPUのレジスタ反映タイミングを少し修正。 ここだけはファミコンの回路を少し思い出しながらの作業となった。

50MHzでのメモリアクセスを考えると、 CHR-ROM ⇒ CHR-ROMアクセス間隔は 18クロック なので、 最速のメモリアクセスは 18クロック - CHR-ROMアクセス5クロック = 13クロック の猶予がある。 最遅のメモリアクセスパターンは、SDRAMリフレッシュ3クロック + PRG-ROMアクセス5クロック = 8クロック。 なので、間に合わない場合はSDRAMを100MHzで動作させることも想定していたけど、 50MHzで間に合いそうだ。

今回はざっくり計算して一応動いてはいるけど、 本来ならコーナーケースも考えて、全パターンのシミュレーションした方が安心。

DE0でファミコン動かすだけなのになんでこんなに苦労してんだあたしゃ

— ⁧ !!⁨かんな丸 (@pgate1) August 22, 2022
メモリアービトレーション回路の動作をDE0で確認し、1chipMSXに持っていくことで、 CHR-ROMが128kBあるようなグラディウスIIや星のカービィやスーパーマリオ3が問題なく動作した。 副次的に、DE0でもこれまでサポートできていなかったそれらのROMも動かせるようになった。
SDRAMをPRG-ROMとCHR-ROMで共有する
Mapper cart;
sdram_ctrl sdram;

reg_wr prg_read_wait, prg_read_ack_wait;
reg_wr chr_read_wait, chr_read_ack_wait;
reg_wr chr_write_wait, chr_write_ack_wait;

// PRG-ROM readは、CHR-ROMアクセス発行と同時だったらこちら優先だが、
// すでにCHR-ROMアクセス時なら終了待ち、ack必要。
if( (
   (cart.prg_rom_read & 
      (^chr_read_wait) & (^chr_read_ack_wait) & (^chr_write_wait) & (^chr_write_ack_wait)
   ) | prg_read_wait
) & sdram.ack){
   sdram.read(0b0000||cart.prg_rom_adrs);
   prg_read_wait := 0b0;
   prg_read_ack_wait := 0b1;
}
else if(cart.prg_rom_read) prg_read_wait := 0b1;
if(prg_read_ack_wait & sdram.ack){
   prg_read_ack_wait := 0b0;
   cart.prg_rom_ack();
}
cart.prg_rom_rdata = sdram.rdata<7:0>;

// PRG-ROM writeは、ROM読み込み時のみなので排他処理不要。
instruct cart.prg_rom_write par{
   sdram.write(0b0000||cart.prg_rom_adrs, 0x00||cart.prg_rom_wdata);
}

// CHR-ROM readは、PRG-ROM readアクセス終了待ち、ack必要。
if( (cart.chr_ram_read | chr_read_wait) &
   (^cart.prg_rom_read) & (^prg_read_wait) & (^prg_read_ack_wait) & sdram.ack){
   sdram.read(0b000100||cart.chr_ram_adrs);
   chr_read_wait := 0b0;
   chr_read_ack_wait := 0b1;
}
else if(cart.chr_ram_read) chr_read_wait := 0b1;
if(chr_read_ack_wait & sdram.ack){
   chr_read_ack_wait := 0b0;
   cart.chr_ram_ack();
}
cart.chr_ram_rdata = sdram.rdata<7:0>;

// CHR-RAM writeは、PRG-ROM readアクセス終了待ち、ack不要。
if( (cart.chr_ram_write | chr_write_wait) &
   (^cart.prg_rom_read) & (^prg_read_wait) & (^prg_read_ack_wait) & sdram.ack){
   sdram.write(0b000100||cart.chr_ram_adrs, 0x00||cart.chr_ram_wdata);
   chr_write_wait := 0b0;
   chr_write_ack_wait := 0b1;
}
else if(cart.chr_ram_write) chr_write_wait := 0b1;
if(chr_write_ack_wait & sdram.ack){
   chr_write_ack_wait := 0b0;
}

NES on FPGA:PRG-ROMとCHR-ROMを1つのSDRAM内で共有することで、DE0ボードCycloneIIIの内蔵メモリだけでは賄えないサイズのROMを動かせました!星のカービィとかマリオ3とか。これでようやく1chipMSXにも移植できそう。 pic.twitter.com/W8SV6SQoiI

— ⁧ !!⁨かんな丸 (@pgate1) September 10, 2022

ちなみに1chipMSXと同時期に発売されて私が購入したDE1は、SDRAMに加えてSRAMも搭載している。 ファミコン実装の際は、SDRAMをPRG-ROMとして、SRAMをCHR-ROMとして使っていたので、 上述したような工夫は必要なかったってわけ。

より正確な動作のために

今回は、ベースクロックの21.47727MHzをPLLで7/3して50.11363MHzを生成し使用した。 DE0の場合、ベースクロック50MHzを元に5.369318MHzのイネーブル信号を作り、 NES on FPGAの各モジュールを動作させている。 これをそのまま約50.1MHzで動作させてもそんなに大きな差は分からないけど、 イネーブル信号を少し正確にする。

参考: DDSを応用した任意サイクルの生成

50.11363MHzから5.369318MHzのイネーブル信号を作るのに、 23bitのカウンタで、加算値383524、最大値3579557、誤差0.0000016のイネーブル信号を生成、 これを使用するようにした。 うん、体感的にはあまり差は分からない。

サウンドについて気になった点の修正

1chipMSXというよりはNES on FPGAの問題。

DE0にはオーディオジャックが付いていないので、これらの調整は1chipMSX上で行った。

ROMを切り替えた時に余計なサウンドが鳴る。 例えばファイナルファンタジー3の動作時にすべてのサウンドチャンネルが有効になる瞬間があり、 不要な音が鳴ってしまう。 これはROM読み込み時にサウンド生成ユニットであるAPUの各チャンネルレジスタのリセットを強化して回避。

拡張音源が鳴りっぱなしになる。 これは、拡張音源を使用したNSF再生後に、通常のROMに切り替えると拡張音源の出力値が残ってしまうので、 拡張音源フラグをミュートフラグとしても使用することで回避。

スピーカーの低音が弱いので、三角波が聞えづらい。 コスパが良いダイソーの300円スピーカーを使ってみたけど、低音が少し弱かった。 そこでNES on FPGAにボリュームブースト信号回路を追加し、 このスピーカーを使う場合は、三角波のボリュームを加算することで聞こえるようにした。

NSF Player

NES on FPGAの機能としてNSFを演奏できます。 音が鳴るだけでなく、せっかくVGAも出力しているので、 波形っぽいエフェクトを画面に出してみました。

NSFプレイヤーにピアノロール的波形エフェクト画面を追加してみました。ちなみに3曲目のV.G.NEOは拡張音源のVRC7を使っている曲で、VRC7の本体としてvm2413を使わせて頂いています。 pic.twitter.com/kAPpSmT6zw

— ⁧ !!⁨かんな丸 (@pgate1) September 22, 2022
当然ながら拡張音源モリモリ森鴎外。 ちなみにVRC7はvm2413のVoiceROMをVRC7のものに変更したものを使用しているので、 実質MSX上でYM2413が動作しているような感じになっています(?)。 いとをかし。

ファミコンの魂が宿った1chipMSX

実装結果

NES on FPGA feat. 1chip MSX
 Cyclone EP1C12Q240C8
  約12kLEのうち使用率 88%
  内蔵RAM約30kBのうち使用率 54%

 NES core : 3313 LEs
 拡張音源 : 4826 LEs
 Mapper 0,1,2,3,4,25,73 : 966 LEs

FPGAデータはこちら Github: NES on FPGA
ご利用にあたっては自己責任でお願いいたします。

所感

MSXフリークたちは1chipMSXに機能改善したMSXを実装しているけど、 私はファミコンだろってことで。 改めてこのような機会を与えて頂いた いっとう氏 に感謝を申し上げます。

2006年ぶりにファミコンを1chipMSXに実装するという宿題を終わらせることができたけど、 はたして、私が当時1chipMSXを購入してファミコンを実装できていただろうかね? 初心者にやさしいDE1というボードで実装し経験を積み、 当時1chipMSXを購入した人たちが使い倒して情報公開しているからこそ今回のチャレンジができたように思います。

そんな形で1chipMSXをFPGAボードとして使ってみて、 1chipMSXの発売当初に提唱されていたOSXって、今、実現できてるのかな?と考えたりして。 1chipMSXに限らず、入手性の良い高性能なFPGAボード、バッドノウハウの少ない開発環境、 作ったものの共有とそれを通じた同志たちの繋がり。 そのような中で成果を出しているプロジェクトはいくつもあって、 それらのプロジェクトが相互に協力し合っている場面もみられます。 なんとなくプラットフォームとしてもうまくいっているんじゃないかな。 プロジェクトはtime goes by、でも私はゆるりとやるばい。 その過程でなにか貢献出来たらなーという感じです。FPGAエンジョイ勢なので。

2022年、AlteraもXilinxも買収され、少しFPGA界隈のすそ野の広がりが落ち着いてきた感じはあるものの(そうでもない?)、 コンピュータアーキテクチャの初学的な位置づけとしてファミコン実装はどうでしょうか、 と無理やりお勧めする流れを作ってみる。 ただ最近、部材不足と円安でFPGAボードの値上がりがつらいですよね…、 学生のうちに研究室で買ってもらうか、アカデミック価格で購入するのが良さそうです。

おまけ

1chipMSXでもポリゴン回路動いた。DSPが内蔵されてないから乗算回路のパスが長くて、動作周波数は25MHz。緑色が白飛びしてるのはデジカメのせい pic.twitter.com/dyy9LNGskc

— ⁧ !!⁨かんな丸 (@pgate1) September 25, 2022
よーし、次はMSX3にプレステ実装しちゃうぞー。

©2022 pgate1