NES on FPGA 2007/05/20

RAM・ROM

 結果的にはSpartan-IIE300に内蔵されているBlockRAMで、NESで必要なメモリサイズ全てを確保することができました。 それぞれのRAMは個別にアクセス可能なので並列性も問題ありません。

_
▼ FPGA内部ブロックRAM割り当て

 Spartan-IIE300には内部ブロックRAMが16個存在し、 1ワード8ビットとすると一つのブロックRAMは512バイトになります。
用途サイズワードブロックRAM数
WRAM2Kバイト8ビット4
VRAM2Kバイト8ビット4
スプライトRAM256バイト8ビット1
パレットRAM32バイト8ビット1
RGBカラーROM128Word16ビット1
描画用フリップバッファ256Word×216ビット2
サウンドディレイバッファ256バイト×28ビット2
合計15

 WRAM、VRAM共に同じサイズの2Kバイトです。 4つのブロックRAMをカスケード接続し2Kバイトとなり、 アドレス11ビットのうち上位2ビットによってRAMを選択しています。 パレットRAMは分散メモリ(レジスタ)として配置してもいいみいたいです。

 これらRAMについてはSFLではなくVHDLで記述しています(下記参照)。 数バイトの小規模なものについてはSFL記述→VerilogHDL変換にて分散RAMとしてマッピングされるようにします。

_
▼ HDLによるRAM記述

 最初はXilinxデバイスのブロックRAM記述を指定したため、 これをそのままAlteraデバイスへ実装することができませんでした。 そこでHDLによる汎用的なRAM記述を書いておくことで、 Alteraなど他のデバイスにも流用できるようにしておきます。 [RAMの記述例] (genericを使用していないのはSFLから接続するためです。)

 これは最近の論理合成ツールの推論機能を利用したものです。 HDLの解析のときにRAM記述を検出し、 自動的にデバイス内のブロックRAMやLUTへマッピングしてくれます。 つまりデバイスに依存しない汎用的なRAM記述で、どのベンダのデバイスにもマッピングでき、 RAMのサイズやアクセス方法に最適なマッピングが行われます(ツールの性能にもよりますが…)。

 また、ブロックRAMを複数使用するときに、カスケード接続(512KB×4)するよう記述していました。 これも、必要なサイズ(2048KB)を汎用的なRAM記述で書いておけばツールが見繕ってくれるため、 こちらでブロックRAMの接続を意識する必要はありません。

_
▼ ROMの初期値設定

 SFLではROM記述ができないため (any文で記述可能ですが、あまり使い道はないような…。VerilogHDLに変換すればROMとして推論可能?)、 VHDLで初期値として記述し、 Xilinxの合成でブロックRAMをインスタンシエート(初期化)してもらえます。 [RGB_ROMのXilinx向け(古い)VHDL記述例]

 このROM記述についても、constant構文を利用した汎用的な記述をすることで、 ツールの推論によりデバイスにマッピングしてもらえます。 [RGB_ROMの汎用的なVHDL記述例]

 最近のsfl2vlでは、mem rom[256]<8> = { 0x12, 0x23, ... }; のような記述でROMの初期化が可能です。 VerilogHDLに変換するとinitial文で記述されてるようなのでROMに論理合成されます。

_
▼ NSLでのmem初期化時の注意 2018/03/21

 レジスタをたくさん使いたい場合、いちいちレジスタ宣言するのも面倒なので、 mem記述で宣言します。

mem pat_reg[4][2];
これをnsl2vlでVerilogHDLにすると、
reg [1:0] pat_reg [0:3];

  always @(posedge m_clock)
    begin
     if(write)
       pat_reg[adrs] <= din;
    end
みたいな感じになります。 注意したいのは、memで宣言だけするとregの初期化もリセットもされないという事。 このためregは不定値として扱われ、回路によっては最適化され意図しない動作となってしまいます。おそろしい!

 mem宣言するときは以下のように、初期化すればゼロクリアされます。

mem pat_reg[4][2] = {0};
nsl2vlでVerilogHDLにするとinitial文で初期値が設定されます。 これはXilinxやIntel(旧Altera)の記述スタイルでも推奨されています。
reg [1:0] pat_reg [0:3];

  initial begin
    pat_reg[0] = 2'b00;
    pat_reg[1] = 0;
    pat_reg[2] = 0;
    pat_reg[3] = 0;
  end

 もちろん任意値の指定も可。

mem pat_reg[4][2] = {3, 2, 1};
reg [1:0] pat_reg [0:3];

  initial begin
    pat_reg[0] = 2'b11;
    pat_reg[1] = 2'b10;
    pat_reg[2] = 2'b01;
    pat_reg[3] = 0;
  end

RAM初期化についての注意! 2018/07/30追記

 上記のとおり初期化すれば、初期値を持たせたRAMを推論させることができます。 ただし、この初期値が有効なのはFPGAデータロード時のみで、 推論されたRAMがリセットを持たない場合は注意が必要です。 つまり初期値から書き換えられたRAMの内容はユーザリセットからまた初期値に戻すことができません。 RAMの初期化のためには別途初期値を持つROMが必要となります。

 使い方としては、ROMに推論させる場合にはmemの初期値を設定し、 RAMに推論させる場合は別途ユーザリセットにより初期値を設定するロジックを持っておくことが良さそうです。

_
▼ SFLによるRAM使用上の注意

 SFLでmem宣言を用いてRAMを記述したとする。 このとき、一箇所でのwriteやreadなら問題とはならないが、 複数箇所でwriteやreadした場合に問題となる。 以下のような記述をした場合に、 コンパイラのRAM推定機能にうまくマッチせずにコンパイル時間が異常に長かったり、 BlockRAMにマッピングしたいのにDistributed RAMに配置されたりしてしまう。

 もちろん複数箇所でのwriteやreadは、タイミング的に重なっていないことが前提だが。
 ダメな記述例(複数箇所でwriteやread)

mem ram[1024]<8>;
	...
	ram[adrs1] := wdata1;
	...
	ram[adrs2] := wdata2;
	...
	rdata := ram[adrs3];
	...
	rdata := ram[adrs4];

 うまくRAM記述としてコンパイラに推定させるには、 一箇所でのwrite、readにまとめる必要がある。
 良い記述例1(instrselfによりwriteやreadをまとめた)

mem ram[1024]<8>;
sel ram_adrs<10>, wdata<8>;
instrself ram_write(ram_adrs, ram_wdata);
instrself ram_read(ram_adrs);

instruct ram_write par{
	ram[adrs] := wdata;
}
instruct ram_read par{
	rdata := ram[adrs];
}
	...
	ram_write(adrs1, wdata1);
	...
	ram_write(adrs2, wdata2);
	...
	ram_read(adrs3);
	...
	ram_read(adrs4);

 別途RAMモジュールを用意しておいて、これを使用する方法もある。 こちらのほうが確実にBlockRAMへマッピングされる。
 良い記述例2(ram_8x1k.sflを用意しておく)

circuit ram_8x1k
{
	input adrs<10>, din<8>;
	output dout<8>;
	instrin read(adrs);
	instrin write(adrs, din);

	mem cells[1024]<8>;
	reg dout_reg<8>;

	par{
		dout = dout_reg;
	}

	instruct read dout_reg := cells[adrs];
	instruct write cells[adrs] := din;
}
%i "ram_8x1k.h"
...
ram_8x1k ram;
	...
	ram.write(adrs1, wdata1);
	...
	ram.write(adrs2, wdata2);
	...
	ram.read(adrs3);
	...
	ram.read(adrs4);

_
▼ 正しき世界は更地の上に 2012/12/07


水中ステージでスタート直後にハンマーを投げてくるクッパ(?)さん

 マリオとテニスのカートリッジを使用した有名なバグがありますね。 他のゲームから電源を切らずにカートリッジを差し替えた場合、バグる事がある。 これはWRAMに前のゲームの値が入ったままでマリオを実行するため。

 実機でのWRAMはSRAMなので、電源ON時の状態は不定値と思われる。 マリオはこれを検出してゼロで初期化するそうな。 ただし、他のゲームから直接切り替えた場合にそれが検出されず、初期化されない場合がある。 他のいくつかのゲームがハングすることもWRAM初期化がうまく実行されないことが原因のひとつと考えられる。 なので、WRAMと、ついでにVRAMも強制的にゼロフィルするように実装した。

 試しに右の画像は、エミュで memset(wram, 0x00, 0x800); を
for(i=0;i<0x800;i++) wram[i]=rand(); して出てきたステージ。 なぜかステージ値や敵パターン値が初期化されていない。

 まあとにかくゼロフィルすれば問題ないでしょうということで、 SFLでの実装は以下のようにした。 ゲームROMをSDカードから読み込んでいる間にゼロフィル処理を実行する。

stage wram_init {
	reg_wr mem_adrs<11>;
	par{
		wram.write(mem_adrs, 0x00);
		vram.write(mem_adrs, 0x00);
		mem_adrs++;
		if(/&mem_adrs) finish;
	}
}

 ちなみに安定版のnslcore_20120527版では出力されたVerilogHDLのRAM記述がRAM推論されにくい記述だったため、 うまくQuartusIIでマッピングされず、わざわざ初期化機能を持ったメモリを作ったりした。 nslcore_20120915版以降だと正常にマッピングされるようだ。


Copyright(C) pgate1 All Rights Reserved.