One of the key elements of FPGA design is the implementation of digital filters, indeed last year we looked at how we could use the Eclypse Z7 board with Zmods to implement digital filters.
However, that project requires the use of external hardware in the form of signal generators etc. I thought it might be a good idea to create a example which uses PYNQ and enables us to generate arbitrary signals using Python, filter it and plot the resultant waveform.
With the project using PYNQ we will be able to visualise both the input and output waveforms.
Vivado DesignTo get started with this project we are going to implement a design in Vivado. This design is going to connect a FIR Compiler to a DMA in the programmable logic. This will allow us to transfer significant numbers of samples through the FIR Filter.
To do this we will need the following IP blocks
- Zynq UltrasScale+ MPSOC Processing System
- AXI Interrupt Controller
- AXI DMA
- FIR Compiler
- Smart Interconnect
- AXI Interconnect
- Processor Reset System
- Concat block
These blocks need to be configured as below
Zynq UltraScale+ MPSoC Processing system - Configured for the Avnet ZU Board, on this IP block I also enabled a S AXI HPC connection, along with the PL PS IRQ
For the FIR compiler I am going to implement a simple low pass filter, with a sampling rate of 100 Msps and a clock rate of 100MHz. This means we will be able to work with signals up to 50 MHz.
To design the filter I again used TFilter to design the low pass filter, the pass band is defined from 0-25MHz while the stop band is defined from 30MHz upwards.
The coefficients generated can be copied over into the FIR Compiler
As we are working with 16 bit inputs, the output will be grow to 33 bits however, to ensure it is easier work with the DMA we will truncate the result to 32 bits.
If we so desired we can modify the design to enable filter coefficients to be uploaded on the fly.
As we are working with the DMA we need to ensure the FIR Compilers AXI Streaming interface has the correct number of signals. We especially need the TLast signal for DMA.
When it comes to the DMA we need to configure it to support up buffer length registers of up to 26 bits. Along with ensuring the read channel is 16 bits and the write channel is 32 bits.
The finished solution should look like the diagram below (TCL script is attached to the project below)
With the diagram completed we can build the project to obtain the HardWare Handoff (HWH) file and the Bit Stream.
PYNQTo get started creating the note book first we need to, obtain the PYNQ image for the ZU board (available here) and write the ISO image to the SD Card. Once the SD Card is ready we are able to boot the board.
With the ZU Board connected to the ethernet, we are able to map the board as a network location and transfer the generated HWH and bit files to the ZU Board.
Make sure you check connect using different credentials, the passwords are xilinx, xilinx.
Make sure when you upload the bit and HWH they are named the same.
To get started with a creating the notebook first we are going to perform the filtering entirely in python.
import numpy as np
import matplotlib.pyplot as plt
frequency_pass = 5e6
sampling_rate = 100e6 # Sampling rate (100 MHz for better resolution)
duration = 2e-6 # Duration in seconds (1 microsecond to show a few cycles)
amplitude = 32767 # Maximum amplitude for signed 16-bit integer
t = np.arange(0, duration, 1/sampling_rate) # Time vector
signal_pass = np.sin(2 * np.pi * frequency_pass * t) * amplitude # Sine wave
signal = signal_pass
plt.figure(figsize=(10, 4))
plt.plot(t, signal)
plt.title(' Signal')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
#plt.ylim(-32768, 32767)
plt.show()
This will show the signal below
If we take a FFT of this signal
# Perform the FFT
fft_result = np.fft.fft(signal)
fft_magnitude = np.abs(fft_result) # Magnitude part of the FFT
frequencies = np.fft.fftfreq(signal.size, d=1/sampling_rate) # Frequency bins
# Plot the magnitude of the FFT
plt.plot(frequencies, fft_magnitude)
plt.title('Magnitude of FFT')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude')
plt.grid(True)
plt.show()
We can see the frequency of the signal in the FFT
To perform the filtering on the hardware we can use the following code, not this signal should pass through the filter.
We will use pynq allocate to define the contiguous memory allocation we need for the transmit and receive buffers. We will use the same signal generated above
from pynq import Overlay
ol = Overlay('/home/xilinx/jupyter_notebooks/sig_gen/sig_gen.bit')
dma = ol.axi_dma_0
dma_send = ol.axi_dma_0.sendchannel
dma_recv = ol.axi_dma_0.recvchannel
from pynq import allocate
#create a buffer the same size of the signal of type int 16
input_buffer = allocate(shape=(len(signal),), dtype=np.int16)
#convert the signal to signed 16 bit representation
int16_signal = signal.astype(np.int16)
#copy the signal into the pynq buffer
np.copyto(input_buffer, int16_signal)
#create a output buffer also of length of signal but 32 bits
output_buffer = allocate(shape=(len(signal),), dtype=np.int32)
#run the filter
dma_send.transfer(input_buffer)
#receve the results
dma_recv.transfer(output_buffer)
#check for errors
dma_recv.error
#check for idle
dma_recv.idle
#copy results from the pynq buffer
np.copyto(filtered_signal, output_buffer)
#plot the result
plt.plot(t, filtered_signal, label='Filtered Signal', linestyle='--')
plt.legend()
plt.title('Signal Filtering with an FIR Filter')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid(True)
plt.show()
Plotting the FFT also gives
# Perform the FFT
fft_result = np.fft.fft(filtered_signal)
fft_magnitude = np.abs(fft_result) # Magnitude part of the FFT
frequencies = np.fft.fftfreq(filtered_signal.size, d=1/sampling_rate) # Frequency bins
# Plot the magnitude of the FFT
plt.plot(frequencies, fft_magnitude)
plt.title('Magnitude of FFT')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude')
plt.grid(True)
plt.show()
Now lets generate a signal which should be filtered out
frequency_pass = 45e6
signal_pass = np.sin(2 * np.pi * frequency_pass * t) * amplitude # Sine wave
signal = signal_pass
plt.figure(figsize=(10, 4))
plt.plot(t, signal)
plt.title(' Signal')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.show()
If we are then to run the filter
filtered_signal = np.convolve(signal, coefficients, mode='same')
plt.plot(t, filtered_signal, label='Filtered Signal', linestyle='--')
plt.legend()
plt.title('Signal Filtering with an FIR Filter')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid(True)
plt.show()
Running the hardware example provides
int16_signal = signal.astype(np.int16)
np.copyto(input_buffer, int16_signal)
dma_send.transfer(input_buffer)
dma_recv.transfer(output_buffer)
np.copyto(filtered_signal, output_buffer)
plt.plot(t, filtered_signal, label='Filtered Signal', linestyle='--')
plt.legend()
plt.title('Signal Filtering with an FIR Filter')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid(True)
plt.show()
This project shows how we can easily learn about FIR filters using PYNQ, we can use the PYNQ environment to create simple demonstrations for signal processing.
Comments