Skip to content
Julian Kemmerer edited this page Oct 2, 2022 · 22 revisions

The basic C concept of 'an array of elements' does not map directly to FPGA resources. Arrays in C can range from a few bytes to gigabytes in size. What size arrays should be implemented in registers v.s. what size in block RAM? How many ports should the RAM have? Should there be more advanced memory interfaces too?

Its possible to imagine some dividing line, ~any array >X bytes in size is implemented as a RAM, etc. But then on FPGA this is particularly complicated since block ram resources also require input/output registers. Introducing multi cycle behavior based on your array size is outside of the simple 'C array of elements' expectation.

Additionally, array reads and writes are seen as pure functions, and can be autopipelined by the tool. If arrays were being inferred as an efficient block/RAM structure by default, then upon conversion to LUTs for autopipelining, resource usage and timing characteristic of the design would change drastically (where once there was a RAM, now there are a bunch of LUTs? or vice versa).

For those reasons PipelineC does not decide if/when arrays should be implemented as RAMs. Instead all arrays/array access is implemented in LUTs by default. The user is required to explicitly call out when RAM structures are to be inferred by synthesis tools (for now).

Arrays

A read from N element array can be thought of as a N to 1 mux. Where the mux select bits are the address/position in the array. Such a mux function is pure, and can be autopipelined.

my_val = my_array[my_index];
// same as made up pure 'MUX' function
my_val = MUX(my_array, my_index);

A write to an N element array can be thought of as a pure function as well. It takes the entire array as an input, and outputs the entire array with a single element modified.

my_array[my_index] = my_val;
// same as made up pure 'ARRAY_WRITE' function
my_array = ARRAY_WRITE(my_array, my_index, my_val);

The logic inside of ~ARRAY_WRITE is implemented with compares and muxes. For each element at index comparison of my_index==index and if so that element in the array gets the write my_val (all other elements are returned unmodified).

Multiple Dimensions

Multiple dimension arrays have their 'address'/'index'/'mux select' bits concatenated to form a single dimensional array. For example:

// 64 byte 2D array
uint8_t my_array[8][8]; // two 3b values for array indices

The two array indices each need three bits each, to hold values 0-7. The two array indices are concatenated to form a six bit address. Equivalent to:

// 64 byte 1 array
uint8_t my_array[64]; // one 6b value for array index

For arrays of non-power-of-two sizes this address/index concatenation can become wasteful. Ex. an array of 33 elements uses just as many address bits as an array of 64. Users can save resources and flatten odd size multiple dimensional arrays into a single dimension using the typical i*constant + j multiply and add of two indices operations.