Sample Generator#

The sample generator is used to generate a baseband signal from a collection of samples stored in its memory, known as a wave.
The module has a certain parallelism, set by the LogNsamplesClock parameter, where the number of samples generated for each clock cycle is 2^LogNsamplesClock.
In its standard configuration, it generates 16 samples per clock cycle.
This generator can also use the samples stored in memory to create waves of arbitrary length which can be used to speed up time sweep operations or to store many waves with little memory occupation.

Theory of Operation#

The difference between this generator and a simple table look up is that this implementation also adds the possibility of interpolating the samples to obtain durations that are independent of the number of samples stored in memory.
In fact, in a normal table look up, the duration of a wave is determined by the number of samples stored in the look up table and, if a wave of different duration needs to be generated, the memory needs to be updated even when the shape of the wave is the same.
In this standard approach, the address of the look up operation starts from a provided start value and increases by 1 for each sample generated, thus:

p[0] = start_address
p[n] = p[n-1] + 1
sample[n] = MEMORY[p[n]]

In our implementation however, the above algorithm is generalized to support variable p increments, also supporting fractions.
This is achived via linear interpolation, where two adiacent samples are red from memory and a new sample is computed as a value inbetween the two.
The p is, in general, a real number that indexes the memory with its integer part whereas the fractional part is used to interpolate the sample. Thus:

p[n] = p[n-1] + increment, p[0] = start_offset
p_intnt[n] = floor(p[n]), p_fracrac[n] = p[n] - p_intnt[n]
sample[n] = (MEM[p_intnt[n] + 1] - MEM[p_intnt[n]])*p_fracrac[n] + MEM[p_intnt[n]]

In this way, a fixed amount of memory can be traversed with different increments creating a different total number of samples.
The increment is computed as:

increment = memory_samples/wanted_duration

When memory_samples = wanted_duration, we get the simple table look up.
In the standard configuration, the increment input is a fixed point number with 11 bits for the integer part and 26 bits for the fractional part.
The start offset input has the same rapresentation, where the integer part represents the start address whereas the fractional part is used to compensate for the fixed point rapresentation.
You can therefore calculate the inputs using the following formulas:

increment = (memory_samples<<26)//wanted_duration
start_offset = start_address<<26 + ((memory_samples<<26)%wanted_duration)//2

Symmetry#

The generator also supports symmetry, in an effort to reduce the number of samples stored in memory.
If the samples stored in memory are symmetic, only the first half of the samples needs to be loaded in memory.
Whenever a wave is generated using those samples, the generator needs the symmetric input flag to be high and the correct symmetry type set.
The symmetry type is set using the i_even and q_even inputs, each setting if the symmetry is even for the in-phase and quadrature values respectively.
Given a symmetric wave of N samples, with indices spanning 0 to N-1; the samples needed are the ones spanning indices 0 to floor(N/2).
It’s usually more memory efficient to use waves with odd N.
The equations to compute the increment and start offset are the same, using N as the memory_samples variable.

Parallelism and Memory Mapping#

As stated above, the generator creates a number of samples for each clock cycle.
Each datapath that creates each sample is referred to as a channel.
The following equations describe the behaviour of the generator:

for i in 0 to 15:

p[16*n+i] = p[16*n+i-1] + increment, p[0] = start_offset
p_intnt[16*n+i] = floor(p[16*n+i]), p_frac[16*n+i] = p[16*n+i] - p_int[16*n+i]
sample[16*n+i] = (MEM_i[p_int[16*n+i] + 1] - MEM_i[p_int[16*n+i]])*p_frac[16*n+i] + MEM_i[p_int[16*n+i]]

In practice:

  • channel 0 generates samples 0,16,32,…

  • channel 1 generates samples 1,17,33,…

  • …

  • channel 15 generates samples 15,31,47,…

Ideally all channels share the same memory, thus needing a memory with a large number of read ports.
These kind of memories are not natively supported on the FPGA however and they need to be created using multiple memories all having the same content.
In particular, each channel has its own 2k sample memory that can be written via a shared write channel.
Each sample is a pair of 16 bit values, representing the in-phase and quadrature component of the wave and it’s written using a 32 bit word encoded with the in-phase sample on the most significant 16 bits.
In order to increase memory efficiency, each memory can be written to individually, in order to store up to 32k samples.
This is only allowed with the interpolation turned off and an increment equal to 1/16.
Doing this will force all channels to access the same address, which increases by 1 at each clock cycle, and will force the output to be equal to MEM_i[p_int[16*n+i]] by automatically setting p_frac = 0.
This allowes the generator to be used as a standard table look-up with 32k samples.

In interpolation mode, each channel memory needs to be filled with the same data starting from the start_address.
In table look up mode, each channel memory needs to be filled with samples 16*n+i, n=0,1,2…, starting from start_address.

The write channel is mapped in the following way:

  • address 0x0000 to 0x07FF -> channel 0 memory

  • address 0x0800 to 0x0FFF -> channel 1 memory

  • …

  • address 0x7800 to 0x7FFF -> channel 15 memory

  • address 0x8000 to 0x87FF -> all channels

Each segment spans 2k samples and the last segment is used to write to all channels on the same address at the same time.

Creating the Project#

To generate a copy of this project:

  1. create a directory called workspace in the root of the repo

  2. move to that directory and source the vivado init script

  3. open vivado

  4. go to tools > settings > vivado store > board repository and add the board_files/ directory located in the root of this repo

  5. source the create_project.tcl script located in this directory

Simulate the Sample Generator#

A test bench is provided for simulation purposes.
In the current configuration of the test bench, a wave of length 3000 is generated from 512 samples using the interpolation feature of the generator.
The test bench automatically loads the memory samples at the start from a text file located in the others/ directory and will also store the generated samples in others/output.txt.