Marc-Julien objois, Catherine Single, Charlena Fong, and Mariya Shterngartz
In certain applications, a standard linear buffer in memory is awkward. Take for instance maintaining a statically sized input stream buffer (as is needed in maintaining an audio input buffer). The functionality of such a buffer requires the last element to be disposed of and the newest element to be inserted at the beginning of the buffer. This would require all elements but the last one to be shifted over by one address in memory. Doing so is time consuming and unneccessary.
This same functionality is offered by a circular buffer, which requires no shifting of blocks of memory.
To implement this functionality, a block of memory is sectioned off which accomodates the number of elements you need to store. For best performance, use a size that is a power of two. In these example images, a size of 8 elements is chosen (with indeterminate data width). Two internal registers must be maintained.
Fig. 1 shows the 8 elements of the buffer (labelled 1 through 8) and the locations represented by the two pointers. As well, the "greyed out" look of the cell labels means they have not been written to. Note that the read pointer is at the end and the write pointer is at the start. These locations can be set arbitrarily so long as the read pointer is set to one memory address less than (including wrapping) the write pointer.
A write operation places the data to be written in the location pointed to by the write pointer, copies the value of the current write pointer into the read pointer, then increments the write pointer by 1. The state of the pointers after 3 write operations is shown in Fig. 2.
The write pointer is always at the next location where data should be written, and the read pointer is always set to the last written element after a write operation. In Fig. 2, the darker labels mean elements 1, 2, and 3 have been written to.
Fig. 3 shows the result after a single read operation after the state of Fig. 2. To read, the data at the read pointer is placed on the output data bus, and the read pointer is decremented by one. In this case, element #3, the last element to be written to the buffer, is read.
In Fig. 4, the next to last element to be written is read. This is the second read operation. Read operations continue in this manner, wrapping around from the left end to the right end of the buffer. When the read pointer reaches the write pointer, the last element has been read. This may or may not be important to the VHDL programmer.
A simple implementation can be achieved in VHDL using a minimum of inputs and outputs. Again, make sure a power of two is chosen for the size of the buffer. This makes managing the pointers easier. The entity declaration is as follows:
entity circular_buffer is generic (ram_data_width : integer := 4; -- width of data ram_address_width : integer := 1; -- width of address num_values : integer := 2); -- number of values port( read : in std_logic; write : in std_logic; reset : in std_logic; datain : in std_logic_vector(ram_data_width-1 downto 0); dataout : out std_logic_vector(ram_data_width-1 downto 0); data_ready : out std_logic; clk : in std_logic ); end circular_buffer;
The generics are used to control lpm_ram_dq
as well as managing
the circular buffer's address and data widths.
In an idle state, the read
and write
lines must be
left low. The reset
line should be chosen to be active high or
low depending on preference.
To write to the buffer, a VHDL construct must put the data on the
datain
input signal, and set the write
line to high.
This initiates a write cycle. As soon as the data_ready
line
goes high, the operation is complete. The buffer is then ready to perform
another operation.
To read from the buffer, simply set read
to high and wait for the
data_ready
line to go high. On the rising edge of data_ready
,
The data on dataout
will be valid. The data should be latched at
this point. On the next cycle, the buffer is ready for another operation.
The read
and write
lines should be mutually
exclusive.
Here is a proposed state machine for the circular buffer implementation. Note that the choice of a buffer size that is a power of two means the synthesized code can be smaller, as there is no need for bounds checking. Adding a '1' bit to the binary number '111' represented using three bits results in '000.'
This can be adapted to whatever speed is needed for individual projects.