NES on FPGA 2004/11/28

dev NES

 CPU、PPU、APU、WRAM、VRAMをモジュールとして持ち、 それらの接続と外部インタフェース部分を記述しています。


アドレスラインをおおざっぱに描くとこんな感じでしょうか。
CPUから出力する16bitのアドレスのうち、WRAMには下位11bit、 PPUのI/Oには下位3bit、APUには下位5bit、カートリッジには下位15bitを接続しています。

_
▼ CPUメモリマップ

any{
	A_reg<15:13>==0b000 : par{	// $0000-$07FF WRAM
		map_wmem();
		mpu.din = wmem.dout;
	}
	A_reg<15:13>==0b001 : par{	// $2000-$2007 PPU
		map_ppu();
		mpu.din = ppu.Dout;
	}
	A_reg<15:13>==0b010 : any{
		A_reg<12:5>==0b00000000 : par{ // $4000-$401F APU, PAD
			map_apu();
			mpu.din = io_reg;
		}
		else : par{ // $4020-$5FFF ExROM
			map_nesrom();
			mpu.din = prg_din;
		}
	}
	A_reg<15:13>==0b011 : par{ // $6000-$7FFF ExRAM
		map_nesrom();
		mpu.din = prg_din;
	}
	A_reg<15> : par{ // $8000-$FFFF ROM
		map_nesrom();
		mpu.din = prg_din;
	}
}

 アドレス$4020以降はカートリッジへのアクセスとなります。
APUとPADアクセス部分では、読み込みと書き込みで用途が異なる部分があります。 読み込み時にはウエイト部分を励起する必要があります。

_
▼ PPUメモリマップ

if(VRAM_CSn==0b0){
	// VRAM
	ppu.PDin = vmem.dout;
}
else{
	// カートリッジ(CHR-ROM)
	ppu.PDin = chr_din;
}

 VRAM_CS(チップセレクト)はカートリッジからの入力です。 この信号がLowの間、VRAMへのアクセスとなります。 カラーパレットはPPU内部にあり、PPU外部のメモリマップには現れません。

_
▼ リードアクセスウエイト

 FPGAクロック33MHzにおいて内部ブロックRAMへのアクセスは2クロック (リード要求、データ取得)必要です。 またCPUからカートリッジへのアクセスでは安全のため、 CPUが次に励起される直前までウエイトをかけています(15クロック)。 ウエイトの後、CPUに対してACKを入力することで、 CPUはレジスタへデータを書き込みます。 PPUからカートリッジへのアクセスではCPUよりも短いウエイトとなっています(9クロック)。

_
▼ DMA

 DMAが動作している間は、CPUのレディ信号に対して0を入力し、CPUを停止させておきます。 これも実機とクロック数を合わせるためです。

 一部情報では、転送元でROMを指定してもDMAできないようだ。 転送元にWRAMを指定すると問題ないみたい。 実装では今のところROMからもDMAできる実装となっているが、 解析結果によっては削減できるかもしれない。

_
▼ 動作タイミングをがんばって実機に近づける! 2019/05/20

 真面目に言うと、ダイレクト・ディジタル・シンセサイザ (Direct Digital Synthesizer, DDS)を応用した任意サイクルの生成です。 プログラムはこちら

 FPGAでのコア部分完全同期設計のためには、 例えばベースクロック50MHzからPPUクロック5.369318MHz(ターゲット周波数)を作る必要があります。 もちろんPLLは使用しません。

 そこでDDSを応用したカウンタでベースクロックからイネーブルトリガを生成する方法を使います。 この方法の良い所は、イネーブルトリガがばらけていること(出現が偏っていないこと)が特徴です。 例えば、ターゲット周波数が30MHzだと、その比は5:3なので、加算値3、しきい値5となります。

	// 以下、疑似HDLコード

	input clk; // ベースクロック入力
	wire add;  // 加算値
	wire max;  // しきい値
	reg count; // カウンタ
	output enable; // トリガ出力

	add = 3;
	max = 5;
	count = 0;

	if(clk){
		if(count < max){
			count += add;
		}
		else{
			count += add - max;
			enable();
		}
	}
// 使う側 input clk; input enable; if(clk){ if(enable){ mpu.run(); // 30Mサイクルで動作 } }
3/5のタイミングでイネーブルトリガが出ます。 あくまでイネーブル信号なので30MHzのクロックでは無いことに注意。

 この加算値としきい値をパラメータと考えて適切な値を設定するのですが、 普通は最大公約数を考えます。 ベースクロック50MHzで、ターゲット周波数が33.8688MHzだと、 最大公約数が3200です。 それぞれ最大公約数で割ると、しきい値が15625、加算値が10584となります。 これは割と少ないビット数のカウンタで誤差が0のサイクルを生成できます。 最大公約数が大きいほどカウンタのビット数が少なくて済むわけですな。 (ここでの誤差は、ターゲット周波数とイネーブルサイクルの差)

 では、最大公約数が小さい場合どうするかというところ。 カウンタのビット数を増やせば増やすほど誤差は小さくなります。 誤差は小さくしたいけど、カウンタのビット数はできるだけ減らしたい。 つまり適切な加算値としきい値を選べると嬉しいのです。

 PPUの動作クロックは5.369318MHzです。 これをベースクロック50MHzから生成するとなると、パラメータ探索結果は以下の通り。

最大公約数(GCD) 2
加算値  1bit       1 しきい値        9  誤差 186237.5555555550 34685.51416689PPM
加算値  2bit       3 しきい値       28  誤差  12175.1428571427  2267.53991050PPM
加算値  4bit      13 しきい値      121  誤差   2582.8264462808   481.03435972PPM
加算値  5bit      16 しきい値      149  誤差    190.4832214769    35.47624139PPM
加算値  7bit     125 しきい値     1164  誤差     97.8075601375    18.21601182PPM
加算値  8bit     189 しきい値     1760  誤差      0.1818181816     0.03386243PPM
加算値 14bit   16270 しきい値   151509  誤差      0.0056894310     0.00105962PPM
加算値 15bit   16837 しきい値   156789  誤差      0.0006250441     0.00011641PPM
加算値 16bit   50322 しきい値   468607  誤差      0.0000554835     0.00001033PPM
加算値 17bit  117481 しきい値  1094003  誤差      0.0000420474     0.00000783PPM
加算値 18bit  218125 しきい値  2031217  誤差      0.0000029542     0.00000055PPM
加算値 20bit  604053 しきい値  5625044  誤差      0.0000014221     0.00000026PPM
加算値 21bit 1862481 しきい値 17343739  誤差      0.0000001155     0.00000002PPM
加算値 22bit 2684659 しきい値 25000000  誤差      0.0000000000     0.00000000PPM
結果、加算値が22ビットあれば誤差0のイネーブルサイクルが作れるようですが、 しきい値も大きいためカウンタのビット数は26ビットも必要になってしまいます。 誤差をいくらか許容するならば※、8ビット程度の加算値を選択することができ、 この時のカウンタは12ビットで済みます。 (※ベースクロック自体の誤差から見て十分小さい誤差かどうか判断)


Copyright(C) pgate1 All Rights Reserved.