November 14, 2024

Finite Impulse Response Filter – FIR Filter – Implementation in Verilog, FPGA, and Vivado


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)   \begin{align*}y_{k}=b_{0}u_{k}+b_{1}u_{k-1}+b_{2}u_{k-2}+b_{3}u_{k-3}+\ldots + b_{N}u_{k-N}=\sum_{i=0}^{N}b_{i}u_{k-i}\end{align*}

where

  • u_{k}, k=0,1,2,3,\ldots are the inputs to the FIR filter at the discrete time instant k.
  • y_{k} is the output of the FIR filter at the discrete-time instant k.
  • b_{0}, b_{1},b_{2},\ldots, b_{N} are the FIR filter coefficients.
  • N 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 N=3, the moving average filter is

(2)   \begin{align*}y_{k}=\frac{u_{k}+u_{k-1}+u_{k-2}+u_{k-3}}{4}\end{align*}

where coefficients are

(3)   \begin{align*}b_{0}=\frac{1}{4}, \; b_{1}=\frac{1}{4},\; b_{2}=\frac{1}{4},\; b_{3}=\frac{1}{4}\end{align*}

To verify the implementation, it is a good idea to expand the filter equation (1) for several values of k. We assume N=3 and we start from k=3

(4)   \begin{align*}y_{3} & =b_{0}u_{3}+b_{1}u_{2}+b_{2}u_{1}+b_{3}u_{0} \\y_{4} & =b_{0}u_{4}+b_{1}u_{3}+b_{2}u_{2}+b_{3}u_{1} \\y_{5} & =b_{0}u_{5}+b_{1}u_{4}+b_{2}u_{3}+b_{3}u_{2} \\y_{6} & =b_{0}u_{6}+b_{1}u_{5}+b_{2}u_{4}+b_{3}u_{3} \end{align*}

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 N=7:

(5)   \begin{align*}y_{k}=\frac{1}{8}u_{k}+\frac{1}{8}u_{k-1}+\frac{1}{8}u_{k-2}+\frac{1}{8}u_{k-3}+\frac{1}{8}u_{k-4}+\frac{1}{8}u_{k-5}+\frac{1}{8}u_{k-6}+\frac{1}{8}u_{k-7}\end{align*}

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
    readmemb("input.data", data);          // open the file for saving the filtered data     FILE1=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);
           fdisplay(FILE1,"%b",output_data);            input_data <= data[k];            if (k==99)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
    readmemb("input.data", data);          // open the file for saving the filtered data     FILE1=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);
           fdisplay(FILE1,"%b",output_data);            input_data <= data[k];            if (k==99)fclose(FILE1);
    end
end
endmodule