HardwareTeams.com - The #1 job board and blog for electrical and computer engineers

# FIR Filter Design and Verification with Python and Cocotb #

## Intro #

In the getting started tutorial we implemented a basic verilog module and simulated it with iverilog and cocotb. This tutorial will be a DSP focused look at cocotb.

## What this tutorial is about #

In this tutorial we will:

1. design a filter in python using numpy and scipy
2. translate that design to verilog
3. verify verilog implementaiton of our filter with cocotb

## Design the FIR #

We need to design some filter taps to put into our verilog design, so lets do that. Lets say our filter has the following requirements:

• a passband of `+/- 0.1 * Fs`
• a transition of `0.1 * Fs`
• must be length 13

### filter_design.py #

``````import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

cutoff = .1       # Desired passband bandwidth, Hz
trans_width = .1  # Width of transition from pass to stop, Hz
numtaps = 13      # Size of the FIR filter.
fs = 1            # normalized sampling rate

# floating point coefficients
filter_coefs = signal.remez(numtaps, [0, cutoff, cutoff + trans_width, 0.5*fs],[1, 0], fs=fs)

# 8 bit integer coefficients
filter_coefs_int = np.round(filter_coefs * (2**7-1))
nfft = 2000;
print(filter_coefs_int)

x_fft = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(filter_coefs/np.sum(filter_coefs), nfft))))
xaxis = np.arange(-0.5, 0.5, 1/nfft)

x_fft_int = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(filter_coefs_int/np.sum(filter_coefs_int), nfft))))

plt.figure(3)
plt.plot(xaxis, x_fft)
plt.plot(xaxis, x_fft_int, linestyle='dashed')
plt.title('real portion of signal x')
plt.grid()
plt.xlabel('Normalized Frequency')
plt.ylabel('dB')
plt.title('Filter Response')
plt.xlim([-.5, .5])
plt.legend(['flating point coefs', '8 bit coefs'])
plt.show()
``````

filter taps: `[-1 -7 -4 4 18 32 38 32 18 4 -4 -7 -1]`

## Implement filter in verilog #

Lets implement our filter in verilog. This is not the best way (or even a good way) to design a filter in verilog, but it is a way to a implement a filter in verilog. Don’t get hung up on how incorrect or correct this is, we just want to demonstrate how to verify a complex system in cocotb.

### fir.v #

``````module fir(clk, data_in, reset, data_out);

input clk;
input signed [7:0] data_in;
input reset;
output signed [15:0] data_out;
reg    signed [15:0] data_out_reg;

// coefficients
wire  signed [7:0]  h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11,h12;
// multiplies
wire  signed [15:0] m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12;
// taps delays
wire  signed [15:0] q1,q2,q3,q4,q5,q6,q7,q8,q9,q10,q11,q12;
wire  signed [15:0] a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12;

// coefs from fir_design.py : [-1. -7. -4.  4. 18. 32. 38. 32. 18.  4. -4. -7. -1.]
// coeffs definition
assign h0  = -1;
assign h1  = -7;
assign h2  = -4;
assign h3  = 4;
assign h4  = 18;
assign h5  = 32;
assign h6  = 38;
assign h7  = 32;
assign h8  = 18;
assign h9  = 4;
assign h10 = -4;
assign h11 = -7;
assign h12 = -1;

// each multiply in the chain
assign m12 = h12 * data_in;
assign m11 = h11 * data_in;
assign m10 = h10 * data_in;
assign m9  = h9  * data_in;
assign m8  = h8  * data_in;
assign m7  = h7  * data_in;
assign m6  = h6  * data_in;
assign m5  = h5  * data_in;
assign m4  = h4  * data_in;
assign m3  = h3  * data_in;
assign m2  = h2  * data_in;
assign m1  = h1  * data_in;
assign m0  = h0  * data_in;

// each add in the chain
assign a1  = q1  + m11;
assign a2  = q2  + m10;
assign a3  = q3  + m9;
assign a4  = q4  + m8;
assign a5  = q5  + m7;
assign a6  = q6  + m6;
assign a7  = q7  + m5;
assign a8  = q8  + m4;
assign a9  = q9  + m3;
assign a10 = q10 + m2;
assign a11 = q11 + m1;
assign a12 = q12 + m0;

// delay line
dff dff1( .clk(clk), .reset(reset),.d(m12), .q(q1));
dff dff2( .clk(clk), .reset(reset),.d(a1),  .q(q2));
dff dff3( .clk(clk), .reset(reset),.d(a2),  .q(q3));
dff dff4( .clk(clk), .reset(reset),.d(a3),  .q(q4));
dff dff5( .clk(clk), .reset(reset),.d(a4),  .q(q5));
dff dff6( .clk(clk), .reset(reset),.d(a5),  .q(q6));
dff dff7( .clk(clk), .reset(reset),.d(a6),  .q(q7));
dff dff8( .clk(clk), .reset(reset),.d(a7),  .q(q8));
dff dff9( .clk(clk), .reset(reset),.d(a8),  .q(q9));
dff dff10(.clk(clk), .reset(reset),.d(a9),  .q(q10));
dff dff11(.clk(clk), .reset(reset),.d(a10), .q(q11));
dff dff12(.clk(clk), .reset(reset),.d(a11), .q(q12));

// filter output data_out[n] = conv(x[n], h[n])
always @(posedge clk)
begin
if(reset)
data_out_reg <= 0;
else
data_out_reg <= a12;
end
assign data_out = data_out_reg;

// Dump waves
initial begin
\$dumpfile("dump.vcd");
\$dumpvars(1, fir);
end
endmodule
``````

The above verilog uses another module, dff, so you will also need to include this in your design. This is a good example for showing how to include multiple verilog files in your cocotb makefile.

### dff.v #

``````module dff(clk, reset, d, q);
input clk;
input reset;
input  [15:0] d;
output [15:0] q;
reg    [15:0] q_r;

always @(posedge clk or posedge reset)
begin
if(reset)
q_r <= 16'b0;
else
q_r <= d;
end

assign q = q_r;
endmodule
``````

## cocotb testbench #

### testbench.py #

``````# Simple tests for an fir_filter module
import cocotb
import random
from cocotb.clock import Clock
from cocotb.triggers import Timer
from cocotb.triggers import RisingEdge
from scipy.signal import lfilter
import numpy as np
import matplotlib.pyplot as plt

# as a non-generator
def wave(amp, f, fs, clks):
clks = np.arange(0, clks)
sample = np.rint(amp*np.sin(2.0*np.pi*f/fs*clks))
return sample

def predictor(signal,coefs):
output = lfilter(coefs,1.0,signal)
return output

@cocotb.test()
async def filter_test(dut):
#initialize
dut.data_in.value = 0
fs       = 1
amp0     = 80
num_clks = 512
nfft     = num_clks;
f0       = 50*(1.0/nfft)
coefs    = np.array([-1., -7., -4.,  4., 18., 32., 38., 32., 18.,  4., -4., -7., -1.])
cnt      = 0

# input data
input_signal = wave(amp0, f0, fs,num_clks) + wave(amp0/2, 200.5*(1.0/nfft), fs, num_clks)

# bit accurate predictor values
data_out_pred = predictor(input_signal, coefs)

# start simulator clock
cocotb.start_soon(Clock(dut.clk, 1, units="ms").start())

# Reset DUT
dut.reset.value = 1
await RisingEdge(dut.clk)
dut.reset.value = 0

output_signal = np.zeros(num_clks)

# run through each clock
for samp in range(num_clks):
await RisingEdge(dut.clk)
# get the output at rising edge
dut_data_out = dut.data_out.value.signed_integer

# feed a new input in
dut.data_in.value  = int(input_signal[samp])

output_signal[samp] = dut_data_out

# wait until reset is over, then start the assertion checking
if(cnt>=2):
assert dut_data_out == data_out_pred[cnt-2], "filter result is incorrect: %d != %d" % (dut_data_out, data_out_pred[cnt-2])
cnt = cnt + 1

in_fft    = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(input_signal, nfft))))

out_fft   = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(output_signal[2:], nfft))))
pred_fft  = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(data_out_pred[:-2], nfft))))
filt_fft  = np.fft.fftshift(20*np.log10(np.abs(np.fft.fft(coefs/sum(coefs), nfft))))

# normalize FFTs lazy style
in_fft   = in_fft   - np.max(in_fft)
out_fft  = out_fft  - np.max(out_fft)
pred_fft = pred_fft - np.max(pred_fft)
xaxis    = np.arange(-0.5, 0.5, 1/nfft)

plt.figure(1)
plt.subplot(1,2,1)
plt.plot(output_signal[2:], marker='x')
plt.plot(data_out_pred[:-2], marker='o')
plt.legend(['DUT', 'Theory'])
plt.title('time domain')
plt.subplot(1,2,2)
plt.stem(output_signal[2:]-data_out_pred[:-2])
plt.title('error : DUT - Golden Reference')

plt.figure(2)
plt.subplot(2,1,1)
plt.plot( xaxis, in_fft)
plt.plot(xaxis, filt_fft)
plt.title('Input to DUT: Frequency Domain Response')
plt.subplot(2,1,2)
plt.plot(xaxis, out_fft, marker='x')
plt.plot(xaxis, pred_fft, marker='o')
plt.title('Output of DUT: Frequency Domain Response')
plt.plot(xaxis, filt_fft)
plt.grid()
plt.xlabel('Normalized Frequency')
plt.ylabel('dB')
plt.title('Filter Response')
plt.xlim([-.5, .5])
plt.legend(['output', 'pred', 'filter'])
plt.show()
``````

### makefile #

``````# defaults
SIM ?= icarus
TOPLEVEL_LANG ?= verilog

VERILOG_SOURCES = \$(PWD)/*.v

# use VHDL_SOURCES for VHDL files

# TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file
TOPLEVEL = fir

# MODULE is the basename of the Python test file
MODULE = testbench

COCOTB_HDL_TIMEUNIT = 1ns
COCOTB_HDL_TIMEPRECISION = 1ps
# include cocotb's make rules to take care of the simulator setup
include \$(shell cocotb-config --makefiles)/Makefile.sim
``````

### results #

Frequency Domain Plots:

Time Domain Plots:

Regression/Assertion Checks: