Search code examples
pythoncastingbit-manipulationfpgaxilinx

Properly converting float64 to 16bit fixed point for PYNQ


I need to convert a float64 value into a fixed point <16,15> (16 bit with 15 bit in the fractional part and 1 in the integer part).

I have already read many solutions:

  1. Convert floating point to fixed point
  2. Simple Fixed-Point Conversion in C
  3. The fxpmath library

However I have not really understood the "type" I need in my specific case.

To explain this better, I have implemented a code that generates a simple sine wave inside PYNQ (the Xilinx framework based on Python):

import numpy as np
###
fs = 44100  
n_samples = 1024 
f_sig = 130 #selected to get almost exactly a single frequency bin (43.066 * 3)
A = 1
seconds = 1

t = np.arange(0,seconds, step=1/fs)

#resolution of fft = fs/n_samples -> 44100/1024 = 43.066
data_in = A * np.sin(2*np.pi*f_sig*t)
data_in = data_in[:1024]
###

Which generates a simple sine wave and then I select the first 1024 samples. The goal of this selection is to send those samples to a FFT logicore block (FFT logicore documentation). In my specific case, the FFT has been configured to accept 16 bit fixed point inputs <16,15> for real and imaginary part (you can find more info in the documentation at page 18) and perform a 1024 points FFT. So basically I need to convert this sinusoid from float64 to ap_fixed<16,15>

However, I am not sure of which Python datatype I should use in my conversion. For example, in the second approach, it uses a uint16 to store the data after conversion. In the third approach the library returns an object.

When calling the function to copy the input to the DMA buffer and compute the FFT I have the following code:

from pynq imort Overlay
from pynq import allocate

dma = overlay.axi_dma_0
buff_in = allocate(1024, 'uknown type') # which type should I use here? 
buff_in[:] = data_in[:]
np.copyto(buff_in, data_in, casting="unsafe")
dma.sendchannel.transfer(buff_in)

I understood the basic algorithm which should be something like:

import numpy as np
my_casting_int = int32(np.round(data_in * 2**15)))
my_casting_uint = uint32(np.round(data_in * 2**15)))

But again, I don't know if I need a signed or unsigned casting, considering that the input data is a float64 sinusoid with sign.

To sum everything up:

  1. My FFT expects 2 arrays of ap_fixed<16,15> for each input sample, one for the real part and one for the imaginary part. Being a real input sample, I will have 16 bits with the real part and 16 bits which are basically all zeros for the imaginary part.
  2. I need to convert my sinusoid from Python float64 into a suitable format for my FFT core (which is ap_fixed<16,15>).
  3. I don't know which Python type I should use after the casting from float64.

Solution

  • I suppose that FFT expects ap_fixed<16,15>, where MSB is the sign bit. In your example you have signed samples (because sinusoidal between -1.0 and 1.0), so your casting must be int (signed int). But if you need a two-complement representation of signed int, it's right if you cast with uint. In both cases, cast with 16 bits is enough.

    np.round(data_in*2**15).astype(np.int16) # returns -16384
    np.round(data_in*2**15).astype(np.uint16) # returns 49152
    

    If you use float64 and need to translate to ap_fixed<16,15> (fxp-s16/15 or S1.15 type) take care about rounding and truncation methods.

    If you use fxpmath, it's more clear what conversion you are doing. It's true that an object is returned, but you can extract the int (raw) value by raw method, or uint by uraw method:

    from fxpmath import Fxp
    
    #...
    
    data_in_fxp = Fxp(data_in, dtype='fxp-s16/15') # or dtype='S1.15'
    
    raw_data_int = data_in_fxp.raw()
    raw_data_uint = data_in_fxp.uraw() # two-complement
    

    A numpy array is returned by those method. If you need a python list of int, just use tolist method, for example: data_in_fxp.raw().tolist()

    Rounding and overflow behaviors could be modified, for more info visit fxpmath#behaviors