Organizing the Chaos of FPGA RAM Initialization: A Vendor- and Language-Agnostic Approach

FPGA tools are infamous for annoying inconsistencies. One of the most chaotic inconsistencies is initialization of on-chip RAMs, where every tool (and even each language within the same tool) uses different methods. Many designers rely on vendor-specific RAM initialization files (e.g., Intel MIF) combined with vendor-provided RAM IP. While that approach works for some, it becomes limiting when you need custom RAM IP or portable code that supports multiple FPGA vendors.

Ideally, designers should be able to use a custom RAM initialization file format with a corresponding RTL RAM module, written in any language, that works across all synthesis tools and simulators. In this article, I share the results of an extensive (and quite painful) exploration to achieve this goal. The solution has been tested with a customizable initialization format and RAM modules defined in SystemVerilog, VHDL-93, and VHDL-2008, in both Vivado 2024.1 and Quartus Prime Pro 23.1.

All code examples are available on my SystemVerilog tutorial and VHDL tutorial.

Initial Attempt

My initial plan was to create a proof-of-concept by designing a custom RAM initialization file and writing a RAM module that parsed the file to initialize an array. In principle, this would let designers define RAM initialization however they wanted.

Although file I/O obviously cannot be synthesized, I assumed that if the file I/O occurred during elaboration, synthesis tools would allow me to parse a file and store its contents in an array using a standard RAM-inference template. After all, I had previously done something similar using functions during elaboration to generate custom lookup tables.

Unfortunately, despite trying every trick I could think of, most synthesis tools simply do not support arbitrary file I/O, even during elaboration.

Partial Solution: RAM Initialization from .mem Files

After abandoning my initial attempt at general file I/O during elaboration, I realized that if there was some file format that could be parsed by Vivado and Quartus, I could achieve my same goals by writing a script to convert a custom initialization format into the supported format.

After some experimentation, I discovered that SystemVerilog’s $readmemh() and $readmemb() system tasks are supported by both Vivado and Quartus. These calls automatically parse a specified .mem file (in hex or binary) and load the results into a provided array. Although the SystemVerilog standard defines .mem with a variety of features, many tools only support a subset. For simplicity, I tested only the most basic functionality: a single value on each line of the file, corresponding to the address line_number-1.

To implement this functionality, I extended the portable simple dual-port (SDP) RAM template (from this previous article):

// Greg Stitt
// StittHub (www.stitt-hub.com)

module ram_sdp_init_file #(
    parameter int DATA_WIDTH = 16,
    parameter int ADDR_WIDTH = 10,
    parameter bit REG_RD_DATA = 1'b0,
    parameter bit WRITE_FIRST = 1'b0,
    parameter string STYLE = "",
    parameter string INIT = ""
) (
    input  logic                  clk,
    input  logic                  rd_en,
    input  logic [ADDR_WIDTH-1:0] rd_addr,
    output logic [DATA_WIDTH-1:0] rd_data,
    input  logic                  wr_en,
    input  logic [ADDR_WIDTH-1:0] wr_addr,
    input  logic [DATA_WIDTH-1:0] wr_data
);
    // Use a Vivado workaround to enable string parameter for ram_style.
    localparam int MAX_STYLE_LEN = 16;
    typedef logic [MAX_STYLE_LEN*8-1:0] string_as_logic_t;
    localparam logic [MAX_STYLE_LEN*8-1:0] MEM_STYLE = string_as_logic_t'(STYLE);

    // Specify ram_style for Vivado, and ramstyle for Quartus.
    (* ram_style = MEM_STYLE, ramstyle = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
    logic [DATA_WIDTH-1:0] rd_data_ram;

    // Read the contents of the INIT file into the RAM array.
    initial begin
        $readmemh(INIT, ram);
    end

    always_ff @(posedge clk) begin
        if (wr_en) ram[wr_addr] <= wr_data;
        if (rd_en) rd_data_ram <= ram[rd_addr];
    end

    if (WRITE_FIRST) begin : l_write_first
        logic bypass_valid_r = 1'b0;
        logic [DATA_WIDTH-1:0] bypass_data_r;

        always_ff @(posedge clk) begin
            if (rd_en && wr_en) bypass_data_r <= wr_data;
            if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
        end

        if (REG_RD_DATA) begin : l_reg_rd_data
            always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
        end else begin : l_no_reg_rd_data
            assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
        end
    end else begin : l_read_first
        if (REG_RD_DATA) begin : l_reg_rd_data
            always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
        end else begin : l_no_reg_rd_data
            assign rd_data = rd_data_ram;
        end
    end
endmodule

The extensions include an INIT string parameter that specifies the path to the .mem file. Lines 30–32 add an initial block that performs the $readmemh() call, which takes the file name and an array as arguments. To initialize the RAM contents, the code uses the ram array.

To verify that this worked, I created a top-level module that instantiates the ram_sdp_init_file module with a specific .mem file:

// Greg Stitt
// StittHub (www.stitt-hub.com)

module ram_init_file_demo #(
    parameter int DATA_WIDTH = 8,
    parameter int ADDR_WIDTH = 6,
    parameter bit REG_RD_DATA = 1'b0,
    parameter bit WRITE_FIRST = 1'b0,
    parameter string STYLE = "",

    // IMPORTANT: Make sure this path is relative to the working directory of your sim/synth project.
    // This can require some experimentation, with some tools not even reporting a
    // warning if the file doesn't exist at the right path.
    parameter string INIT = "ram_init_demo.mem"   
) (
    input  logic                  clk,
    input  logic                  rd_en,
    input  logic [ADDR_WIDTH-1:0] rd_addr,
    output logic [DATA_WIDTH-1:0] rd_data,
    input  logic                  wr_en,
    input  logic [ADDR_WIDTH-1:0] wr_addr,
    input  logic [DATA_WIDTH-1:0] wr_data
);
    ram_sdp_init_file #(
        .DATA_WIDTH (DATA_WIDTH),
        .ADDR_WIDTH (ADDR_WIDTH),
        .REG_RD_DATA(REG_RD_DATA),
        .WRITE_FIRST(WRITE_FIRST),
        .STYLE      (STYLE),
        .INIT       (INIT)
    ) DUT (
        .clk    (clk),
        .rd_en  (rd_en),
        .rd_addr(rd_addr),
        .rd_data(rd_data),
        .wr_en  (wr_en),
        .wr_addr(wr_addr),
        .wr_data(wr_data)
    );

endmodule

I then synthesized this module in both Vivado and Quartus and ran post-synthesis simulations using the included testbench ram_init_file_demo_tb, with POST_SYNTH_SIM set to 1. The testbench first verifies the initialization by reading all addresses and ensuring that the data matches the .mem contents, which worked correctly in both Vivado and Quartus.

Note that for this testbench to work, you first need to run the script described in the next section. Additionally, unless you are using the Vivado simulator, you will need to compile the simulation libraries for your target FPGA and link them into your chosen simulator. That process can be cumbersome, so it is skipped here, but it will be the topic of a future article.

At this point, we have a SystemVerilog solution that works with .mem files in both Quartus and Vivado. Two tasks remain: 1) get this working in VHDL, and 2) support custom initialization files.

When I shifted my efforts to replicating the SystemVerilog functionality in VHDL, I ran into significant roadblocks. To my knowledge, VHDL has no built-in functionality equivalent to $readmemh. File I/O support is limited, and I could not find a method supported by both Vivado and Quartus. After seeing that the official RAM initialization inference templates from AMD and Intel differ significantly, I concluded that I would need an alternative approach to achieve my original goals.

Using Custom RAM Initialization File Formats

The previous section presented a partial solution that works for SystemVerilog using .mem files. We’ll address the VHDL issue later, but first, let’s explore how to create a custom RAM initialization file format.

Although .mem files enable RAM initialization, they are not always convenient. For example, an application might need to encode a specific pattern in one address range, fill another range with a fixed value, use random numbers in a different range, and still specify individual values outside of any pattern.

To illustrate what such a custom initialization file might look like, I created an example in YAML format:

# ram_init_demo.yaml
width: 8
depth: 64
default: 0x00

patterns:
  # Fill addresses 0 to 7 with 0xFF
  - range: [0x00, 0x07]
    fill: 0xFF

  # Fill addresses 8 to 15 with sequence 0x10, 0x12, 0x14, etc.
  - range: [0x08, 0x0F]
    seq_start: 0x10 # Starting sequence value
    seq_step: 2     # Amount between each sequence value (i.e. step)

  # Fill addresses 16 to 23 with 0xAA, 0x55, 0x77, 0xAA, 0x55, 0x77, etc.
  - range: [0x10, 0x17]
    repeat: [0xAA, 0x55, 0x77]    

  # Fill addresses 24 to 31 with random values
  - range: [0x18, 0x1f]
    random:
      seed: 12345  # optional
      min: 0x10    # optional
      max: 0x1F    # optional

# Initialize specific addresses with custom values
values:  
  0x20: 0xBC
  0x21: 0xA0
  0x22: 0x55  

This format includes required width and depth keys for the memory, along with an optional default key that specifies the values of any addresses not explicitly defined. The patterns section supports four types of patterns:

  1. Fill pattern: Assigns a specified value to all addresses within a given range.
  2. Sequence pattern: Applies a sequence to a specified address range, where seq_start defines the starting value and seq_step defines the increment between values. In the example, the sequence starts at 0x10 and increments by 2 each time.
  3. Repeat pattern: Repeats a list of values throughout the specified range, cycling back to the start of the list after reaching the end.
  4. Random pattern: Assigns random values to a range, with optional min/max constraints and an optional seed for deterministic results.

Finally, as shown on lines 28–31, the values section allows specific values to be assigned to individual addresses.

Because most synthesis tools cannot parse arbitrary code, we need a way to convert this custom YAML format into a .mem file. To accomplish this, I created a Python script, create_mem_file.py, which takes the YAML input and generates the required .mem file. The script also provides a variety of configurable options.

usage: create_mem_file.py [-h] [--mem MEM] [--sv SV] [--vhdl VHDL] [--pkg PKG]
[--array ARRAY] [-w]
yaml_file

Generate RAM initialization and HDL packages from YAML

positional arguments:
yaml_file

optional arguments:
-h, --help show this help message and exit
--mem MEM Memory output file (.mem format)
--sv SV SystemVerilog output file
--vhdl VHDL VHDL output file
--pkg PKG Package name (default: YAML filename + _pkg)
--array ARRAY Array name (default: YAML filename + _array)
-w, --warning Enable overwrite warnings

For now, we’ll ignore most of the flags and simply provide the YAML input and .mem output file names using:

./create_mem_file.py ram_init_demo.yaml --mem ram_init_demo.mem

If we open the resulting ram_init_demo.mem file, we see the expected values:

FF
FF
FF
FF
FF
FF
FF
FF
10
12
14
16
18
1A
1C
1E
AA
55
77
etc.

We can see the 0xFF fill pattern first, followed by the sequence pattern, then the start of the repeat pattern, and so on. This .mem file can now be provided as the INIT parameter to the ram_sdp_init_file module, giving us a portable solution—though it is still limited to SystemVerilog.

Please note that the create_mem_file.py script has not been thoroughly tested for error handling in the YAML file (e.g., invalid ranges). It was intended as a proof-of-concept for creating your own custom RAM initialization format, rather than as a fully mature tool. Feel free to modify and extend it to suit your own needs.

A Complete Solution

The solution in the previous section achieved all of the original goals, except for supporting VHDL. It also has one particularly annoying limitation: the path of the initialization file must be specified relative to the project’s working directory in whichever tool you are using. While this may not sound difficult, figuring out the correct directory often requires a lot of trial and error. I had originally intended to print a meaningful error message showing the working directory when the file wasn’t found, but some synthesis tools simply ignored this. Others did not even allow checking whether a file existed. As a result, you may need to manually try multiple paths until it works. Even worse, Vivado still synthesized the design when the file didn’t exist, producing an uninitialized RAM. This behavior can be dangerous, so if you decide to use this approach, make sure to run post-synthesis simulations to verify that the initialization file was included.

After dealing with the frustration of juggling different paths for different tools, I realized there is a more reliable and portable way to achieve the original goal. We do not need to specify a file at all in the RTL code, which avoids all inconsistent file I/O behaviors during synthesis. Instead, we can generate an array of constants from our custom YAML format, place that array in a package, and use it to directly initialize the RAM via a module parameter.

The following code shows how we adapted the SDP RAM to support initialization via an array parameter in SystemVerilog:

// Greg Stitt
// StittHub (www.stitt-hub.com)

module ram_sdp_init_array #(
    parameter int DATA_WIDTH = 16,
    parameter int ADDR_WIDTH = 10,
    parameter bit REG_RD_DATA = 1'b0,
    parameter bit WRITE_FIRST = 1'b0,
    parameter string STYLE = "",
    parameter logic [DATA_WIDTH-1:0] INIT[2**ADDR_WIDTH] = '{default: 'x}
) (
    input  logic                  clk,
    input  logic                  rd_en,
    input  logic [ADDR_WIDTH-1:0] rd_addr,
    output logic [DATA_WIDTH-1:0] rd_data,
    input  logic                  wr_en,
    input  logic [ADDR_WIDTH-1:0] wr_addr,
    input  logic [DATA_WIDTH-1:0] wr_data
);
    // Use a Vivado workaround to enable string parameters for ram_style.
    localparam int MAX_STYLE_LEN = 16;
    typedef logic [MAX_STYLE_LEN*8-1:0] string_as_logic_t;
    localparam logic [MAX_STYLE_LEN*8-1:0] MEM_STYLE = string_as_logic_t'(STYLE);

    // Specify ram_style for Vivado, and ramstyle for Quartus.
    (* ram_style = MEM_STYLE, ramstyle = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH] = INIT;
    logic [DATA_WIDTH-1:0] rd_data_ram;

    always_ff @(posedge clk) begin
        if (wr_en) ram[wr_addr] <= wr_data;
        if (rd_en) rd_data_ram <= ram[rd_addr];
    end

    if (WRITE_FIRST) begin : l_write_first
        logic bypass_valid_r = 1'b0;
        logic [DATA_WIDTH-1:0] bypass_data_r;

        always_ff @(posedge clk) begin
            if (rd_en && wr_en) bypass_data_r <= wr_data;
            if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
        end

        if (REG_RD_DATA) begin : l_reg_rd_data
            always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
        end else begin : l_no_reg_rd_data
            assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
        end
    end else begin : l_read_first
        if (REG_RD_DATA) begin : l_reg_rd_data
            always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
        end else begin : l_no_reg_rd_data
            assign rd_data = rd_data_ram;
        end
    end
endmodule

On line 10, we change the INIT parameter from a string specifying a file to an array with a size equivalent to the RAM contents. I provide an undefined default value in case the user does not want to specify an initializer. In this case, the simulation will display undefined values until data has been written to the RAM. Finally, on line 26, the ram array that gets synthesized into actual RAM is initialized with the INIT parameter.

Of course, we still need to provide the array when instantiating this module. This can be done using any legal SystemVerilog syntax. Since we started with a Python script that generated a .mem file, we can now use the same script to create a SystemVerilog package containing the corresponding array.

By running the create_mem_file.py script with the --sv flag, we can specify the filename of the generated SystemVerilog package file:

./create_mem_file.py ram_init_demo.yaml --sv ram_init_demo_pkg.sv

which gives us the following code:

package ram_init_demo_pkg;

    localparam logic [7:0] RAM_INIT_DEMO_ARRAY[64] =
    '{
        0: 'hFF,
        1: 'hFF,
        2: 'hFF,
        3: 'hFF,
        4: 'hFF,
        5: 'hFF,
        6: 'hFF,
        7: 'hFF,
        8: 'h10,
        9: 'h12,
        10: 'h14,
        11: 'h16,
        12: 'h18,
        13: 'h1A,
        14: 'h1C,
        15: 'h1E,
        16: 'hAA,
        17: 'h55,
        18: 'h77,
        19: 'hAA,
        20: 'h55,
        21: 'h77,
        22: 'hAA,
        23: 'h55,
        24: 'h1D,
        25: 'h10,
        26: 'h19,
        27: 'h1B,
        28: 'h16,
        29: 'h18,
        30: 'h1D,
        31: 'h15,
        32: 'hBC,
        33: 'hA0,
        34: 'h55,
        default: 'h0
    };

endpackage

We can now create a top-level module that instantiates the SDP RAM using this localparam array:

// Greg Stitt
// StittHub (www.stitt-hub.com)

module ram_init_array_demo
    import ram_init_demo_pkg::RAM_INIT_DEMO_ARRAY;
#(
    parameter int DATA_WIDTH = 8,
    parameter int ADDR_WIDTH = 6,
    parameter bit REG_RD_DATA = 1'b0,
    parameter bit WRITE_FIRST = 1'b0,
    parameter string STYLE = ""    
) (
    input  logic                  clk,
    input  logic                  rd_en,
    input  logic [ADDR_WIDTH-1:0] rd_addr,
    output logic [DATA_WIDTH-1:0] rd_data,
    input  logic                  wr_en,
    input  logic [ADDR_WIDTH-1:0] wr_addr,
    input  logic [DATA_WIDTH-1:0] wr_data
);

    ram_sdp_init_array #(
        .DATA_WIDTH (DATA_WIDTH),
        .ADDR_WIDTH (ADDR_WIDTH),
        .REG_RD_DATA(REG_RD_DATA),
        .WRITE_FIRST(WRITE_FIRST),
        .STYLE      (STYLE),
        .INIT       (ram_init_demo_pkg::RAM_INIT_DEMO_ARRAY)
    ) DUT (
        .clk    (clk),
        .rd_en  (rd_en),
        .rd_addr(rd_addr),
        .rd_data(rd_data),
        .wr_en  (wr_en),
        .wr_addr(wr_addr),
        .wr_data(wr_data)
    );

endmodule

Notice that on line 28, we simply provide the array from the package to the INIT parameter.

We tested this approach in both Vivado and Quartus using post-synthesis simulations, and it worked in both tools. This brings us closer to our goal, though we still need to implement the same functionality for VHDL. Fortunately, the requirements for VHDL are now much simpler.

We first tested VHDL-93 using the following SDP RAM code:

-- Greg Stitt
-- StittHub (www.stitt-hub.com)

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ram_sdp_init_array is
    generic (
        DATA_WIDTH  : positive         := 16;
        ADDR_WIDTH  : positive         := 10;
        REG_RD_DATA : boolean          := false;
        WRITE_FIRST : boolean          := false;
        STYLE       : string           := "";
        INIT        : std_logic_vector := ""
        );
    port (
        clk     : in  std_logic;
        rd_en   : in  std_logic;
        rd_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        rd_data : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_en   : in  std_logic;
        wr_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        wr_data : in  std_logic_vector(DATA_WIDTH-1 downto 0)
        );
end ram_sdp_init_array;


architecture default_arch of ram_sdp_init_array is

    type ram_t is array (0 to 2**ADDR_WIDTH-1) of std_logic_vector(DATA_WIDTH-1 downto 0);

    -- Convert the INIT SLV back into a normal array.
    function to_array return ram_t is
        variable r : ram_t;
    begin
        r := (others => (others => 'U'));

        if INIT = "" then
            return r;
        end if;
            
        for i in 0 to 2**ADDR_WIDTH-1 loop
            r(i) := INIT((i+1)*DATA_WIDTH-1 downto i*DATA_WIDTH);
        end loop;

        return r;
    end function;

    signal ram                : ram_t := to_array;
    -- Tell Quartus what type of RAM to use
    attribute ramstyle        : string;
    attribute ramstyle of ram : signal is STYLE;

    -- Tell Vivado what type of RAM to use
    attribute ram_style        : string;
    attribute ram_style of ram : signal is STYLE;

    signal rd_data_ram : std_logic_vector(DATA_WIDTH-1 downto 0);
begin
    process (clk)
    begin
        if (rising_edge(clk)) then
            if (wr_en = '1') then
                ram(to_integer(unsigned(wr_addr))) <= wr_data;
            end if;

            if (rd_en = '1') then
                rd_data_ram <= ram(to_integer(unsigned(rd_addr)));
            end if;
        end if;
    end process;

    l_write_first : if (WRITE_FIRST) generate
        signal bypass_data_r  : std_logic_vector(DATA_WIDTH-1 downto 0);
        signal bypass_valid_r : std_logic := '0';
    begin
        process (clk)
        begin
            if (rising_edge(clk)) then
                if (rd_en = '1') then
                    bypass_valid_r <= '0';
                    if (wr_en = '1') then
                        bypass_data_r <= wr_data;
                        if (rd_addr = wr_addr) then
                            bypass_valid_r <= '1';
                        end if;
                    end if;
                end if;
            end if;
        end process;

        l_reg_rd_data : if (REG_RD_DATA) generate
            process (clk)
            begin
                if (rising_edge(clk)) then
                    if (rd_en = '1') then
                        if (bypass_valid_r = '1') then
                            rd_data <= bypass_data_r;
                        else
                            rd_data <= rd_data_ram;
                        end if;
                    end if;
                end if;
            end process;
        end generate;

        l_no_reg_rd_data : if (not REG_RD_DATA) generate
            rd_data <= bypass_data_r when bypass_valid_r = '1' else rd_data_ram;
        end generate;
    end generate;

    l_read_first : if (not WRITE_FIRST) generate
    begin
        l_reg_rd_data : if (REG_RD_DATA) generate
            process (clk)
            begin
                if (rising_edge(clk)) then
                    if (rd_en = '1') then
                        rd_data <= rd_data_ram;
                    end if;
                end if;
            end process;
        end generate;

        l_no_reg_rd_data : if (not REG_RD_DATA) generate
            rd_data <= rd_data_ram;
        end generate;
    end generate;
end default_arch;

You might notice one unusual characteristic of this entity. Specifically, it uses an unconstrained std_logic_vector for the INIT array. This is due to one of the biggest limitations of VHDL-93: you cannot have an array of an unconstrained type. Normally, you would need to define a type for the array in a separate package so you can use it within the generic region. By itself, this is not a major issue, but without the generic packages introduced in VHDL-2008, it becomes impossible to instantiate RAMs with different element widths.

To work around this limitation, a common approach is to represent the array as a single large std_logic_vector and then convert it back into an array internally. One might be tempted to use std_logic_vector(DATA_WIDTH-1 downto 0) as the type of the INIT generic, but VHDL-93 also prohibits defining generics in terms of other generics—hence the need for an unconstrained array. Finally, I defaulted INIT to an empty vector to allow detection of whether a value was provided in the generic map.

On lines 33–48, a function converts the std_logic_vector back into the desired array format, which is then used to initialize the ram array. While this approach works in VHDL-93, it is clearly non-ideal.

Next, we use the Python script again to generate a VHDL package containing the array, while also creating a std_logic_vector version of the array:

./create_mem_file.py ram_init_demo.yaml --vhdl ram_init_demo_pkg.vhd
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package ram_init_demo_pkg is

    constant C_NUM_ELEMENTS  : integer := 64;
    constant C_ELEMENT_WIDTH : integer := 8;
    constant C_TOTAL_BITS    : integer := C_NUM_ELEMENTS*C_ELEMENT_WIDTH;
    type ram_init_demo_array_t is array(0 to C_NUM_ELEMENTS-1) of std_logic_vector(C_ELEMENT_WIDTH-1 downto 0);

    -- Automatically generated constant array
    constant RAM_INIT_DEMO_ARRAY : ram_init_demo_array_t :=
    (
        0 to 7 => x"FF",
        8 => x"10",
        9 => x"12",
        10 => x"14",
        11 => x"16",
        12 => x"18",
        13 => x"1A",
        14 => x"1C",
        15 => x"1E",
        16 => x"AA",
        17 => x"55",
        18 => x"77",
        19 => x"AA",
        20 => x"55",
        21 => x"77",
        22 => x"AA",
        23 => x"55",
        24 => x"1D",
        25 => x"10",
        26 => x"19",
        27 => x"1B",
        28 => x"16",
        29 => x"18",
        30 => x"1D",
        31 => x"15",
        32 => x"BC",
        33 => x"A0",
        34 => x"55",
        35 to 63 => x"00"
    );

    -- Convert array to SLV (for VHDL-93)
    function to_slv(a : ram_init_demo_array_t) return std_logic_vector;

    constant RAM_INIT_DEMO_ARRAY_SLV : std_logic_vector(C_TOTAL_BITS-1 downto 0) := to_slv(RAM_INIT_DEMO_ARRAY);

end package ram_init_demo_pkg;

package body ram_init_demo_pkg is

    function to_slv(a : ram_init_demo_array_t) return std_logic_vector is
        variable result : std_logic_vector(C_TOTAL_BITS-1 downto 0);
    begin
        for i in a'range loop
            result((i+1)*C_ELEMENT_WIDTH-1 downto i*C_ELEMENT_WIDTH) := a(i);
        end loop;
        return result;
    end function;

end package body ram_init_demo_pkg;

Finally, we create a top-level entity that instantiates ram_sdp_init_array, passing RAM_INIT_DEMO_ARRAY_SLV as the INIT parameter:

-- Greg Stitt
-- StittHub (www.stitt-hub.com)

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

-- Include to get initializer array.
use work.ram_init_demo_pkg.all;

entity ram_init_array_demo is
    generic (
        DATA_WIDTH  : positive := 8;
        ADDR_WIDTH  : positive := 6;
        REG_RD_DATA : boolean  := false;
        WRITE_FIRST : boolean  := false;
        STYLE       : string   := ""
        );
    port (
        clk     : in  std_logic;
        rd_en   : in  std_logic;
        rd_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        rd_data : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_en   : in  std_logic;
        wr_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        wr_data : in  std_logic_vector(DATA_WIDTH-1 downto 0)
        );
end ram_init_array_demo;

architecture default_arch of ram_init_array_demo is

begin

    U_TOP : entity work.ram_sdp_init_array
        generic map (
            DATA_WIDTH  => DATA_WIDTH,
            ADDR_WIDTH  => ADDR_WIDTH,
            REG_RD_DATA => REG_RD_DATA,
            WRITE_FIRST => WRITE_FIRST,
            STYLE       => STYLE,
            INIT        => RAM_INIT_DEMO_ARRAY_SLV
            )
        port map (clk     => clk,
                  rd_en   => rd_en,
                  rd_addr => rd_addr,
                  rd_data => rd_data,
                  wr_en   => wr_en,
                  wr_addr => wr_addr,
                  wr_data => wr_data);

end architecture;

Again, we tested this example in both Vivado and Quartus using post-synthesis simulations, and it worked in both tools. However, having spent too much of my career manually converting arrays back and forth between std_logic_vector types, I wanted to provide a more elegant solution using VHDL-2008.

With VHDL-2008, we can directly use the RAM_INIT_DEMO_ARRAY array in ram_init_demo_pkg.vhd. However, since VHDL requires explicit type conversions—even when the types are defined identically—we need an additional package. To address this, we created a new ram_sdp_init_array_2008.vhd file:

-- Greg Stitt
-- StittHub (www.stitt-hub.com)

library ieee;
use ieee.std_logic_1164.all;

package ram_sdp_init_array_2008_pkg is
    type ram_init_t is array (natural range <>) of std_logic_vector;
end package;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.ram_sdp_init_array_2008_pkg.all;

entity ram_sdp_init_array_2008 is
    generic (
        DATA_WIDTH  : positive                                                := 16;
        ADDR_WIDTH  : positive                                                := 10;
        REG_RD_DATA : boolean                                                 := false;
        WRITE_FIRST : boolean                                                 := false;
        STYLE       : string                                                  := "";
        INIT        : ram_init_t(0 to 2**ADDR_WIDTH-1)(DATA_WIDTH-1 downto 0) := (others => (others => 'U'))
        );
    port (
        clk     : in  std_logic;
        rd_en   : in  std_logic;
        rd_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        rd_data : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_en   : in  std_logic;
        wr_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        wr_data : in  std_logic_vector(DATA_WIDTH-1 downto 0)
        );
end ram_sdp_init_array_2008;


architecture default_arch of ram_sdp_init_array_2008 is

    type ram_t is array (0 to 2**ADDR_WIDTH-1) of std_logic_vector(DATA_WIDTH-1 downto 0);
    signal ram                : ram_t := ram_t(INIT);
    
    -- Tell Quartus what type of RAM to use
    attribute ramstyle        : string;
    attribute ramstyle of ram : signal is STYLE;

    -- Tell Vivado what type of RAM to use
    attribute ram_style        : string;
    attribute ram_style of ram : signal is STYLE;

    signal rd_data_ram : std_logic_vector(DATA_WIDTH-1 downto 0);
begin
    process (clk)
    begin
        if (rising_edge(clk)) then
            if (wr_en = '1') then
                ram(to_integer(unsigned(wr_addr))) <= wr_data;
            end if;

            if (rd_en = '1') then
                rd_data_ram <= ram(to_integer(unsigned(rd_addr)));
            end if;
        end if;
    end process;

    l_write_first : if (WRITE_FIRST) generate
        signal bypass_data_r  : std_logic_vector(DATA_WIDTH-1 downto 0);
        signal bypass_valid_r : std_logic := '0';
    begin
        process (clk)
        begin
            if (rising_edge(clk)) then
                if (rd_en = '1') then
                    bypass_valid_r <= '0';
                    if (wr_en = '1') then
                        bypass_data_r <= wr_data;
                        if (rd_addr = wr_addr) then
                            bypass_valid_r <= '1';
                        end if;
                    end if;
                end if;
            end if;
        end process;

        l_reg_rd_data : if (REG_RD_DATA) generate
            process (clk)
            begin
                if (rising_edge(clk)) then
                    if (rd_en = '1') then
                        if (bypass_valid_r = '1') then
                            rd_data <= bypass_data_r;
                        else
                            rd_data <= rd_data_ram;
                        end if;
                    end if;
                end if;
            end process;
        end generate;

        l_no_reg_rd_data : if (not REG_RD_DATA) generate
            rd_data <= bypass_data_r when bypass_valid_r = '1' else rd_data_ram;
        end generate;
    end generate;

    l_read_first : if (not WRITE_FIRST) generate
    begin
        l_reg_rd_data : if (REG_RD_DATA) generate
            process (clk)
            begin
                if (rising_edge(clk)) then
                    if (rd_en = '1') then
                        rd_data <= rd_data_ram;
                    end if;
                end if;
            end process;
        end generate;

        l_no_reg_rd_data : if (not REG_RD_DATA) generate
            rd_data <= rd_data_ram;
        end generate;
    end generate;
end default_arch;

Notice that on lines 7–9, we define a new package that declares an unconstrained array type ram_init_t with an unconstrained element. If you are using VHDL-2008, you likely already have this type defined and can simply use your own.

On line 23, the INIT parameter now specifies a range for both the size of the ram_init_t array and the element width. On line 40, the entity assigns INIT to the ram array, using a type conversion from ram_init_t to ram_t. You might wonder why I didn’t just use a single type, which leads to a lengthy discussion of VHDL best practices. In short, VHDL does not provide standard, built-in array types, so IP developed in isolation must define the types it needs. Integrating such IP into a different project often results in multiple definitions of what is effectively the same type. Unless you write all the code in a project and ensure all entities use the same type from the same package, this situation is unavoidable.

We are now almost done. The final step is to instantiate the 2008 SDP RAM in a new top-level module:

-- Greg Stitt
-- StittHub (www.stitt-hub.com)

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

-- Included to get initializer array.
use work.ram_init_demo_pkg.all;

-- Included to get type of initializer array for casting.
use work.ram_sdp_init_array_2008_pkg.all;

entity ram_init_array_demo_2008 is
    generic (
        DATA_WIDTH  : positive := 8;
        ADDR_WIDTH  : positive := 6;
        REG_RD_DATA : boolean  := false;
        WRITE_FIRST : boolean  := false;
        STYLE       : string   := ""
        );
    port (
        clk     : in  std_logic;
        rd_en   : in  std_logic;
        rd_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        rd_data : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_en   : in  std_logic;
        wr_addr : in  std_logic_vector(ADDR_WIDTH-1 downto 0);
        wr_data : in  std_logic_vector(DATA_WIDTH-1 downto 0)
        );
end ram_init_array_demo_2008;

architecture default_arch of ram_init_array_demo_2008 is
begin
    
    U_TOP : entity work.ram_sdp_init_array_2008
        generic map (
            DATA_WIDTH  => DATA_WIDTH,
            ADDR_WIDTH  => ADDR_WIDTH,
            REG_RD_DATA => REG_RD_DATA,
            WRITE_FIRST => WRITE_FIRST,
            STYLE       => STYLE,
            INIT        => ram_init_t(RAM_INIT_DEMO_ARRAY)
            )
        port map (clk     => clk,
                  rd_en   => rd_en,
                  rd_addr => rd_addr,
                  rd_data => rd_data,
                  wr_en   => wr_en,
                  wr_addr => wr_addr,
                  wr_data => wr_data);

end architecture;

And we’re finally done! We now have an RTL RAM module in SystemVerilog, VHDL-93, and VHDL-2008 that can be initialized using a custom format by generating a package with a Python script, which defines an array containing the RAM’s initial contents. While directly providing a RAM file to an RTL module might seem slightly more convenient, there is no reliable way to do this across tools. Even if it were possible, the headaches from managing relative paths in different synthesis and simulation environments would likely outweigh any convenience.

Conclusions

In this article, I described my exploration process to find a RAM initialization method that supports SystemVerilog and VHDL, works in Vivado and Quartus, and allows designers to create their own custom file formats. Although the final solution requires a Python script, I imagine most use cases will find this acceptable, since RAM initialization files are generally not updated frequently.

This approach also offers several practical advantages. For example, with large RAMs, it is often undesirable to commit a massive file to a repository. Using a custom format allows the initialization contents to be defined more concisely. The custom format can then be committed to the repository, with the potentially larger .mem, .sv, or .vhd files generated before compilation.

While the YAML format presented here is sufficient for nearly all purposes I’ve encountered, it is not intended as a standard initialization format. Rather, it serves as an example of a custom format combined with a Python script to generate a universally compatible initialization file. If your application requires different initialization patterns, I encourage you to extend the script accordingly.

Hopefully in the future this problem is solved more elegantly by allowing synthesis tools to do file I/O during elaboration, but until that happens, I’m hoping the many hours I spent discovering this approach will serve as a sufficient workaround.