How to initialize RAM from file using TEXTIO

A convenient way to populate block RAM with initial values is to read binary or hexadecimal literals from an ASCII file. This is also a good way to create a ROM (read-only memory) in VHDL. After all, RAM and ROM are the same thing in FPGAs, ROM is a RAM that you only read from.

The examples throughout this article will assume that the following constants and RAM type have been declared at the start of the declarative region of the VHDL file.

constant ram_depth : natural := 256;
constant ram_width : natural := 32;
 
type ram_type is array (0 to ram_depth - 1)
  of std_logic_vector(ram_width - 1 downto 0);

This blog post is part of a series about using the TEXTIO library in VHDL. Read the other articles here:

Stimulus file read in testbench using TEXTIO

BMP file bitmap image read using TEXTIO

READLINE, LINE, HREAD, OREAD, and BREAD

The subprograms and types needed for reading and writing external files in VHDL are located in the TEXTIO package. This package is part of the std library. The standard library is always loaded; therefore, we don’t have to import it explicitly with the library keyword.

We can simply go ahead and use the TEXTIO package in the header of our VHDL file like this:

use std.textio.all;

We will store the RAM data in an ASCII file where one line of text corresponds to a memory slot. To read a line of text we use the READLINE procedure from the TEXTIO package. The procedure takes two arguments, the file name as a constant input and the parsed line of text as an inout variable. The prototype declaration of the READLINE procedure and the LINE type taken from the VHDL standard specification is shown below.

procedure READLINE (file F: TEXT; L: inout LINE);
 
type LINE is access STRING; -- A LINE is a pointer
                            -- to a STRING value.

Although the class of the LINE parameter isn’t explicitly specified in the prototype declaration of READLINE, it’s a variable because that’s the default class for inout parameters. The LINE type is simply an access type to a string, a pointer to a dynamically allocated string object.

VHDL-2008 defines the OREAD, HREAD, and BREAD procedures for extracting octal, hexadecimal, and binary values from a LINE object. The methods for reading octal and hexadecimal values are quite similar, the octal values are merely a subset of the hexadecimals. For simplicity, we are going to skip octal reads in this article and focus on how to read hexadecimal and binary values from a text file.

The code below shows the definitions of the procedures that are relevant for us, they are only available in VHDL-2008 and newer revisions. The OREAD and HREAD procedures come in two overloaded flavors for each of the supported output types. The optional GOOD output can be used for detecting read errors, although, most tools will produce an error or warning regardless if this output is used or not.

procedure OREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR;
                                 GOOD : out BOOLEAN);
procedure OREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR);
 
procedure HREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR;
                                  GOOD : out BOOLEAN);
procedure HREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR);
 
alias BREAD is READ [LINE, STD_ULOGIC_VECTOR, BOOLEAN];
alias BREAD is READ [LINE, STD_ULOGIC_VECTOR];
procedure READLINE (file F: TEXT; L: inout LINE);
 
procedure READ (L: inout LINE; VALUE: out BIT;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out BIT);
 
procedure READ (L: inout LINE; VALUE: out BIT_VECTOR;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out BIT_VECTOR);
 
procedure READ (L: inout LINE; VALUE: out BOOLEAN;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out BOOLEAN);
 
procedure READ (L: inout LINE; VALUE: out CHARACTER;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out CHARACTER);
 
procedure READ (L: inout LINE; VALUE: out INTEGER;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out INTEGER);
 
procedure READ (L: inout LINE; VALUE: out REAL;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out REAL);
 
procedure READ (L: inout LINE; VALUE: out STRING;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out STRING);
 
procedure READ (L: inout LINE; VALUE: out TIME;
                               GOOD: out BOOLEAN);
procedure READ (L: inout LINE; VALUE: out TIME);
 
procedure SREAD (L: inout LINE; VALUE: out STRING;
                                STRLEN: out NATURAL);
alias STRING_READ is SREAD [LINE, STRING, NATURAL];
 
alias BREAD is READ [LINE, BIT_VECTOR, BOOLEAN];
alias BREAD is READ [LINE, BIT_VECTOR];
alias BINARY_READ is READ [LINE, BIT_VECTOR, BOOLEAN];
alias BINARY_READ is READ [LINE, BIT_VECTOR];
 
procedure OREAD (L: inout LINE; VALUE: out BIT_VECTOR;
                                GOOD: out BOOLEAN);
procedure OREAD (L: inout LINE; VALUE: out BIT_VECTOR);
alias OCTAL_READ is OREAD [LINE, BIT_VECTOR, BOOLEAN];
alias OCTAL_READ is OREAD [LINE, BIT_VECTOR];
 
procedure HREAD (L: inout LINE; VALUE: out BIT_VECTOR;
                                GOOD: out BOOLEAN);
procedure HREAD (L: inout LINE; VALUE: out BIT_VECTOR);
alias HEX_READ is HREAD [LINE, BIT_VECTOR, BOOLEAN];
alias HEX_READ is HREAD [LINE, BIT_VECTOR];
procedure READ (L : inout LINE; VALUE : out STD_ULOGIC; GOOD : out BOOLEAN);
procedure READ (L : inout LINE; VALUE : out STD_ULOGIC);
 
procedure READ (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR; GOOD : out BOOLEAN);
procedure READ (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR);
 
alias BREAD is READ [LINE, STD_ULOGIC_VECTOR, BOOLEAN];
alias BREAD is READ [LINE, STD_ULOGIC_VECTOR];
alias BINARY_READ is READ [LINE, STD_ULOGIC_VECTOR, BOOLEAN];
alias BINARY_READ is READ [LINE, STD_ULOGIC_VECTOR];
 
procedure OREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR; GOOD : out BOOLEAN);
procedure OREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR);
alias OCTAL_READ is OREAD [LINE, STD_ULOGIC_VECTOR, BOOLEAN];
alias OCTAL_READ is OREAD [LINE, STD_ULOGIC_VECTOR];
 
procedure HREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR; GOOD : out BOOLEAN);
procedure HREAD (L : inout LINE; VALUE : out STD_ULOGIC_VECTOR);
alias HEX_READ is HREAD [LINE, STD_ULOGIC_VECTOR, BOOLEAN];
alias HEX_READ is HREAD [LINE, STD_ULOGIC_VECTOR];

Read hex values from file

Hexadecimal is a handy format for describing RAM content because two hex characters directly translate into one byte, eight bits. Every character describes a nibble (half-byte) and each line in the text file describes the content of one RAM slot. The listing below shows an excerpt from the ram_content_hex.txt file. It has been filled with example values ranging from 1 to 256 decimal, written as hex.

1
2
255
256
00000001
00000002
...
000000FF
00000100

To load the data from the text file we use an impure function declared below the ram_type, but above the RAM signal declaration. The code below shows the init_ram_hex function which reads the data from the text file and returns it as a ram_type object.

impure function init_ram_hex return ram_type is
  file text_file : text open read_mode is "ram_content_hex.txt";
  variable text_line : line;
  variable ram_content : ram_type;
begin
  for i in 0 to ram_depth - 1 loop
    readline(text_file, text_line);
    hread(text_line, ram_content(i));
  end loop;
 
  return ram_content;
end function;

The readline procedure inside of the for-loop reads one line of text at the time and assigns it to the text_line variable. This object is of type line, which is an access type to a string object, a pointer to a dynamically allocated string. On the next line, the hread procedure reads the string from the line object and converts it to a std_ulogic_vector. This type can be assigned directly to the std_logic_vector that each RAM cell is constructed of.

Finally, we declare the RAM signal while calling our init_ram_hex function to provide the initial values for it:

signal ram_hex : ram_type := init_ram_hex;

Need the Questa/ModelSim project files?

Let me send you a Zip with everything you need to get started in 30 seconds

How does it work?

Tested on Windows and Linux Loading Gif.. How it works

HREAD in VHDL-2002 and VHDL-93

Unfortunately, the HREAD procedure is only available in VHDL-2008. In all previous versions of VHDL the standard READ procedure must be used instead. The READ procedure is overloaded with many different output types, but there isn’t an option for reading hexadecimal values.

Let’s write a custom algorithm for converting a hexadecimal ASCII characters to a VHDL std_logic_vector. First, we need to read the characters one by one from the text_line object, then we decode their values and assign them to the correct slice of the RAM slot vector. The code below shows an equivalent implementation of the init_ram_hex function which also works in legacy VHDL versions.

impure function init_ram_hex return ram_type is
  file text_file : text open read_mode is "ram_content_hex.txt";
  variable text_line : line;
  variable ram_content : ram_type;
  variable c : character;
  variable offset : integer;
  variable hex_val : std_logic_vector(3 downto 0);
begin
  for i in 0 to ram_depth - 1 loop
    readline(text_file, text_line);
 
    offset := 0;
 
    while offset < ram_content(i)'high loop
      read(text_line, c);
 
      case c is
        when '0' => hex_val := "0000";
        when '1' => hex_val := "0001";
        when '2' => hex_val := "0010";
        when '3' => hex_val := "0011";
        when '4' => hex_val := "0100";
        when '5' => hex_val := "0101";
        when '6' => hex_val := "0110";
        when '7' => hex_val := "0111";
        when '8' => hex_val := "1000";
        when '9' => hex_val := "1001";
        when 'A' | 'a' => hex_val := "1010";
        when 'B' | 'b' => hex_val := "1011";
        when 'C' | 'c' => hex_val := "1100";
        when 'D' | 'd' => hex_val := "1101";
        when 'E' | 'e' => hex_val := "1110";
        when 'F' | 'f' => hex_val := "1111";
 
        when others =>
          hex_val := "XXXX";
          assert false report "Found non-hex character '" & c & "'";
      end case;
 
      ram_content(i)(ram_content(i)'high - offset
        downto ram_content(i)'high - offset - 3) := hex_val;
      offset := offset + 4;
 
    end loop;
  end loop;
 
  return ram_content;
end function;

The algorithm simply goes through every line while looking at every character, converting it to the correct binary value. If a character that isn’t in the range 0x0-0xF is encountered, an assert failure is raised in the when others branch. The offset variable controls the slice position within each memory cell to assign the decoded value to.

You may be asking yourself why don’t we create a custom hread procedure instead of coding it inside of the init_ram_hex function? Then we wouldn’t have to change the init_ram_hex function at all, we would simply use our custom hread procedure in place of the missing standard one.

That would work in most simulators and some synthesizers like Lattice iCEcube2, but it won’t synthesize in Xilinx Vivado. The error message below clearly states what the problem is.

In Vivado:
[Synth 8-27] Procedure argument of type ‘line’ is not supported [init_ram_tb.vhd:15]

procedure hread(l: inout line; value: out std_logic_vector) is
  variable c : character;
  variable ok : boolean;
  variable i : integer := 0;
  variable hex_val : std_logic_vector(3 downto 0);
begin
  while i < value'high loop
    read(l, c);
   
    case c is
      when '0' => hex_val := "0000";
      when '1' => hex_val := "0001";
      when '2' => hex_val := "0010";
      when '3' => hex_val := "0011";
      when '4' => hex_val := "0100";
      when '5' => hex_val := "0101";
      when '6' => hex_val := "0110";
      when '7' => hex_val := "0111";
      when '8' => hex_val := "1000";
      when '9' => hex_val := "1001";
      when 'A' | 'a' => hex_val := "1010";
      when 'B' | 'b' => hex_val := "1011";
      when 'C' | 'c' => hex_val := "1100";
      when 'D' | 'd' => hex_val := "1101";
      when 'E' | 'e' => hex_val := "1110";
      when 'F' | 'f' => hex_val := "1111";
   
      when others =>
        hex_val := "XXXX";
        assert false report "Found non-hex character '" & c & "'";
    end case;
   
    value(value'high - i downto value'high - i - 3) := hex_val;
    i := i + 4;
  end loop;
end procedure;

Read binary values from file

You may want to store the RAM values as binary literals instead of hex characters if the RAM width isn’t a multiple of 8. The listing below shows the same content as before, but represented in binary format using only the characters 0 and 1.

1
2
255
256
00000000000000000000000000000001
00000000000000000000000000000010
...
00000000000000000000000011111111
00000000000000000000000100000000

The algorithm shown below is for reading binary values from file. It is similar to reading hexadecimals, but in VHDL-2008 you should use the BREAD procedure call instead of HREAD. It will translate one ASCII character to a single std_ulogic value, which is implicitly converted to std_logic.

impure function init_ram_bin return ram_type is
  file text_file : text open read_mode is "ram_content_bin.txt";
  variable text_line : line;
  variable ram_content : ram_type;
begin
  for i in 0 to ram_depth - 1 loop
    readline(text_file, text_line);
    bread(text_line, ram_content(i));
  end loop;
 
  return ram_content;
end function;

Finally, we initialize the RAM signal by calling our new impure function as shown in the code below.

signal ram_bin : ram_type := init_ram_bin;

Need the Questa/ModelSim project files?

Let me send you a Zip with everything you need to get started in 30 seconds

How does it work?

Tested on Windows and Linux Loading Gif.. How it works

BREAD in VHDL-2002 and VHDL-93

We can easily make our code portable to legacy VHDL versions by calling READ instead of BREAD. The excerpt from the VHDL standard below shows the prototype of READ that we are interested in using.

procedure READ (L: inout LINE; VALUE: out BIT);

The READ procedure that outputs a std_ulogic didn’t exist before VHDL-2008, therefore we have to use the bit version from the TEXTIO library. Fortunately, this type can easily be converted to std_logic by using the standard To_StdLogicVector function.

The implementation of init_ram_bin shown below works in VHDL-2002 and as well as in VHDL-93.

impure function init_ram_bin return ram_type is
  file text_file : text open read_mode is "ram_content_bin.txt";
  variable text_line : line;
  variable ram_content : ram_type;
  variable bv : bit_vector(ram_content(0)'range);
begin
  for i in 0 to ram_depth - 1 loop
    readline(text_file, text_line);
    read(text_line, bv);
    ram_content(i) := To_StdLogicVector(bv);
  end loop;
 
  return ram_content;
end function;

Backport of the IEEE std_logic_1164 library

An alternative to changing the code for legacy VHDL versions is to use the std_logic_1164_additions third-party package. By downloading and adding this library to your project you will be able to use the new procedures also in VHDL-2002 and VHDL-93. Of course, then you will be importing a lot more and your code will always depend on this package.

Similar Posts

4 Comments

  1. Does HRead only work on 32 bit values? It’s not stated anywhere, but I’ve had issues with 8 bit values.

    1. It should work with bytes as well. There are many ways to read bytes from a text file, and if you have written them as 32-bit values in a text file like this:

      AABBCCDD
      00112233
      

      Then you can read the 8-bit values like this:

      EXAMPLE_PROC : process
        file text_file : text open read_mode is "hex_file.txt";
        variable text_line : line;
        variable byte : std_logic_vector(7 downto 0);
      
      begin
      
          for i in 0 to 1 loop
            readline(text_file, text_line);
      
            for i in 0 to 3 loop
              hread(text_line, byte);
              report("byte: " & to_hstring(byte));
            end loop;
      
          end loop;
        
        finish;
        wait;
      end process;
      

      And the output to the simulator console will be:

      # ** Note: byte: AA
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: BB
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: CC
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: DD
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: 00
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: 11
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: 22
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      # ** Note: byte: 33
      #    Time: 0 ns  Iteration: 0  Instance: /test_tb
      
  2. Hi Jonas,

    In my application I need to initialize the first part but not all of the memory. I do it like this. Not sure if there is a better way

    -- fill the memory with zero
    while not endfile(f) loop
       -- read and decode the file data
       -- check within memory bounds
       -- write the memory contents
    end loop
    
    1. Hello, Andrew. I hope you are doing well!

      That will work if the file always has the data you want to write and nothing else.

      You state in the code comments that there will be checking of memory bounds. That adds an extra level of safety in case someone changes the data file. Perhaps you also can check that the last write address is as expected after the While loop ends.

      Good work. You can do this! 😎

Leave a Reply

Your email address will not be published. Required fields are marked *