Wer mit Xilinx FPGAs anfängt, der braucht früher oder später mal ein RAM.
Jetzt gibt es in diesen FPGAs zwei Möglichkeiten, ein RAM zu realisieren:
1) als Distributed RAM, bei dem (wertvolle) LUTs als 16x1 Speicherblöcke
zusammengeschaltet werden, oder
2) als BRAM (auch Block-RAM genannt), die je nach FPGA unterschiedlich
große synchrone RAM-Blöcke darstellen
Grundsätzlich wird ein RAM als Array in VHDL so beschrieben:
type speicher is array(0 to (2**Addrbreite)-1) of STD_LOGIC_VECTOR(Wortbreite-1 downto 0);
signal memory : speicher;
Wie kann man aber nun, ausgehend von einer generischen Beschreibung eines RAM-Arrays, steuern welchen RAM-Typ man bekommt?
Eigentlich ist es ganz einfach:
Wenn die Lese-Adresse getaktet ist,
dann gibt es synchrones BRAM.
Es empfiehlt sich aber immer ein Blick in das jeweils aktuelle Handbuch des jeweils verwendeten Synthesizers. Denn der muss aus dieser Beschreibung erkennen, welches RAM denn nun gewollt ist, und die entsprechenden Komponenten instanziieren. Für Xilinx Vivado ist das der Xilinx Vivado Synthesis User Guide UG901. Aber auch im Xilinx Vivado UltraFast Design Methodology Guide UG949 finden sich im Kapitel "RTL Coding Guidelines" beachtenswerte Tipps. Ähnliche Manuals gibt es auch bei den anderen FPGA-Anbietern.
Distributed RAM:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity RAM is
Generic (
Addrbreite : natural := 8; -- Speicherlänge = 2^Addrbreite
Wortbreite : natural := 8
);
Port ( clk : in STD_LOGIC;
A : in STD_LOGIC_VECTOR (Addrbreite-1 downto 0);
Write : in STD_LOGIC;
Din : in STD_LOGIC_VECTOR (Wortbreite-1 downto 0);
Dout : out STD_LOGIC_VECTOR (Wortbreite-1 downto 0)
);
end RAM;
architecture DistributedRAM of RAM is
type speicher is array(0 to (2**Addrbreite)-1) of STD_LOGIC_VECTOR(Wortbreite-1 downto 0);
signal memory : speicher;
begin
process begin
wait until rising_edge(Write);
memory(to_integer(unsigned(A))) <= Din;
end process;
Dout <= memory(to_integer(unsigned(A)));
end DistributedRAM;
Beim Distributed RAM werden die Ausgangsdaten sofort (asynchron) nach dem Ändern der Lese-Adresse ausgegeben.
Die obige Beschreibung ist allerdings doch recht asynchron denn es gibt keinen (namentlichen) Takt, sondern das Schreibsignal wird als Takt verwendet.
Angenehmer ist daher schon die folgende Beschreibung eines Distributed RAMs:
architecture DistributedRAMsync of RAM is
type speicher is array(0 to (2**Addrbreite)-1) of STD_LOGIC_VECTOR(Wortbreite-1 downto 0);
signal memory : speicher;
begin
process begin
wait until rising_edge(CLK);
if (Write='1') then
memory(to_integer(unsigned(A))) <= Din;
end if;
end process;
Dout <= memory(to_integer(unsigned(A)));
end DistributedRAMsync;
Hier wird entsprechend gängiger Designpraxis ein globaler Takt verwendet, das Schreibsignal dient nur als Clock-Enable.
BRAM (Block-RAM):
architecture BlockRAM of RAM is
type speicher is array(0 to (2**Addrbreite)-1) of STD_LOGIC_VECTOR(Wortbreite-1 downto 0);
signal memory : speicher;
begin
process begin
wait until rising_edge(CLK);
if (Write='1') then
memory(to_integer(unsigned(A))) <= Din;
end if;
Dout <= memory(to_integer(unsigned(A)));
end process;
end BlockRAM;
Beim BRAM muß dann noch ein wenig aufgepasst werden, weil die Daten mit
einem Takt Latency (also erst nach der nächsten steigenden Flanke) zurückgegeben werden.
Aber das geht ja schon aus der Beschreibung hervor: wenn gerade jetzt (also irgendwann vor einer Taktflanke) die Leseadresse vorbereitet wird, wird die erst mit der nächsten Taktflanke auf das RAM-Array angewendet. Und erst danach stehen die gelesenen Daten am Dout bereit.
Hier noch eine kleine Implementation eines Dual-Port-RAMs. Eigentlich ist die Beschreibung genauso wie die Vorige, nur wird eine von der Schreib-Adresse unabhängige Lese-Adresse verwendet.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity RAM is
Generic (
Addrbreite : natural := 8; -- Speicherlänge = 2^Addrbreite
Wortbreite : natural := 8
);
Port ( clk : in STD_LOGIC;
Write : in STD_LOGIC;
Awr : in STD_LOGIC_VECTOR (Addrbreite-1 downto 0);
Ard : in STD_LOGIC_VECTOR (Addrbreite-1 downto 0);
Din : in STD_LOGIC_VECTOR (Wortbreite-1 downto 0);
Dout : out STD_LOGIC_VECTOR (Wortbreite-1 downto 0)
);
end RAM;
architecture BlockRAM of RAM is
type speicher is array(0 to (2**Addrbreite)-1) of STD_LOGIC_VECTOR(Wortbreite-1 downto 0);
signal memory : speicher;
begin
process begin
wait until rising_edge(CLK);
if (Write='1') then
memory(to_integer(unsigned(Awr))) <= Din;
end if;
Dout <= memory(to_integer(unsigned(Ard)));
end process;
end BlockRAM;
Anstatt eines STD_LOGIC_VECTOR kann auch der Typ UNSIGNED aus der NUMERIC_STD-Lib verwendet werden. Die Definition des Arrays sieht dann so aus:
:
type speicher is array(0 to (2**Addrbreite)-1) of unsigned(Wortbreite-1 downto 0);
signal memory : speicher;
:
Die Konvertierungen für den Zugriff auf das Array so:
:
memory(to_integer(unsigned(A))) <= unsigned(Din);
:
Dout <= std_logic_vector(memory(to_integer(unsigned(A))));
: