In this FPGA tutorial, we explain how to implement a finite impulse response (FIR) filter in Verilog and FPGAs from scratch. We use the Vivado development environment to implement the FIR filter. We will create a Verilog test bench that will simulate the response of the FIR filter in Vivado and we will analyze the timing diagrams. First, we generate a decimal test data sequence in Python. We generate a time series and add a random noise to the time series. Then, we convert this sequence into a series of binary numbers of the fixed word length. The resulting binary sequence is saved in a data file. This data file containing the binary sequence is loaded into a Verilog testbench. The binary numbers are filtered by the FIR filter that is implemented in Verilog. Finally, we save the filtered binary sequence into a data file, open this file in Python, and convert back the filtered binary sequence into a decimal representation.
Important remarks: The FIR implementation presented in this tutorial is not the most optimal one and not the most robust one. Also, the filter implementation presented in this tutorial can be improved. However, the material presented in this tutorial is a good starting point for developing more robust implementations. We will explain the basic principles of implementing the FIR filter and how to analyze the filter behavior. –As you will see, there are a number of issues that need to be addressed when implementing FIR algorithms in FPGAs. For example, a person can easily make an error by not carefully analyzing the effects of the finite word length for representing decimal numbers. Also, the FIR filter works as expected in the simulation. However, when we try to synthesize and implement the filter in real hardware, we might experience timing issues and some parts of the design have to be rewritten. These and other issues will be addressed in future tutorials.
YouTube tutorial is given here:
Basics of Finite Impulse Response (FIR) Filter
The Finite Impulse Response (FIR) filter has the following form:
(1)
where
- , are the inputs to the FIR filter at the discrete time instant .
- is the output of the FIR filter at the discrete-time instant .
- are the FIR filter coefficients.
- is the order of the FIR filter.
The figure below shows a block diagram of the FIR filter:
FIR filters have a large number of applications in control engineering and signal processing. The most simple and maybe the most widely used example is the moving average filter. For , the moving average filter is
(2)
where coefficients are
(3)
To verify the implementation, it is a good idea to expand the filter equation (1) for several values of . We assume and we start from
(4)
Implementation of FIR Filter in FPGAs and Verilog
As a demonstration of how to implement the FIR filter, we consider a moving average filter of the order :
(5)
To implement this filter, we perform the following steps:
- Step 1 in Python: convert the filter coefficients from a decimal to a binary representation. We use 8 bits for this conversion.
- Step 2 in Python: generate a test data sequence. We will generate a noisy data sequence that will be filtered by the FIR filter.
- Step 3 in Python: convert the generated test data sequence to a binary representation. We use 16 bits for this conversion.
- Step 4 in Python: save the generated sequence in a data file such that we can load it from the Vivado simulation.
- Step 5 in Verilog and Vivado: implement the FIR filter.
- Step 6 in Verilog and Vivado: simulate the FIR filter by using the generated sequence. Save the filtered data in a data file.
- Step 7 in Python: load the filtered data sequence, convert it to a decimal representation, and plot the results. Compare the results with the FIR filter that is implemented in Python.
The following Python code will perform steps 1,2,3 and 4
'''
Title: FIR Filter Implementation in FPGAs
Author: Aleksandar Haber, PhD
Description: This Python file is used to generate a noisy data sequence
and to convert numbers from a decimal to a binary representation. In addition,
this file is used to plot the outputs of the FIR filter.
License: This code file and all the code files should not be used for commercial
or research purposes. That is,you are not allowed to use this code if you work for
a company, university, or any commercial or research institution without paying
an appropriate fee. You are not allowed to use this code for teaching on
online learning platforms and you are not allowed to use this code in official
university courses.
You are not allowed to publicly repost this code. You are not allowed to
modify this code in any way. You are not allowed to integrate this code
in any type of commercial or research projects. You are not allowed to
use this code to generate scientific articles or reports.
You are not allowed to use this code to train a large language model
or any type of an AI algorithm.
DELIBERATE OR INDELIBERATE VIOLATIONS OF THIS LICENSE
WILL INDUCE LEGAL ACTIONS AND LAWSUITS.
Contact information:
ml.mecheng@gmail.com
'''
import numpy as np
import matplotlib.pyplot as plt
# this function is used to perform the transformation from a signed binary to
# the signed decimal representation
def todecimal(x, bits):
assert len(x) <= bits
n = int(x, 2)
s = 1 << (bits - 1)
return (n & s - 1) - (n & s)
# compute a binary representation of the filter coefficients
# number of coefficients
tap=8
# for computing first scale, we want to represent filter coefficients
# as 8 bit numbers
N1=8
# this is used to convert the filter inputs to 16-bit signed values
N2=16
# this is the output bit width
N3=32
real_coeff=(1/tap);
# bit representation of the coefficients
coeff_bit=np.binary_repr(int(real_coeff*(2**(N1-1))),N1)
# double check, invert, it should be equal to real_coeff
todecimal(coeff_bit, N1)/(2**(N1-1))
# generate a test sequence
timeVector=np.linspace(0,2*np.pi,100)
output=np.sin(2*timeVector)+np.cos(3*timeVector)+0.3*np.random.randn(len(timeVector))
plt.plot(output)
plt.show()
# convert to integers
# this list containst the N2-bit signed representation of the sin sequence
list1=[]
for number in output:
list1.append(np.binary_repr(int(number*(2**(N1-1))),N2))
# save the converted sequence to the data file
with open('input.data', 'w') as file:
for number in list1:
file.write(number + '\n')
# after this line, you need to run the Vivado code
This file will create a test data file called “input.data”. This data file contains a binary representation of the input sequence for testing the performance of the FIR filter. This test data file is opened by the test bench file in Verilog.
We implement the FIR filter in Vivado and Verilog. The implementation is given below (there is also an alternative implementation given at the end of this tutorial).
/* Second implementation of the FIR filter
Author: Aleksandar Haber
See the license file!
License: This code file and all the code files should not be used for commercial
or research purposes. That is,you are not allowed to use this code if you work for
a company, university, or any commercial or research institution without paying
an appropriate fee. You are not allowed to use this code for teaching on
online learning platforms and you are not allowed to use this code in official
university courses.
You are not allowed to publicly repost this code. You are not allowed to
modify this code in any way. You are not allowed to integrate this code
in any type of commercial or research projects. You are not allowed to
use this code to generate scientific articles or reports.
You are not allowed to use this code to train a large language model
or any type of an AI algorithm.
DELIBERATE OR INDELIBERATE VIOLATIONS OF THIS LICENSE
WILL INDUCE LEGAL ACTIONS AND LAWSUITS.
Contact information:
ml.mecheng@gmail.com
*/
/*
input_data - input filter sample
CLK - clock/delay for propagating data down the filter taps
RST - used to reset the filter, that is, it sets all the memorized samples to 0
- RST=1 -resets the filter
- To start the filter, put RST=0 and ENABLE=1
ENABLE - Used to start the filter, set ENABLE=1 to start the filter
output_data - Output sample of the filter
sampleT - This is an additional output that is used to debug the filter
you can set this sample to be equal to any variable so you can
better understand what is happening inside
*/
`timescale 1ns / 1ps
module fir_filter (input_data,CLK,RST,ENABLE,output_data,sampleT);
// FIR coefficient word width
parameter N1=8;
// input data word width
parameter N2=16;
// output data word width
parameter N3=32;
// this array is used to store the coefficients
wire signed [N1-1:0] b[0:7];
// filter coefficients
assign b[0]=8'b00010000;
assign b[1]=8'b00010000;
assign b[2]=8'b00010000;
assign b[3]=8'b00010000;
assign b[4]=8'b00010000;
assign b[5]=8'b00010000;
assign b[6]=8'b00010000;
assign b[7]=8'b00010000;
// input data
input signed [N2-1:0] input_data;
// output data samples
output signed [N2-1:0] sampleT;
// clock
input CLK;
// used to set to zero all filter taps
input RST;
// used to enable the filter
input ENABLE;
// filtered data
output signed [N3-1:0] output_data;
// this array is used to store samples and to shift them
reg signed [N2-1:0] samples[0:6];
// this equation implements the filter
assign output_data= b[0]*input_data
+b[1]*samples[0]
+b[2]*samples[1]
+b[3]*samples[2]
+b[4]*samples[3]
+b[5]*samples[4]
+b[6]*samples[5]
+b[7]*samples[6];
always @(posedge CLK)
begin
// this is used to reset the filter before starting the filter
if (RST==1'b1)
begin
samples[0]<=0;
samples[1]<=0;
samples[2]<=0;
samples[3]<=0;
samples[4]<=0;
samples[5]<=0;
samples[6]<=0;
end
// here, we store the past values in the filter memory
// note that the input_value is the value that is set before
// the clock rising edge
if ((ENABLE==1'b1)&&(RST==1'b0))
begin
samples[0]<=input_data;
samples[1]<=samples[0];
samples[2]<=samples[1];
samples[3]<=samples[2];
samples[4]<=samples[3];
samples[5]<=samples[4];
samples[6]<=samples[5];
end
end
//assign output_data=output_data_reg;
assign sampleT=samples[0];
endmodule
The testbench file is given below. This testbench file opens the input data from the file called “input.data”, and saves the filtered data in the file called “save.data”.
/* Testbench file for testing
the second implementation of the FIR filter
Author: Aleksandar Haber
See the license file!
License: This code file and all the code files should not be used for commercial
or research purposes. That is,you are not allowed to use this code if you work for
a company, university, or any commercial or research institution without paying
an appropriate fee. You are not allowed to use this code for teaching on
online learning platforms and you are not allowed to use this code in official
university courses.
You are not allowed to publicly repost this code. You are not allowed to
modify this code in any way. You are not allowed to integrate this code
in any type of commercial or research projects. You are not allowed to
use this code to generate scientific articles or reports.
You are not allowed to use this code to train a large language model
or any type of an AI algorithm.
DELIBERATE OR INDELIBERATE VIOLATIONS OF THIS LICENSE
WILL INDUCE LEGAL ACTIONS AND LAWSUITS.
Contact information:
ml.mecheng@gmail.com
*/
`timescale 1ns / 1ps
module testbench;
// FIR coefficient word width
parameter N1=8;
// input data word width
parameter N2=16;
// output data word width
parameter N3=32;
// clock
reg CLK;
// reset for the FIR coefficient
reg RST;
// enable FIR coefficient
reg ENABLE;
// input data sample
reg [N2-1:0] input_data;
// this array stores all input samples from the file
reg [N2-1:0] data[99:0];
// output data
wire[N3-1:0] output_data;
// additional output for testing
wire [N2-1:0] sampleT;
// unit under test
fir_filter UUT(.input_data(input_data),
.output_data(output_data),
.CLK(CLK),
.RST(RST),
.ENABLE(ENABLE),
.sampleT(sampleT)
);
// integer for the for loop
integer k;
// file for saving the filtered data
integer FILE1;
// clock has a period of 10 [ns]
always #10 CLK=~CLK;
initial
begin
// set the input sample number
k=0;
// load the data samples and store them in the array called data
fopen("save.data","w");
// set the clock to zero
CLK=0;
#20
// reset the filter, this will set all the coefficients to zero
RST=1'b1;
#40
// here we enable the filter
RST=1'b0;
ENABLE=1'b1;
input_data <= data[k];
#10
for (k = 1; k<100; k=k+1)
begin
@(posedge CLK);
fclose(FILE1);
end
end
endmodule
The Python code that implements the step 7 is given below. It opens the filtered data from the file “save.data”. Also, this code implements the FIR filter in Python. The results are compared an plotted on the same graph.
# from here, we read the filtered values, convert them to decimal representation
# and plot the filtered results
read_b=[]
# read data
with open("save.data") as file:
for line in file:
read_b.append(line.rstrip('\n'))
# this list contains the converted values
n_l=[]
for by in read_b:
n_l.append(todecimal(by,N3)/(2**(2*(N1-1))))
# implementation of the FIR filter in Python
p_output=[]
samples=np.zeros(shape=(8,1))
for inputValue in output:
samples[1:7,:]=samples[0:6,:]
samples[0,0]=inputValue
filteredOutput=0
for i in range(8):
filteredOutput=filteredOutput+(1/8)*samples[i,0]
p_output.append(filteredOutput)
p_output=np.array(p_output)
# plot the data
plt.plot(output,color='blue',linewidth=3,label='Original signal')
plt.plot(n_l,color='red',linewidth=3,label='Filtered signal FPGA')
plt.plot(p_output,color='black',linewidth=3,label='Filtered signal Python')
plt.legend()
plt.savefig('results.png',dpi=600)
plt.show()
The results are shown in the figure below.
There is an alternative way of implementing the FIR filter. The alternative approach is shown in the listing below.
/* First implementation of the FIR filter
Author: Aleksandar Haber
License: This code file and all the code files should not be used for commercial
or research purposes. That is,you are not allowed to use this code if you work for
a company, university, or any commercial or research institution without paying
an appropriate fee. You are not allowed to use this code for teaching on
online learning platforms and you are not allowed to use this code in official
university courses.
You are not allowed to publicly repost this code. You are not allowed to
modify this code in any way. You are not allowed to integrate this code
in any type of commercial or research projects. You are not allowed to
use this code to generate scientific articles or reports.
You are not allowed to use this code to train a large language model
or any type of an AI algorithm.
DELIBERATE OR INDELIBERATE VIOLATIONS OF THIS LICENSE
WILL INDUCE LEGAL ACTIONS AND LAWSUITS.
Contact information:
ml.mecheng@gmail.com
*/
/*
input_data - input filter sample
CLK - clock/delay for propagating data down the filter taps
RST - used to reset the filter, that is, it sets all the memorized samples to 0
- RST=1 -resets the filter
- To start the filter, put RST=0 and ENABLE=1
ENABLE - Used to start the filter, set ENABLE=1 to start the filter
output_data - Output sample of the filter
sampleT - This is an additional output that is used to debug the filter
you can set this sample to be equal to any variable so you can
better understand what is happening inside
*/
`timescale 1ns / 1ps
module fir_filter (input_data,CLK,RST,ENABLE,output_data,sampleT);
// FIR coefficient word width
parameter N1=8;
// input data word width
parameter N2=16;
// output data word width
parameter N3=32;
// this array is used to store the coefficients
wire signed [N1-1:0] b[0:7];
// filter coefficients
assign b[0]=8'b00010000;
assign b[1]=8'b00010000;
assign b[2]=8'b00010000;
assign b[3]=8'b00010000;
assign b[4]=8'b00010000;
assign b[5]=8'b00010000;
assign b[6]=8'b00010000;
assign b[7]=8'b00010000;
// input data
input signed [N2-1:0] input_data;
// output data samples
output signed [N2-1:0] sampleT;
// clock
input CLK;
// used to set to zero all filter taps
input RST;
// used to enable the filter
input ENABLE;
// filtered data
output signed [N3-1:0] output_data;
// this register is used to store the output
reg signed [N3-1:0] output_data_reg;
// this array is used to store samples and to shift them
reg signed [N2-1:0] samples[0:6];
always @(posedge CLK)
begin
if (RST==1'b1)
begin
samples[0]<=0;
samples[1]<=0;
samples[2]<=0;
samples[3]<=0;
samples[4]<=0;
samples[5]<=0;
samples[6]<=0;
output_data_reg<=0;
end
if ((ENABLE==1'b1)&&(RST==1'b0))
begin
output_data_reg<= b[0]*input_data
+b[1]*samples[0]
+b[2]*samples[1]
+b[3]*samples[2]
+b[4]*samples[3]
+b[5]*samples[4]
+b[6]*samples[5]
+b[7]*samples[6];
samples[0]<=input_data;
samples[1]<=samples[0];
samples[2]<=samples[1];
samples[3]<=samples[2];
samples[4]<=samples[3];
samples[5]<=samples[4];
samples[6]<=samples[5];
end
end
assign output_data=output_data_reg;
assign sampleT=samples[0];
endmodule
The testbench file is given below.
/* Testbench file for testing
the first implementation of the FIR filter
Author: Aleksandar Haber
License: This code file and all the code files should not be used for commercial
or research purposes. That is,you are not allowed to use this code if you work for
a company, university, or any commercial or research institution without paying
an appropriate fee. You are not allowed to use this code for teaching on
online learning platforms and you are not allowed to use this code in official
university courses.
You are not allowed to publicly repost this code. You are not allowed to
modify this code in any way. You are not allowed to integrate this code
in any type of commercial or research projects. You are not allowed to
use this code to generate scientific articles or reports.
You are not allowed to use this code to train a large language model
or any type of an AI algorithm.
DELIBERATE OR INDELIBERATE VIOLATIONS OF THIS LICENSE
WILL INDUCE LEGAL ACTIONS AND LAWSUITS.
Contact information:
ml.mecheng@gmail.com
*/
`timescale 1ns / 1ps
module testbench;
// FIR coefficient word width
parameter N1=8;
// input data word width
parameter N2=16;
// output data word width
parameter N3=32;
// clock
reg CLK;
// reset for the FIR coefficient
reg RST;
// enable FIR coefficient
reg ENABLE;
// input data sample
reg [N2-1:0] input_data;
// this array stores all input samples from the file
reg [N2-1:0] data[99:0];
// output data
wire[N3-1:0] output_data;
// additional output for testing
wire [N2-1:0] sampleT;
// unit under test
fir_filter UUT(.input_data(input_data),
.output_data(output_data),
.CLK(CLK),
.RST(RST),
.ENABLE(ENABLE),
.sampleT(sampleT)
);
// integer for the for loop
integer k;
// file for saving the filtered data
integer FILE1;
// clock has a period of 10 [ns]
always #10 CLK=~CLK;
initial
begin
// set the input sample number
k=0;
// load the data samples and store them in the array called data
fopen("save.data","w");
// set the clock to zero
CLK=0;
#20
// reset the filter, this will set all the coefficients to zero
RST=1'b1;
#40
// here we enable the filter
RST=1'b0;
ENABLE=1'b1;
input_data <= data[k];
#10
for (k = 1; k<100; k=k+1)
begin
@(posedge CLK);
fclose(FILE1);
end
end
endmodule