1 |
2 |
Muraer |
"""
|
2 |
|
|
MIT License
|
3 |
|
|
|
4 |
|
|
Copyright (c) 2017 Mario Mauerer
|
5 |
|
|
|
6 |
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7 |
|
|
of this software and associated documentation files (the "Software"), to deal
|
8 |
|
|
in the Software without restriction, including without limitation the rights
|
9 |
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10 |
|
|
copies of the Software, and to permit persons to whom the Software is
|
11 |
|
|
furnished to do so, subject to the following conditions:
|
12 |
|
|
|
13 |
|
|
The above copyright notice and this permission notice shall be included in all
|
14 |
|
|
copies or substantial portions of the Software.
|
15 |
|
|
|
16 |
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17 |
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18 |
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19 |
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20 |
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21 |
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22 |
|
|
SOFTWARE.
|
23 |
|
|
|
24 |
|
|
VIIRF - Versatile IIR Filter
|
25 |
|
|
|
26 |
|
|
This stand-alone script is used to configure the cascaded IIR filter hardware.
|
27 |
|
|
It checks the provided data for errors and generates multiple files used for configuring the filter's generics
|
28 |
|
|
and the testbenches.It also calculates the filter's step-response (both floating-point and quantized filter)
|
29 |
|
|
|
30 |
|
|
Python: 3.5.2
|
31 |
|
|
NOTE: The behavior of e.g., integers, longs or integer divisions differs from python 2 and 3!
|
32 |
|
|
Python 3 is preferred/required due to the infinite integer-precision provided by python 3.
|
33 |
|
|
Do NOT use NUMPY-datatypes for filter calculations. They are limited to 64 bit and this is quickly not enough
|
34 |
|
|
for high-precision / low-noise filters.
|
35 |
|
|
|
36 |
|
|
"""
|
37 |
|
|
import numpy as np
|
38 |
|
|
import math
|
39 |
|
|
import datetime
|
40 |
|
|
import os
|
41 |
|
|
import matplotlib.pyplot as plt
|
42 |
|
|
from IIR_SOS_Filt_df1 import SOS_Casc_df1_Float, SOS_Casc_df1_Quantized
|
43 |
|
|
|
44 |
|
|
|
45 |
|
|
def main():
|
46 |
|
|
""" Implementation of the filter-configuration script.
|
47 |
|
|
"""
|
48 |
|
|
|
49 |
|
|
""" USER CONFIGURATION: """
|
50 |
|
|
|
51 |
|
|
""" SOS filter coefficients of direct-form 1 filter implementation
|
52 |
|
|
Deliver as list of: [b0, b1, b2, 1, a1, a2]
|
53 |
|
|
"""
|
54 |
|
|
""" This example-filter is an over-the-top lowpass filter to illustrate the numerical stability
|
55 |
|
|
of the cascaded biquads.
|
56 |
|
|
Designed with Matlab's fdatool.
|
57 |
|
|
Chebyshev Type II, fs = 8MHz, fpass = 100kHz, fstop = 103kHz, Astop=100dB, Apass=1dB
|
58 |
|
|
27 sections, order = 53
|
59 |
|
|
"""
|
60 |
|
|
SOS = [
|
61 |
|
|
[0.995479543133467093, -1.984442343028223199, 0.995479543133467093, 1.000000000000000000, -1.992735837405045007,
|
62 |
|
|
0.998943837906379417],
|
63 |
|
|
[0.990160401678707558, -1.973793179424183863, 0.990160401678707558, 1.000000000000000000, -1.990574581081460126,
|
64 |
|
|
0.996817448646951321],
|
65 |
|
|
[0.989994808108325630, -1.973370399921241303, 0.989994808108325630, 1.000000000000000000, -1.988322781675001094,
|
66 |
|
|
0.994642785115447126],
|
67 |
|
|
[0.991233302433756958, -1.975696584512673271, 0.991233302433756958, 1.000000000000000000, -1.985941378398403678,
|
68 |
|
|
0.992382744424644914],
|
69 |
|
|
[0.991659186917251634, -1.976348896290843538, 0.991659186917251634, 1.000000000000000000, -1.983386802441603702,
|
70 |
|
|
0.989996937324842396],
|
71 |
|
|
[0.991088443582708867, -1.974955022041059438, 0.991088443582708867, 1.000000000000000000, -1.980609068243228243,
|
72 |
|
|
0.987439951339094235],
|
73 |
|
|
[0.989791457625747717, -1.972046311400692975, 0.989791457625747717, 1.000000000000000000, -1.977549417505724660,
|
74 |
|
|
0.984659251857910700],
|
75 |
|
|
[0.987954582977658147, -1.967983929921569342, 0.987954582977658147, 1.000000000000000000, -1.974137317922536994,
|
76 |
|
|
0.981592546952532841],
|
77 |
|
|
[0.985631182144554474, -1.962860596839200111, 0.985631182144554474, 1.000000000000000000, -1.970286545701848580,
|
78 |
|
|
0.978164380415427415],
|
79 |
|
|
[0.982774011953296389, -1.956564178822880251, 0.982774011953296389, 1.000000000000000000, -1.965889972689622844,
|
80 |
|
|
0.974281625675177509],
|
81 |
|
|
[0.979253170496530601, -1.948812091805058877, 0.979253170496530601, 1.000000000000000000, -1.960812519064707216,
|
82 |
|
|
0.969827417018972904],
|
83 |
|
|
[0.974849525369247516, -1.939136275833626355, 0.974849525369247516, 1.000000000000000000, -1.954881497685421854,
|
84 |
|
|
0.964652853916498465],
|
85 |
|
|
[0.969223223534541378, -1.926817757162398781, 0.969223223534541378, 1.000000000000000000, -1.947873233632636314,
|
86 |
|
|
0.958565521324582837],
|
87 |
|
|
[0.961850833163988006, -1.910757730499921481, 0.961850833163988006, 1.000000000000000000, -1.939494351783139114,
|
88 |
|
|
0.951313448987238353],
|
89 |
|
|
[0.951915030351768388, -1.889252974781179262, 0.951915030351768388, 1.000000000000000000, -1.929355449638937792,
|
90 |
|
|
0.942562554513742157],
|
91 |
|
|
[0.938119438257709160, -1.859621313051272651, 0.938119438257709160, 1.000000000000000000, -1.916934024503464062,
|
92 |
|
|
0.931864889025346255],
|
93 |
|
|
[0.918395732636982709, -1.817613163905800633, 0.918395732636982709, 1.000000000000000000, -1.901522707459767370,
|
94 |
|
|
0.918614305102019579],
|
95 |
|
|
[0.889505718582964788, -1.756619076035403682, 0.889505718582964788, 1.000000000000000000, -1.882158880178752547,
|
96 |
|
|
0.901986187040051623],
|
97 |
|
|
[0.846725210100101044, -1.667053994501234815, 0.846725210100101044, 1.000000000000000000, -1.857535164039942233,
|
98 |
|
|
0.880860806020991594],
|
99 |
|
|
[0.784291731375497903, -1.537281285666471931, 0.784291731375497903, 1.000000000000000000, -1.825904322530537804,
|
100 |
|
|
0.853741893377616523],
|
101 |
|
|
[0.697754992932913987, -1.358310063925080913, 0.697754992932913987, 1.000000000000000000, -1.785035725009338492,
|
102 |
|
|
0.818719360505246518],
|
103 |
|
|
[0.587659395799238982, -1.131033533640340982, 0.587659395799238982, 1.000000000000000000, -1.732396994259293388,
|
104 |
|
|
0.773624816388282732],
|
105 |
|
|
[0.459692699491257184, -0.866373131097798344, 0.459692699491257184, 1.000000000000000000, -1.665995235127884611,
|
106 |
|
|
0.716751802598463050],
|
107 |
|
|
[0.320384043209753555, -0.577024918342495030, 0.320384043209753555, 1.000000000000000000, -1.586704538299201772,
|
108 |
|
|
0.648848540201372170],
|
109 |
|
|
[0.180337233564054289, -0.285068622336177968, 0.180337233564054289, 1.000000000000000000, -1.502750870671594852,
|
110 |
|
|
0.576957777166854702],
|
111 |
|
|
[0.067223579650870141, -0.048886921409928924, 0.067223579650870141, 1.000000000000000000, -1.433955574106285269,
|
112 |
|
|
0.518049962948276210],
|
113 |
|
|
[0.149602974506854058, 0.149602974506854058, 0.000000000000000000, 1.000000000000000000, -0.703314433339987333,
|
114 |
|
|
0.000000000000000000]]
|
115 |
|
|
|
116 |
|
|
""" Filter section gain-values and overall output gain.
|
117 |
|
|
This array can have different dimensions/lengths:
|
118 |
|
|
If SOSGAIN_EN is true: It must be at least as long as the number of sections, as each section
|
119 |
|
|
requires an individual gain.
|
120 |
|
|
If FINALGAIN_EN is true: It must be at least one element long, the final output gain of the filter.
|
121 |
|
|
If neither of these two parameters is true: It must be an empty list.
|
122 |
|
|
If both parameters are true: The list must be the number of sections plus 1 (section-gains + output gain)
|
123 |
|
|
"""
|
124 |
|
|
G = [0.158364443548321743]
|
125 |
|
|
|
126 |
|
|
""" Defines if section-output-gains in hardware will be enabled.
|
127 |
|
|
The length of G must correspond with this setting (see above).
|
128 |
|
|
"""
|
129 |
|
|
SOSGAIN_EN = False
|
130 |
|
|
|
131 |
|
|
""" Defines if a final filter output gain will be used.
|
132 |
|
|
The length of G must correspond with this setting (see above).
|
133 |
|
|
"""
|
134 |
|
|
FINALGAIN_EN = True
|
135 |
|
|
|
136 |
|
|
""" Number of bits of the unfiltered input data vector.
|
137 |
|
|
The filter operates on signed vectors.
|
138 |
|
|
"""
|
139 |
|
|
W_DAT_INPUT = 16
|
140 |
|
|
|
141 |
|
|
""" Total number of bits of the filter coefficients.
|
142 |
|
|
The coefficients are signed vectors.
|
143 |
|
|
See W_FRAC below for explanation of Q-notation.
|
144 |
|
|
"""
|
145 |
|
|
W_COEF = 18
|
146 |
|
|
|
147 |
|
|
""" Number of bits used for the fraction of the quantized coefficients. Must be <= W_COEF.
|
148 |
|
|
Integer arithmetic: Q-Notation: The difference between W_COE and W_FRAC are the integer bits.
|
149 |
|
|
E.g., for Q1.15, it would require W_COEF = 16 and W_FRAC = 15.
|
150 |
|
|
"""
|
151 |
|
|
W_FRAC = 16
|
152 |
|
|
|
153 |
|
|
""" Filter-input gain: Must be a power of two.
|
154 |
|
|
The filter gain can be used to extend the number of bits of the filtered data,
|
155 |
|
|
in order to increase the precision/filter resolution.
|
156 |
|
|
If not used/desired: Set to 1.
|
157 |
|
|
W_SECT_DAT (filter-internal vector width) will depend on this value.
|
158 |
|
|
"""
|
159 |
|
|
GAIN_INPUT = 4
|
160 |
|
|
|
161 |
|
|
""" Number of bits of (signed) filter output vector
|
162 |
|
|
Filtered result will be saturated to the range of this (signed) vector.
|
163 |
|
|
If filter overshoots (to full-scale inputs) should be covered correctly, use at least 1 bit more
|
164 |
|
|
than at filter input (W_DAT_INPUT).
|
165 |
|
|
"""
|
166 |
|
|
W_DAT_OUTPUT = 32
|
167 |
|
|
|
168 |
|
|
""" This script can only take the required bit-extension caused by the final gain into account.
|
169 |
|
|
If the SOS-gains are used, it could be necessary to manually extend the filter-internal vector width.
|
170 |
|
|
This can be done with this parameter. It directly affects W_SECT_DAT.
|
171 |
|
|
Adjust if the sections saturate or the filter does not achieve the desired response due to vector length limits.
|
172 |
|
|
Set to 0 if not used/required.
|
173 |
|
|
"""
|
174 |
|
|
NUM_BITS_SECT_DAT_EXT_MANUAL = 0
|
175 |
|
|
|
176 |
|
|
""" Simulation-specific user-configuration. This does not affect the filter parameters.
|
177 |
|
|
"""
|
178 |
|
|
STEP_AMPLITUDE = 1.0 # Amplitude of the step for the step-response. Range: 0-1
|
179 |
|
|
NUM_STEP_SIM = 800 # Number of filter iterations for the simulation of the step-response
|
180 |
|
|
|
181 |
|
|
""" File-Names and directories for Data Storage.
|
182 |
|
|
This script generates various text-files containing filter configurations and testbench stimuli.
|
183 |
|
|
"""
|
184 |
|
|
FILENAME_METADATA = 'FilterMetaData.txt' # Stores information for the VHDL generic header
|
185 |
|
|
DIRNAME_STIMULIDATA = 'FilterStimuliData' # Directory where testbench-related data is stored.
|
186 |
|
|
|
187 |
|
|
""" END OF USER INPUT: Implementation follows. """
|
188 |
|
|
|
189 |
|
|
# Convert to floating-point; to homogenize the input
|
190 |
|
|
SOS = [[float(y) for y in x] for x in SOS] # SOS is a list of lists.
|
191 |
|
|
G = [float(x) for x in G]
|
192 |
|
|
W_DAT_INPUT = int(W_DAT_INPUT)
|
193 |
|
|
W_COEF = int(W_COEF)
|
194 |
|
|
W_FRAC = int(W_FRAC)
|
195 |
|
|
# GAIN_INPUT = int(GAIN_INPUT) Don't do this - check for exact power of two and integer-ness later.
|
196 |
|
|
W_DAT_OUTPUT = int(W_DAT_OUTPUT)
|
197 |
|
|
|
198 |
|
|
# Check the viability of the user-supplied data. This function may raise errors and create console output.
|
199 |
|
|
check_user_inputdata(SOS, G, SOSGAIN_EN, FINALGAIN_EN, W_COEF, W_FRAC, GAIN_INPUT)
|
200 |
|
|
|
201 |
|
|
# Calculate the required internal filter vector length (W_SECT_DAT):
|
202 |
|
|
w_sect_dat = get_internal_bit_width(W_DAT_INPUT, G, GAIN_INPUT, W_DAT_OUTPUT, FINALGAIN_EN)
|
203 |
|
|
w_sect_dat = w_sect_dat + NUM_BITS_SECT_DAT_EXT_MANUAL
|
204 |
|
|
s = 'Filter-internal bit-width (W_SECT_DAT): ' + repr(w_sect_dat)
|
205 |
|
|
print(s)
|
206 |
|
|
|
207 |
|
|
# Quantize the coefficients:
|
208 |
|
|
SOSd, Gd = quantize_filter_coefs(SOS, G, W_FRAC)
|
209 |
|
|
|
210 |
|
|
# Write the required VHDL-generics into a file for storage:
|
211 |
|
|
write_filter_metadata(FILENAME_METADATA, SOS, W_DAT_INPUT, W_DAT_OUTPUT, GAIN_INPUT, w_sect_dat, W_COEF, W_FRAC,
|
212 |
|
|
FINALGAIN_EN, SOSGAIN_EN)
|
213 |
|
|
|
214 |
|
|
# Write the coefficients into different files for the testbench:
|
215 |
|
|
# First, create the directory for all testbench-related data, if necessary:
|
216 |
|
|
if not os.path.exists(DIRNAME_STIMULIDATA):
|
217 |
|
|
os.makedirs(DIRNAME_STIMULIDATA)
|
218 |
|
|
write_filter_coe_testbench(SOSd, Gd, SOSGAIN_EN, FINALGAIN_EN, DIRNAME_STIMULIDATA)
|
219 |
|
|
|
220 |
|
|
# Create a floating-point step-input vector. A vector would not be needed (it's all the same value),
|
221 |
|
|
# but this allows an easy change to other inputs if so desired (e.g., noise).
|
222 |
|
|
floatstepin = np.ones(NUM_STEP_SIM, dtype=float) * STEP_AMPLITUDE
|
223 |
|
|
|
224 |
|
|
# Simulate the filter's step response with the floating-point implementation:
|
225 |
|
|
filt_float_inst = SOS_Casc_df1_Float(SOS, G, GAIN_INPUT, SOSGAIN_EN, FINALGAIN_EN) # Create the filter instance
|
226 |
|
|
filt_float_response = []
|
227 |
|
|
for i in range(0, NUM_STEP_SIM):
|
228 |
|
|
filt_float_response.append(filt_float_inst.update_filter(floatstepin[i]))
|
229 |
|
|
|
230 |
|
|
# Create the quantized step-input vector.
|
231 |
|
|
max_in = int(2 ** (W_DAT_INPUT - 1) - 1) # Maximum (signed) filter input value
|
232 |
|
|
quantstepin = np.round(np.ones(NUM_STEP_SIM) * max_in * STEP_AMPLITUDE)
|
233 |
|
|
quantstepin = [int(x) for x in quantstepin] # Convert to python integer
|
234 |
|
|
|
235 |
|
|
# Simulate the quantized filter's step response with the VHDL-reference model:
|
236 |
|
|
filt_quant_inst = SOS_Casc_df1_Quantized(SOSd, Gd, GAIN_INPUT, SOSGAIN_EN, FINALGAIN_EN, W_DAT_INPUT, w_sect_dat,
|
237 |
|
|
W_DAT_OUTPUT, W_COEF, W_FRAC)
|
238 |
|
|
filt_quant_response = []
|
239 |
|
|
for i in range(0, NUM_STEP_SIM):
|
240 |
|
|
filt_quant_response.append(filt_quant_inst.update_filter(quantstepin[i]))
|
241 |
|
|
|
242 |
|
|
# Convert the quantized output to float and rescale back such that the plots can be compared
|
243 |
|
|
filt_quant_response_float = [float(x) / max_in / STEP_AMPLITUDE for x in filt_quant_response]
|
244 |
|
|
|
245 |
|
|
# Write the digital filter input (stimuli) and reference output into two files for testbench-use.
|
246 |
|
|
filename_stims = os.path.join(DIRNAME_STIMULIDATA, 'InputDat_Stimuli.txt')
|
247 |
|
|
write_list_file(filename_stims, quantstepin)
|
248 |
|
|
filename_stims = os.path.join(DIRNAME_STIMULIDATA, 'OutputDat_GoldenReference.txt')
|
249 |
|
|
write_list_file(filename_stims, filt_quant_response)
|
250 |
|
|
|
251 |
|
|
print('Done. Displaying the plot.')
|
252 |
|
|
|
253 |
|
|
# Plot the filter's step-response:
|
254 |
|
|
fig = plt.figure()
|
255 |
|
|
ax = fig.add_subplot(111)
|
256 |
|
|
x = np.arange(1, NUM_STEP_SIM + 1, 1)
|
257 |
|
|
plt.step(x, filt_float_response, label='Floating-Point', marker='o')
|
258 |
|
|
plt.step(x, filt_quant_response_float, label='Quantized (Rescaled)', marker='*')
|
259 |
|
|
ax.set_xlabel('Sample Nr.')
|
260 |
|
|
ax.set_ylabel('Filter Response')
|
261 |
|
|
ax.set_title('Filter Step Responses')
|
262 |
|
|
plt.legend()
|
263 |
|
|
plt.grid()
|
264 |
|
|
plt.show() # This will block until the plotted window is closed.
|
265 |
|
|
|
266 |
|
|
|
267 |
|
|
def write_list_file(filename, dat):
|
268 |
|
|
""" Writes the content of the list "dat" into a text-file.
|
269 |
|
|
Existing files will be cleared / overwritten.
|
270 |
|
|
"""
|
271 |
|
|
clear_txt_file(filename)
|
272 |
|
|
[write_string_append(filename, repr(x)) for x in dat]
|
273 |
|
|
|
274 |
|
|
|
275 |
|
|
def write_filter_coe_testbench(sosd, gd, sosgain_en, finalgain_en, dirpath):
|
276 |
|
|
""" Writes the coefficients in multiple files needed for the filter testbench
|
277 |
|
|
dirpath is a string pointing to the desired directory where the files will be created in.
|
278 |
|
|
"""
|
279 |
|
|
sosd_np = np.array(sosd)
|
280 |
|
|
numsec, _ = sosd_np.shape
|
281 |
|
|
filename_b0 = os.path.join(dirpath, 'CoeffStimuli_b0.txt')
|
282 |
|
|
filename_b1 = os.path.join(dirpath, 'CoeffStimuli_b1.txt')
|
283 |
|
|
filename_b2 = os.path.join(dirpath, 'CoeffStimuli_b2.txt')
|
284 |
|
|
filename_a1 = os.path.join(dirpath, 'CoeffStimuli_a1.txt')
|
285 |
|
|
filename_a2 = os.path.join(dirpath, 'CoeffStimuli_a2.txt')
|
286 |
|
|
filename_g = os.path.join(dirpath, 'CoeffStimuli_g.txt')
|
287 |
|
|
filename_finalgain = os.path.join(dirpath, 'CoeffStimuli_finalgain.txt')
|
288 |
|
|
|
289 |
|
|
clear_txt_file(filename_b0)
|
290 |
|
|
clear_txt_file(filename_b1)
|
291 |
|
|
clear_txt_file(filename_b2)
|
292 |
|
|
clear_txt_file(filename_a1)
|
293 |
|
|
clear_txt_file(filename_a2)
|
294 |
|
|
# Note, these values are cleared and it will always have to be written something to the gain-files,
|
295 |
|
|
# since the testbench always reads them.
|
296 |
|
|
clear_txt_file(filename_g)
|
297 |
|
|
clear_txt_file(filename_finalgain)
|
298 |
|
|
|
299 |
|
|
# Note that the SOS-coeffs are given as list of: [b0, b1, b2, 1, a1, a2]
|
300 |
|
|
# b0, b1, b2, a1, a2 are always required:
|
301 |
|
|
b0 = sosd_np[:, 0]
|
302 |
|
|
b1 = sosd_np[:, 1]
|
303 |
|
|
b2 = sosd_np[:, 2]
|
304 |
|
|
a1 = sosd_np[:, 4]
|
305 |
|
|
a2 = sosd_np[:, 5]
|
306 |
|
|
|
307 |
|
|
if sosgain_en is True and finalgain_en is True:
|
308 |
|
|
g = gd[:-1] # The section gains: all but the last element, which is the output gain
|
309 |
|
|
outgain = gd[-1] # Last element is the final output gain
|
310 |
|
|
elif sosgain_en is True and finalgain_en is False:
|
311 |
|
|
g = gd # There is no output-gain
|
312 |
|
|
outgain = 0 # Write bogus-value; the filter will not use it, but the testbench will want to read it.
|
313 |
|
|
elif sosgain_en is False and finalgain_en is True:
|
314 |
|
|
outgain = gd[-1] # G contains only the output gain. Get it out of the list.
|
315 |
|
|
g = np.zeros(numsec, dtype=int) # Write zero; to allow testbench-read
|
316 |
|
|
elif sosgain_en is False and finalgain_en is False:
|
317 |
|
|
outgain = 0 # Write zero; to allow testbench-read
|
318 |
|
|
g = np.zeros(numsec, dtype=int) # Write zero; to allow testbench-read
|
319 |
|
|
|
320 |
|
|
# Write the coefficients of the sections into their files:
|
321 |
|
|
[write_string_append(filename_b0, repr(x)) for x in b0]
|
322 |
|
|
[write_string_append(filename_b1, repr(x)) for x in b1]
|
323 |
|
|
[write_string_append(filename_b2, repr(x)) for x in b2]
|
324 |
|
|
[write_string_append(filename_a1, repr(x)) for x in a1]
|
325 |
|
|
[write_string_append(filename_a2, repr(x)) for x in a2]
|
326 |
|
|
[write_string_append(filename_g, repr(x)) for x in g]
|
327 |
|
|
write_string_append(filename_finalgain, repr(outgain))
|
328 |
|
|
|
329 |
|
|
|
330 |
|
|
def write_filter_metadata(filename, sos, w_dat_input, w_dat_output, gain_input, w_sect_dat, w_coef, w_frac,
|
331 |
|
|
finalgain_en, sosgain_en):
|
332 |
|
|
""" Writes the filter's meta-data (for VHDL generic) into a text-file for storage."""
|
333 |
|
|
clear_txt_file(filename)
|
334 |
|
|
t = datetime.datetime.now().strftime("%d.%m.%Y - %H:%M:%S")
|
335 |
|
|
sos_np = np.array(sos)
|
336 |
|
|
numsec, _ = sos_np.shape # Nr. of filter sections
|
337 |
|
|
write_string_append(filename, t + '\n')
|
338 |
|
|
write_string_append(filename, 'generic(')
|
339 |
|
|
write_string_append(filename, 'NUM_SEC : integer := ' + repr(numsec) + ';')
|
340 |
|
|
write_string_append(filename, 'W_DAT_INPUT : integer := ' + repr(w_dat_input) + ';')
|
341 |
|
|
write_string_append(filename, 'GAIN_INPUT : integer := ' + repr(gain_input) + ';')
|
342 |
|
|
write_string_append(filename, 'W_SECT_DAT : integer := ' + repr(w_sect_dat) + ';')
|
343 |
|
|
write_string_append(filename, 'W_COEF : integer := ' + repr(w_coef) + ';')
|
344 |
|
|
write_string_append(filename, 'W_FRAC : integer := ' + repr(w_frac) + ';')
|
345 |
|
|
if sosgain_en is True:
|
346 |
|
|
write_string_append(filename, 'SOSGAIN_EN : boolean := true;')
|
347 |
|
|
else:
|
348 |
|
|
write_string_append(filename, 'SOSGAIN_EN : boolean := false;')
|
349 |
|
|
if finalgain_en is True:
|
350 |
|
|
write_string_append(filename, 'FINALGAIN_EN : boolean := true;')
|
351 |
|
|
else:
|
352 |
|
|
write_string_append(filename, 'FINALGAIN_EN : boolean := false;')
|
353 |
|
|
write_string_append(filename, 'W_DAT_OUTPUT : integer := ' + repr(w_dat_output) + ';')
|
354 |
|
|
write_string_append(filename, 'W_DAT_INTF : integer := SET_SOMETHING;')
|
355 |
|
|
write_string_append(filename, 'USE_PIPELINE_CORE : boolean := SET_SOMETHING') # No semicolon - last entry
|
356 |
|
|
write_string_append(filename, ');')
|
357 |
|
|
|
358 |
|
|
|
359 |
|
|
def clear_txt_file(filename):
|
360 |
|
|
""" Clears the content of a text-file.
|
361 |
|
|
If the file does not exist, a new empty file with this name is created.
|
362 |
|
|
"""
|
363 |
|
|
with open(filename, "w") as file:
|
364 |
|
|
pass
|
365 |
|
|
|
366 |
|
|
|
367 |
|
|
def write_string_append(filename, string):
|
368 |
|
|
""" Writes a string to a file.
|
369 |
|
|
If file does not exist, it will create it.
|
370 |
|
|
If file exists, content will be overwritten.
|
371 |
|
|
newline is done automatically.
|
372 |
|
|
"""
|
373 |
|
|
with open(filename, "a") as file:
|
374 |
|
|
print(string, file=file)
|
375 |
|
|
|
376 |
|
|
|
377 |
|
|
def quantize_filter_coefs(sos, g, w_frac):
|
378 |
|
|
""" Quantizes the filter coefficients; from floating to fixed-point
|
379 |
|
|
Note: Python 3's integers are arbitrary precision.
|
380 |
|
|
"""
|
381 |
|
|
sos_d = np.round(np.array(sos) * np.power(2.0, w_frac))
|
382 |
|
|
g_d = np.round(np.array(g) * np.power(2.0, w_frac))
|
383 |
|
|
sos_d = sos_d.tolist() # Convert back to python-3 list instead of numpy-array due to better precision.
|
384 |
|
|
sos_d = [[int(y) for y in x] for x in sos_d] # Python-3 integer.
|
385 |
|
|
g_d = g_d.tolist()
|
386 |
|
|
g_d = [int(x) for x in g_d]
|
387 |
|
|
return (sos_d, g_d)
|
388 |
|
|
|
389 |
|
|
|
390 |
|
|
def get_internal_bit_width(w_dat_input, g, gain_input, w_dat_output, finalgain_en):
|
391 |
|
|
""" Calculates the required internal filter widths (W_SECT_DAT).
|
392 |
|
|
This only works perfectly, if only finalgain_en is true. If there are SOS-gains that are smaller than one,
|
393 |
|
|
it is not accounted for by this script. W_SECT_DAT then has to be manually increased, if the filter performance
|
394 |
|
|
is unsatisfactory.
|
395 |
|
|
"""
|
396 |
|
|
|
397 |
|
|
# Add one bit for the filter-internal bit-width to accommodate filter overshoots up to twice the input value.
|
398 |
|
|
# This is relevant for (near-) full-scale inputs, that would otherwise saturate the sections prematurely.
|
399 |
|
|
w_sect_dat = w_dat_input + 1
|
400 |
|
|
|
401 |
|
|
# Add the bits required due to the filter input gain
|
402 |
|
|
# Note: gain_input has to be a power of two. This script checked this earlier (see "check_user_inputdata").
|
403 |
|
|
w_sect_dat += int(math.ceil(math.log(gain_input, 2)))
|
404 |
|
|
|
405 |
|
|
if finalgain_en is True:
|
406 |
|
|
# If the final filter output gain is enabled and less than one, the internal bit-width has to be increased
|
407 |
|
|
# such that precision is not lost (see below)
|
408 |
|
|
# The filter output gain is the last element in G:
|
409 |
|
|
gain_out = g[-1]
|
410 |
|
|
|
411 |
|
|
""" If the final output gain is smaller than 1, it means that additional bits are needed before the final gain,
|
412 |
|
|
as the values are larger than what will be output after the output gain and our data vectors need to be able
|
413 |
|
|
to cover this extended range. (e.g., if the final output gain is 0.1 and our filter settles to 1, it have to
|
414 |
|
|
be able to store a value of 10 inside our filter.
|
415 |
|
|
If the overall output gain is larger than 1, no additional bits are needed inside our filter, since it
|
416 |
|
|
"creates" the bits/information with the output gain. No check is needed if the extended vector fits into the
|
417 |
|
|
output, as it will just be saturated.
|
418 |
|
|
"""
|
419 |
|
|
if gain_out < 1:
|
420 |
|
|
""" Figure out how many bits are required to accommodate the (larger) value before the gain.
|
421 |
|
|
This is done by solving the (signed-value) maximum of the two (pre/after gain) vectors for the number of
|
422 |
|
|
bits required; like so: 1 / gain_out = 2^(x-1)-1 ==> Solve for x, which is the number of additional
|
423 |
|
|
bits required due to the output gain. Solution: x = ln((2(gain_out+1))/gain_out)/ln(2)
|
424 |
|
|
==> use ceil of this value.
|
425 |
|
|
"""
|
426 |
|
|
numbits_outgain_addtl = int(math.ceil(math.log((2.0 * (gain_out + 1)) / gain_out) / math.log(2.0)))
|
427 |
|
|
w_sect_dat += numbits_outgain_addtl
|
428 |
|
|
|
429 |
|
|
return w_sect_dat
|
430 |
|
|
|
431 |
|
|
|
432 |
|
|
def check_user_inputdata(sos, g, sosgain_en, finalgain_en, w_coef, w_frac, gain_input):
|
433 |
|
|
""" This function checks the user-supplied filter configuration for errors or inconsistencies.
|
434 |
|
|
This makes sure the hardware/filter is not wrongly configured.
|
435 |
|
|
The function may raise runtime errors and/or produces console outputs.
|
436 |
|
|
"""
|
437 |
|
|
sos_np = np.array(sos) # Convert to numpy-array to get the shape (and further functions, see below)
|
438 |
|
|
sos_x, sos_y = sos_np.shape
|
439 |
|
|
g_np = np.array(g)
|
440 |
|
|
len_g, = g_np.shape
|
441 |
|
|
# Check correct format of coefficients. They have to be supplied as list of [b0, b1, b2, 1, a1, a2].
|
442 |
|
|
if sos_y != 6:
|
443 |
|
|
raise RuntimeError('Number of SOS coefficients per section is incorrect.'
|
444 |
|
|
'Deliver section-coefficients as list of: [b0, b1, b2, 1, a1, a2]')
|
445 |
|
|
|
446 |
|
|
# Check if coefficient a0 of sos is fixed to 1:
|
447 |
|
|
# Careful with float comparison.
|
448 |
|
|
for i in range(0, sos_x):
|
449 |
|
|
if np.isclose(sos_np[i, 3], 1.0, rtol=1e-8, atol=1e-9, equal_nan=False) is False:
|
450 |
|
|
raise RuntimeError('SOS-coefficients at index 3 of SOS-array must all be equal to 1. '
|
451 |
|
|
'Deliver section-coefficients as list of: [b0, b1, b2, 1, a1, a2]')
|
452 |
|
|
|
453 |
|
|
# Check, if the length of the output-gain vector corresponds to the selection settings (sosgain_en, finalgain_en)
|
454 |
|
|
if sosgain_en is False and finalgain_en is False:
|
455 |
|
|
if len_g != 0:
|
456 |
|
|
raise RuntimeError('If both SOSGAIN_EN and FINALGAIN_EN are False, G must be an empty list.')
|
457 |
|
|
elif sosgain_en is True and finalgain_en is False:
|
458 |
|
|
if len_g != sos_x:
|
459 |
|
|
raise RuntimeError('If SOSGAIN_EN is True and FINALGAIN_EN is False, G must be as long as '
|
460 |
|
|
'there are sections.')
|
461 |
|
|
elif sosgain_en is True and finalgain_en is True:
|
462 |
|
|
if len_g != sos_x + 1:
|
463 |
|
|
raise RuntimeError('If SOSGAIN_EN and FINALGAIN_EN are True, G must be as long as '
|
464 |
|
|
'the number of sections plus 1. The last entry in G is the final output gain.')
|
465 |
|
|
elif sosgain_en is False and finalgain_en is True:
|
466 |
|
|
if len_g != 1:
|
467 |
|
|
raise RuntimeError('If SOSGAIN_EN is False and FINALGAIN_EN is True, G must be a list of length 1, '
|
468 |
|
|
'containing the filter output gain.')
|
469 |
|
|
|
470 |
|
|
# Check, if the filter is not too long. Maximum nr. of sections: 255 (hardcoded in VHDL)
|
471 |
|
|
if sos_x > 255:
|
472 |
|
|
raise RuntimeError('Too many filter sections desired. Max. nr. of sections is 255 (Hardcoded in VHDL).')
|
473 |
|
|
|
474 |
|
|
s = 'Number of SOS this filter comprises: ' + repr(sos_x)
|
475 |
|
|
print(s)
|
476 |
|
|
|
477 |
|
|
# Check, if the section-gains/output gain could be disabled:
|
478 |
|
|
if sosgain_en is True:
|
479 |
|
|
if finalgain_en is True:
|
480 |
|
|
len_iter = len_g - 1
|
481 |
|
|
if np.isclose(g_np[len_g - 1], 1.0, rtol=1e-8, atol=1e-9, equal_nan=False) is True:
|
482 |
|
|
print('Detected that the final filter gain is unity. FINALGAIN_EN could be set to False.')
|
483 |
|
|
else:
|
484 |
|
|
len_iter = len_g
|
485 |
|
|
gain_notunity = 0
|
486 |
|
|
for i in range(0, len_iter):
|
487 |
|
|
if np.isclose(g_np[i], 1.0, rtol=1e-8, atol=1e-9, equal_nan=False) is False:
|
488 |
|
|
gain_notunity = 1
|
489 |
|
|
if gain_notunity < 0.5:
|
490 |
|
|
print('Detected that all section-gains are unity. SOSGAIN_EN could be set to False '
|
491 |
|
|
'(Disables output-gains of section); Less hardware-resources used.')
|
492 |
|
|
|
493 |
|
|
# Check if the delivered coefficients are within the range of the number-space covered by the
|
494 |
|
|
# coefficients/fractions.
|
495 |
|
|
# Q-Notation: Signed Qm.n format.
|
496 |
|
|
m = int(w_coef - w_frac) # Nr. of integer bits
|
497 |
|
|
if m < 1:
|
498 |
|
|
raise RuntimeError('W_COEF - W_FRAC must be >= 1. Terminating.')
|
499 |
|
|
n = int(w_frac) # Nr. of fraction bits
|
500 |
|
|
coef_min = -1.0 * float((2.0 ** (m - 1)))
|
501 |
|
|
coef_max = float(2.0 ** (m - 1) - 2.0 ** (-1 * n))
|
502 |
|
|
# Check, if all coefficients are in range:
|
503 |
|
|
for i in range(0, sos_x):
|
504 |
|
|
for k in range(0, sos_y):
|
505 |
|
|
if sos[i][k] > coef_max or sos[i][k] < coef_min:
|
506 |
|
|
s = 'Coefficient SOS[' + repr(i) + ',' + repr(k) + '] out of range with value ' + repr(sos[i][k])
|
507 |
|
|
print(s)
|
508 |
|
|
s = 'Specified Q-format: Q' + repr(m) + '.' + repr(n)
|
509 |
|
|
print(s)
|
510 |
|
|
s = 'Allowed range: ' + repr(coef_min) + ' < Coeff. < ' + repr(coef_max)
|
511 |
|
|
print(s)
|
512 |
|
|
raise RuntimeError('Coefficient out of range for specified Q-format. See console-output for details')
|
513 |
|
|
|
514 |
|
|
# Check the gain-coefficients for conformity:
|
515 |
|
|
for i in range(0, len_g):
|
516 |
|
|
if g[i] > coef_max or g[i] < coef_min:
|
517 |
|
|
s = 'Coefficient G[' + repr(i) + '] out of range with value ' + repr(g[i])
|
518 |
|
|
print(s)
|
519 |
|
|
s = 'Specified Q-format: Q' + repr(m) + '.' + repr(n)
|
520 |
|
|
print(s)
|
521 |
|
|
s = 'Allowed range: ' + repr(coef_min) + ' < Coeff. < ' + repr(coef_max)
|
522 |
|
|
print(s)
|
523 |
|
|
raise RuntimeError('Coefficient out of range for specified Q-format. See console-output for details')
|
524 |
|
|
|
525 |
|
|
# If there is only a single section, check that finalgain is used, and not sos-gain.
|
526 |
|
|
# Otherwise, the section-width calculation is not correct / the filter saturates.
|
527 |
|
|
if sos_x == 1:
|
528 |
|
|
if sosgain_en is True and finalgain_en is False:
|
529 |
|
|
raise RuntimeError('When only using one section, use the final gain and not the SOS-gain. '
|
530 |
|
|
'Otherwise, section-data widths not calculated correctly.')
|
531 |
|
|
|
532 |
|
|
# Check, if the filter-gain is specified as a power of two and as integer:
|
533 |
|
|
if isinstance(gain_input, int) is False:
|
534 |
|
|
raise RuntimeError('GAIN_INPUT must be an integer (and power of two)')
|
535 |
|
|
if gain_input < 1:
|
536 |
|
|
raise RuntimeError('GAIN_INPUT must be >= 1')
|
537 |
|
|
if ((gain_input & (gain_input - 1)) == 0) is False:
|
538 |
|
|
raise RuntimeError('GAIN_INPUT must be a power of two')
|
539 |
|
|
|
540 |
|
|
# Check the value of the filter's overall output gain:
|
541 |
|
|
if finalgain_en is True:
|
542 |
|
|
gain_out = g[-1]
|
543 |
|
|
if gain_out > 1:
|
544 |
|
|
print('NOTE: final filter output gain is > 1. This could be covered/supported by the input gain'
|
545 |
|
|
'(and maybe an output gain < 1) for increased filter precision (but higher resource utilization'
|
546 |
|
|
'due to longer filter-internal vector widths)')
|
547 |
|
|
|
548 |
|
|
|
549 |
|
|
if __name__ == "__main__":
|
550 |
|
|
main() # Simply execute the main script.
|