Initializing An FPGA ROM From A Text File
In a previous post I described how to generate a PSK31 waveform in a microcontroller using two numerically controller oscillators (NCOs). I’m currently working on implementing a similar algorithm in an FPGA. One part of the design requires a ROM to map signals from one domain to another. Since the mapping will change as I continue testing and the project develops, and I don’t want to have to change the source code each time it does, I decided to initialize the ROM from a text file.
I thought this would be a straightforward process but it didn’t take me long to realize the examples I was working from dealt with simulations and testbenchs, and I needed something that could be synthesized. After spending the better part of an evening search through forums and trying different things I finally got it to work and decided to document my lessons learned here for anyone else trying to do the same thing.
Initialization At Signal Declaration
My first mistake was trying to initialize the ROM as part of the VHDL reset logic. The problem here is obvious in hindsight. As one writer in a forum put it, by the time you get to the reset logic the circuit has been synthesized and there’s no file from which to read.
The solution is to put the initialization in a function and use the function to initialize the ROM at signal declaration, like this:
library ieee; use ieee.std_logic_1164.ALL; use ieee.numeric_std.ALL; use ieee.math_real.ALL; use ieee.std_logic_misc.ALL; use std.textio.all; <snip> -- -- Declarations used to define the array of ROM data. -- type rom_array is array(2**ADDRESS_WIDTH-1 downto 0) of std_logic_vector(NUMBER_OF_SHIFTS downto 0); impure function rom_init(filename : string) return rom_array is file rom_file : text open read_mode is filename; variable rom_line : line; variable rom_value : bit_vector(NUMBER_OF_SHIFTS downto 0); variable temp : rom_array; begin for rom_index in 0 to 2**ADDRESS_WIDTH-1 loop readline(rom_file, rom_line); read(rom_line, rom_value); temp(rom_index) := to_stdlogicvector(rom_value); end loop; return temp; end function; constant rom : rom_array := rom_init(filename => "rom_values.txt");
bit_vector vs Other Data Types
If you look at for loop used to read values from the file in the code above you’ll notice it’s a three step process:
- Read a line of text from the file
- Read a bit_vector from the line of text
- Convert the bit vector to a std_logic_vector
Three steps are necessary because the read function in std.textio.all doesn’t support std_bit_vector as a return type. You have to read the value in as one of the supported types and do the conversion.
Having said this, you can replace std.textio.all with IEEE.std_logic_textio.all. This package provides read and write functions for all the IEEE 1164 data types, including std_logic_vector.
Following the code example I had on hand, I originally used a while loop to read the data from the text file. However, I got an error indicating the loop was not terminating and topping out at 64 iterations.
This is where the difference between simulation and synthesis comes into play. Since code loops inside of synthesis are unrolled the compiler needs to know where to stop and while loops don’t provide that hard limit. Using a for loop with its explicit termination fixed that problem.
Summing It Up
So as it turns out, initializing a ROM from a text file is a straightforward process so long as you remember the differences between simulation and synthesis. And I’m reminded of this difference each time I forget.