FPGAにおけるRAMの設計

FPGAにおけるRAMの設計

 FPGAでRAMを使う場合、通常はFPGAメーカーの開発環境で用意しているIPを使います。理由は、時間的に効率が良い。というのもありますが、メーカー製のIPを使うと、RAMのメモリにあたるDFFの配置をFPGA内の専用ハードウェアに割り当ててくれるからです。

 Intel製やXilinx製のFPGAであれば、FPGAデバイスの中にRAMをハードウェアで内蔵しています。このRAMはブロックで管理されていて、ブロック毎にデータビットの幅やメモリ量を可変することができます。内蔵のRAMハードウェアについては、FPGAによって構成が違うので、使いたいFPGAのデータシートを読んで構造を理解しておくと、効率よくメモリを構成することができます。

 また、メーカー製IPなので最適化されており、高速なメモリ動作を望めますし、入出力のクロックを別にできるとか、大規模なメモリを作れる。さらに、入力と出力のビット幅を変更することもできます。

 このように、通常はメーカー製のIPを使用します。しかし、IPを使う事によるデメリットもあります。

  • 小さいメモリを作っても、最低のメモリブロックを消費してしまう。
  • メモリブロックの場所はハード的に決まっているので、配置配線が厳しくなることがある。
  • FPGAを別の製品に変更すると、IPを設計し直す必要がある。
  • IPを設計し直すとIPのIOピンが変わったり、タイミングが変わることがある。
  • メーカー製IPがバージョンによって仕様変更することがある。

このようなデメリットもあります。当然、メリットとデメリットを考えてメーカー製IPを使用するかどうかを検討します。メーカー製IPを使用しない。と決めた時には、自分でメモリを設計することになります。

 自分でメモリを設計するにあたり、入力と出力のクロックが違う場合は素直にあきらめてメーカー製IPを使用するようにしましょう。入出力のクロックが同期していれば何とかなる可能性もありますが、非同期の時などはアドレス管理で失敗することが多く、リカバリーもなかなか難しくなります。

 自分で作る場合には、小規模のメモリでかつ入力と出力のクロックが同じ場合と限定するのが良いと思います。

 市販のデバイスでのメモリはSRAMが一般的だと思いますが、FPGA内部にRAMを作る場合には、SRAMを模擬する必要も無いので、入力のデータと出力のデータのバスは別にした方が楽です。

 サンプルとして設計したメモリの構成を次に示します。今回は16ビット幅で8ワード分のメモリにしました。

  VHDLのソースコードを次に示します。

--  TITLE:					"RAM_16b_8d.vhd"
--  MODULE NAME:			
--  PROJECT CODE:			
--  AUTHOR:					 (xxxx@nakaharagiken.com)
--  CREATION DATE:			2020.12.31
--  SOURCE:            		
--  LICENSE:           		Copyright (c) 2020 nakaharagiken.com
--  DESCRIPTION:            16bit 8deep のRAM
--  NOTES TO USER:			
--  SOURCE CONTROL:			
--  REVISION HISTORY:  	    v0.0	2020.12.31	
--
--
--
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
use IEEE.std_logic_arith.all;
use ieee.numeric_std.all;

entity RAM_16b_8d is
port (
	clock:				in		std_logic;						-- system clock
	nreset:				in		std_logic;						-- asynchronous reset 							____reset___|~~~normal~~~
	address:			in		std_logic_vector(2 downto 0);	-- アドレス
	datain:				in		std_logic_vector(15 downto 0);	-- 入力データ
	dataen:				in		std_logic;						-- データイネーブル								____|~|___________________
	dataout:			out		std_logic_vector(15 downto 0);	-- 出力データ
	datareq:			in		std_logic						-- 出力データリクエスト							____|~~request~|__________
);
end entity RAM_16b_8d;

architecture rtl of RAM_16b_8d is

---------------------------------------------------------------
-- SIGNALS
---------------------------------------------------------------
-- 配列の宣言
subtype WORD_TYPE is std_logic_vector(15 downto 0);							-- 配列の幅
type    WORD_ELEM is array(0 to 7) of WORD_TYPE;							-- 配列の要素数
signal  rMemory:    WORD_ELEM;  											-- 信号の宣言

signal	routput:	std_logic_vector(15 downto 0) := (others => '0');		-- 出力データ

begin

-----------------------------------------------------------
--
-- RAM
--
-----------------------------------------------------------
process (clock,nreset) begin
	if nreset = '0' then
        for i in 0 to 7 loop
            rMemory(i) <= (others => '0');
        end loop;
	elsif clock' event and clock = '1' then
		if (dataen = '1') then									-- ライト
			rMemory(CONV_INTEGER(address)) <= datain;
		end if;
		if (datareq = '1') then									-- リード
			routput <= rMemory(CONV_INTEGER(address));
		end if;
	end	if;
end process;

dataout <= routput;


end rtl;

 メモリ用にDFFを16×8個用意する必要があります。メモリのように同じデータ幅で数個を用意するような場合には、配列を使うのが便利です。

 配列の宣言方法は、本ブログで解説していますので参考にしてください。

 39行目~41行目で配列の宣言をしています。よく、配列の幅と深さの指定方法がわからない。という事がありますが、コメントを参照してください。まず、配列の幅=データビット幅をsubtypeで型宣言しておきます。次にそれをいくつ必要なのか(=アドレスの数)を指定します。

 52行目からがRAMの設計になりますが、54行目でiを使用してループしています。配列はこのようにiの変数でループを回して記述することができます。よく勘違いする方がいますが、54行目~56行目のループはコンパイラへの指示です。実際にこのようなループするハードウェアが作られる訳ではありません。コンパイラがi=0~7になるまでループして、rMemory(i) <= (others => ‘0’);を作ります。つまり、以下のような記述と同じになります。

rMemory(0) <= (others => '0');
rMemory(1) <= (others => '0');
rMemory(2) <= (others => '0');
rMemory(3) <= (others => '0');
rMemory(4) <= (others => '0');
rMemory(5) <= (others => '0');
rMemory(6) <= (others => '0');
rMemory(7) <= (others => '0');

 このような8行をコンパイラが代わって記述してくれる。と理解してください。

 VHDL特有の記述が59行目と62行目にあります。これは配列にアクセスするための型変換をしています。address はstd_logic_vector(2 downto 0)という型ですが、配列のアクセスはInteger型である必要があります。したがって、std_logic_vector -> Integerへの型変換をしています。

 一般なRAMと同じようにアドレスが確定していれば、出力しても良いので、61行目のdatareqによるラッチは必要ないかも知れませんが、用途によってアレンジしてみてください。

 次にテストベンチも載せておきます。

--  TITLE:					"TB_RAM_16b_8d.vhd"
--  MODULE NAME:			Test Bench
--  PROJECT CODE:			
--  AUTHOR:					
--  CREATION DATE:			
--  REVISION HISTORY:  	    
--  SOURCE:            		
--  LICENSE:           		Copyright (c) 2020 nakaharagiken.com
--  DESCRIPTION:            
--  NOTES TO USER:      	
--  SOURCE CONTROL:  	    RAM_16b_8dのテストベンチ
--
--
--
--
--
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;


entity TB_RAM_16b_8d is
end TB_RAM_16b_8d;

architecture sim of TB_RAM_16b_8d is


-- 回路記述部
-------------------------------------------------------------
component RAM_16b_8d
port (
	clock:				in		std_logic;						-- system clock
	nreset:				in		std_logic;						-- asynchronous reset 							____reset___|~~~normal~~~
	address:			in		std_logic_vector(2 downto 0);	-- アドレス
	datain:				in		std_logic_vector(15 downto 0);	-- 入力データ
	dataen:				in		std_logic;						-- データイネーブル								____|~|___________________
	dataout:			out		std_logic_vector(15 downto 0);	-- 出力データ
	datareq:			in		std_logic						-- 出力データリクエスト							____|~~request~|__________
);
end component;


constant SYSCLK_PERIOD : time := 100 ns; -- 10MHz


-- 信号の宣言
signal	rclock:				std_logic;
signal	rnreset:			std_logic;							--	__reset___|~~~normal~~~
signal	rdatain:			std_logic_vector(15 downto 0);	-- 入力データ
signal	rdataen:			std_logic;						-- データイネーブル								____|~|___________________
signal	raddress:			std_logic_vector(2 downto 0);	-- アドレス
signal	wdataout:			std_logic_vector(15 downto 0);	-- 出力データ
signal	rdatareq:			std_logic;						-- 出力データリクエスト							____|~~request~|__________

signal	j:					integer range 0 to 7;

begin

inst_RAM_16b_8d : RAM_16b_8d
port map(
	clock				=>	rclock,					--	in		std_logic;						-- system clock
	nreset				=>	rnreset,				--	in		std_logic;						-- asynchronous reset 					____reset___|~~~normal~~~
	address				=>	raddress,				--	in		std_logic_vector(2 downto 0);	-- アドレス
	datain				=>	rdatain,				--	in		std_logic_vector(15 downto 0);	-- 入力データ
	dataen				=>	rdataen,				--	in		std_logic;						-- データイネーブル								____|~|___________________
	dataout				=>	wdataout,				--	out		std_logic_vector(15 downto 0);	-- 出力データ
	datareq				=>	rdatareq				--	in		std_logic						-- 出力データリクエスト							____|~~request~|__________
);



-- Clock Driver
process begin
	rclock <= '1';
	wait for SYSCLK_PERIOD / 2;
	rclock <= '0';
	wait for SYSCLK_PERIOD / 2;
end process;

-- reset
process begin
	rnreset <= '0';
	wait for (SYSCLK_PERIOD * 2);
	rnreset <= '1';
	wait;
end process;


process begin

	j <= 0;
	rdataen <= '0';
	rdatareq <= '0';
	rdatain <= (others => '0');
	
	wait for (SYSCLK_PERIOD * 50);
	wait for (SYSCLK_PERIOD / 2);

	for i in 0 to 100 loop
		wait for (SYSCLK_PERIOD / 4);
		raddress <= CONV_std_logic_vector(j,3);
		rdatain <= CONV_std_logic_vector(i,16);
		rdataen <= '1';
		wait for (SYSCLK_PERIOD);
		rdataen <= '0';
		wait for (SYSCLK_PERIOD * 10);
		wait for (SYSCLK_PERIOD / 4);

		rdatareq <= '1';
		wait for (SYSCLK_PERIOD);
		rdatareq <= '0';
		if (j < 7) then
			j <= j+1;
		else
			j <= 0;
		end if;
	end loop;
	wait;

end process;
end sim;

 このテストベンチではループや変数を積極的に使ってメモリへのアドレスとデータを自動生成しています。

 最後に、シミュレートの結果です。アドレスへとrdataen、rdatareqのタイミングから、wdataout出力のデータが一致しているのがわかります。