The goal of this post is turn a fixed VHDL design into a configurable one. Change a fixed RAM IP into RAM that can be sized, without changing source or logic.
I use michaelkellett's VHDL RAM Memory design before: use RAM design for Altera Cyclone on Vivado and PYNQ.
In that design, the size of the memory , both data size and number of elements, was fixed. 64 elements. 12 bits per element.
I'm going to reuse Michael's design a few times in the future, with different memory sizes.
That's a good opportunity to generalise the VHDL. This post describes what I've done.
The outcome is a RAM IP that can be used identical to the original one. With differently size.
I made the default size identical to what Michael designed. 64 positions of 12 bit data.
In my design, the actual size is 128 positions of 16 bit data.
That's not randomly chosen: in the future this block will hold a sample block from the Zynq XADC.
But first I want to test.
Generalising the VHDL RAM design
Check the code in the original block for the fixed design, where size of data and number of elements are fixed.
I'm generalising 3 of the attributes used in that design:
- the data width: how many bits in each memory element
- the data length: how many elements can the RAM store
- the address length: how many bits needed in the address register to address all elements (e.g: 6 can address 64 distinct elements, 7 for 128, 8 for 256, ...).
The Generic code
You can define defaults in the generic code, and I use the values of Michael's original RAM design, 64 elements of 12 bit data, address needs to be 6 bits.
-- from Micheal Kellett: https://www.element14.com/community/groups/fpga-group/blog/2021/07/31/cheap-cyclone-10 library IEEE; use IEEE.std_logic_1164.all; use IEEE.NUMERIC_STD.all; package mem_inf is component mem_inf_ram is generic ( data_width : integer := 12; -- default 12 bits per element data_length: integer := 64; -- default 64 addressable elements address_width : integer := 6 -- default range fits in 6 bits ); port( we : in STD_LOGIC; en : in STD_LOGIC; clk : in STD_LOGIC; address : in STD_LOGIC_VECTOR(address_width - 1 downto 0); data : in STD_LOGIC_VECTOR(data_width - 1 downto 0); q : out STD_LOGIC_VECTOR(data_width - 1 downto 0) ); end component; end package; library IEEE; use IEEE.std_logic_1164.all; use IEEE.NUMERIC_STD.all; entity mem_inf_ram is generic ( data_width : integer; data_length: integer; address_width : integer ); port( we : in STD_LOGIC; en : in STD_LOGIC; clk : in STD_LOGIC; address : in STD_LOGIC_VECTOR(address_width - 1 downto 0); data : in STD_LOGIC_VECTOR(data_width - 1 downto 0); q : out STD_LOGIC_VECTOR(data_width - 1 downto 0) ); end mem_inf_ram; architecture mem_inf_ram of mem_inf_ram is type ram_type is array (0 to data_length - 1) of std_logic_vector(data_width - 1 downto 0); -- array must be 0 - n, if declared the other way wit downto to match Xilinx example then Quartus fitter fails signal ram : ram_type; begin process(clk) begin if rising_edge(clk) then if en = '1' then if we = '1' then ram(to_integer(unsigned((address)))) <= data; end if; q <= ram(to_integer(unsigned((address)))); end if; end if; end process; end mem_inf_ram;
The Specific implementation code
The block of memory that will be used in the design needs to be 128 elements, 16 bit data, and the address needs to be 7 bits to address all elements.
library IEEE; use IEEE.STD_LOGIC_1164.ALL; use work.mem_inf.all; entity mem_ram_128_16 is port ( we : in STD_LOGIC; en : in STD_LOGIC; clk : in STD_LOGIC; address : in STD_LOGIC_VECTOR(7 - 1 downto 0); data : in STD_LOGIC_VECTOR(16 - 1 downto 0); q : out STD_LOGIC_VECTOR(16 - 1 downto 0) ); end mem_ram_128_16; architecture Behavioral of mem_ram_128_16 is begin mem : mem_inf_ram generic map ( data_width => data'length, data_length => 128, address_width => address'length ) port map ( we => we, en => en, clk => clk, address => address, data => data, q => q ); end Behavioral;
The image below show how this block implements a usable RAM IP, and overrides the defaults:
All of this is vendor independent. The generic design is portable.
I'll use it on a specific design though: a Zynq FPGA. The RAM IP block can be dragged on a Vivado Block Design like any other IP.
Try it on Zynq / PYNQ
Block Design
The final 128x16 RAM IP can be used just like in the original post. You'll find the exact same elements back.
Because the address and data widths are different, you'll see that there are different bus widths used in this design: 7 + 16 = 23.
See the original post for the settings. Just adapt the bus, address and data sizes, in all blocks downstream the AXI Interconnect.
Test on the Zynq
I'm going to use a similar testbed as in the original post. The goal is only to see if the generic design works.
As you can see on the images, the testbed is called memory_ram_dma.ypinb. This is an indication of what my future plans are.
Setup:
Testbed:
Results:
Success. The generic object is ready for use in designs.
This is one of the advantages: the reusable part - that contains the actual logic - is pre-tested.
Vivado project and jupyter notebook attached.