We’ve now presented several approaches to filtering within an FPGA on this blog. We talked about two of the simplest filters, a generic filter implementation, a cheaper version of the same, and even how to build a moving average filter.
What we haven’t discussed is how to test these filters.
Just to make matters worse, these filters are only the tip of the iceberg of the filters we’d like to test. Not only do we have the current set of filters to test, but I’d also like to present Verilog logic for Symmetric filters, Half-band filters, Hilbert transforms and even some slower versions of those same filters.
Each of these filters is going to need a test bench. At the same time, these test benches are going to have a lot of code in common between each other. Let’s consider, today, what functionality a generic Verilator based filtering test bench requires, so that we can then create a filter test bench engine that can be used, and modified if needed, to test all of these filters.
Abstracting a logic filter
The first step is going to be abstracting what a filter looks like in
general so that one basic test bench can support many filters. For this,
we can draw upon the design of our most
generic filter, and see a
set of ports looking roughly like Fig 1. There’s an
to clear the filter’s internal memory, an
i_tap_wr signal to indicate that
the filter is being loaded with new taps through
i_tap, and an
to indicate both that a new sample is ready at
i_sample and that a sample
is ready to be read out at
Now, given this form, let’s consider the questions that need to be answered by a test bench:
Does the filter have the impulse response we expect it to have?
Does this filter faithfully apply the impulse response to any signal we give it?
Will a carefully constructed input stream overflow the filter?
based filters in particular also have some properties that will affect
how we interact with them from one test bench to the next. For many of our
filters, these are
parameters within our design. Those same parameters will
affect how we interact with the filter to answer the questions above.
The properties that a test harness for all filters will need to know are:
IW, the number of bits per input sample
TW, the number of bits per tap
OW, the number of bits per output sample
DELAY, the delay between an impulse input and the first sample that responds to that input.
NTAPS, the number of taps in the filter
NCLOCKS, the minimum number of clocks between successive input clock enables
Our purpose today will be to build a generic filtering test harness. The goal for this harness will be to handle all of the various filter properties we just discussed above, to interact with a filter of the form shown in Fig 1., and then to answer about the filter’s performance based upon the questions above. You can imagine that this test harness will have the form shown in Fig 2, where the filter itself fits within a virtual socket in this virtual test harness.
The harness won’t quite be a complete test bench, but rather a component of a complete test bench. As such it should make it easier to build filter test benches using this harness.
Building a test harness
We’ll need to make the C++ class that encapsulates this object easily inheritable, as (for example) the meaning of the taps loaded into one filter (such as a generic one) might be very different from another (such as a symmetric filter). Hence, most of these methods will be virtual.
Let’s outline what one such filter test
might look like. We’ll let it inherit from a basic
class, such as we’ve discussed
class, if you recall, primarily provided us with the ability to
the clock by raising and then lowering the clock input pin, as well as
recording our simulation to a VCD
file. The final function of
class is to give us access to a simple
reset() function which will set
i_reset pin and toggle the clock.
We’ll define our test harness class with some defaults–things that can be
overridden later by the test bench that uses this harness. Further, since
we don’t know what the class name of our actual filter is going to be until
build time, we’ll make the test harness class,
FILTERTB, a template class.
That way, the compiler can fill in the template details once they are known.
Remember all of those properties we said that a digital filter would have? These will be variables of our object.
The test harness will need to know what these properties are. Hence, we’ll start out by handling the setting and referencing of these various properties. This won’t actually change anything about the Verilator generated filter code itself, nor will these values connect to the values often set by parameters within the filter (though it probably should), these routines will just let the actual test bench tell tell the test harness what properties are associated with the filter.
There’s the number of bits allocated to the input sample, IW.
OW is the number of bits allocated to output samples.
TW is the number of bits allocated to each of the taps.
DELAY is the number of clocks between an input and the first filter response that it impacts.
may require multiple clocks between each global CE signal,
We’ll keep track of this feature/capability in the CKPCE, the clocks
per CE parameter.
The minimum number of clocks per CEs is just one, something enforced above.
NTAPS is the number of filter coefficients, or taps in this filter–it is the
length of the impulse
Our test harness will use this to know how many samples following an
impulse need to be examined.
Now with those properties aside, we can start looking at methods (actions) this filtering test harness will need to handle. As you’ll notice from below, we’ll make most of these virtual–so that different types of filters can change them if need be.
tick() method as a generic method that will call
and simply step the clock by one tick. It will also record any results into
a VCD file
(or other file) that may need to be recorded–if desired.
You may remember some time ago how I recommended debugging DSP algorithms graphically. That recommendation applies to filters as well. For those cases where you want to output the results of your filtering operation to examine, our test harness allows us to open a file wherein we’ll place both input values and output values.
In order to apply test vectors and receive independent results, we’ll need to be able to reset the filter to clear any internal state.
For our test structure, we want to be able to load
coefficients into our design. This will involve sending a vector of integers,
of a given length, to the
test() method is nearly identical to the
apply() method, with the
single exception that the
first. This will be important for
sending any particular test vectors to the
filter, and for making certain
that there aren’t any unintended consequences in the result.
Further, rather than calculating the impulse response. over and over again, we’ll calculate it once and cache the results. That means that any time there is a change in the filter’s coefficients, we’ll need to be able to clear this cache.
While we have a method above for loading coefficients into our filter, let’s create another method that not only loads coefficients into our filter but also verifies that the resulting impulse response contains the coefficients that we want.
As with many of these methods, this method is virtual so that filters with less than generic tap structures, such as symmetric filters as one example, can overload this function and provide their own test method.
With all of the above functionality, there’s no reason why we can’t apply
particular known inputs to this
in order to measure its
Since this is likely to be a common request, we’ll make a method for doing
this and place it into our test harness as well. The
nfreq describes how
many frequencies we are interested examining, and the result will be placed
The algorithm will work by placing complex exponential test vectors into the
We can use
mag to control the amplitude of these test vectors,
mag of one being the largest magnitude the
will accept without overflow.
Finally, we’ll add two further canned tests. The first,
checks whether a carefully constructed input designed to overflow the
does or does not. A true value means that the
passed this test.
The second test is appropriate for lowpass filters only. This test will measure the highest passband frequency, the lowest stopband frequency, the depth of the stopband, and the ripple across the passband. It will work for most lowpass frequency response functions, but perhaps not for all lowpass filters.
Together, these are the methods (actions) we’ll teach our test harness how to apply to all filters, These methods are declared in this C++ header file. We’ll discuss how to implement them in the next section. For now, let me challenge you to ask the question: have we gotten everything? Are there other particular capabilities that a generic filter test harness should have–capabilities that we’ve overlooked? Feel free to write me and tell me about anything I’ve missed.
Test harness method details
Let’s now turn to the details of how this test harness is implemented. In general, we’ll just work our way through the various methods associated with this test harness C++ class, discussing how to implement each along the way.
Handling different data widths
The first thing we are going to implement is a helper method for adjusting the
number of bits in a number. Since
will return numbers with only
N bits in them, we’d like to be able to
sign extend these values to their full
Likewise, if the value is unsigned, we’ll also like to extend the value–this time with zero bits.
Advancing the state of the test bench
As you may recall from the discussion of the
class, it has a
tick() method within it to handle toggling the clock for us.
What it doesn’t have is a means of recording samples for later study
with Matlab or
Octave. We’ll create that here.
As you can see from above, anytime the
i_ce flag is true, the input value
and corresponding output value will be written to a file (if open) for
Resetting the Filter
reset() function just sets the
i_reset signal for one clock tick. Just
to be safe, we’ll make sure the other indication lines are zeros as well.
We’ll use this function to make certain the filter starts from a clean state at the beginning of any tests.
There is a consequence, though, to creating a clean test bench that only provides expected and reasonable inputs, such as setting the reset with all of these other inputs set to zero. In particular, this method of testing logic may not be as complete as a more formal approach that will examine all possible inputs to our filter module–both the reasonable as well as the unreasonable.
In our approach today, we’ll just stick to the reasonable filter inputs.
Applying the filter to a known input
The core function of any
linear filter operation
is to apply the
to a given set of data samples. That’s the purpose of the
below. We’ll start by making sure we’re not holding wires high from any prior
context that didn’t clean up properly behind itself.
Now that we’re set up, let’s walk through each of the input samples.
For each input sample, we’ll place it on the
i_sample input to the
and then read the result from the
o_result output of the
Although all of the
we’ve looked at so far accept one input value
and produce one output value on each clock, not every
is of this type.
There’s a lot to be gained by sharing resources across clocks if the
sample rate can be guaranteed to be less than half of the system clock rate.
To make sure that we can still test
of this variety, we’ll tick our clock
m_nclk-1 additional times
Loading new filter coefficients
For any filter,
that doesn’t have fixed taps, we’ll need to be able to load a new set of
coefficients into the filter,
As we’ve designed this, doing so requires setting the
i_tap_wr input, and then ticking the clock once to load each new coefficient
into the filter.
These will need to be rebuilt, but we’ll hold off on that task until they are referenced.
Getting the results from a test vector
A function that is very similar to the
apply() method is the
below. The idea behind the
test() method is that we wish to determine the
response to a given test vector. This method differs from the
method in only a few small respects.
We then call the filter
nlen times to feed the test vector into the
Well, not quite. We’re going to pass the test vector followed by a series
of zeros–one for each delay value. That way, we can quietly remove any
delay effects from the test vectors response–something not done within the
Here’s the next difference: after the first
nlen samples, the next several
samples are all set to zero.
Some filters will require multiple clocks to come to an answer. Let’s make sure we give those filters the clocks they need. We’ll start by making sure that the i_ce line is zero, and then we’ll tick off any extra clocks we are missing.
Further, we’ll only record the results after an initial
DELAY() number of
samples have been passed into the
As you’ll see shortly, this
test() method is going to feature prominently
in any more sophisticated test—such as measuring the
in the next section.
Measuring the Impulse Response
The impulse response of a
is defined by its response to a single non-zero value.
Many of our test components will depend upon this value, so it’s important
that we have a means to both initially measure it, and then to use that
measurement later without requiring it to be measured over and again. This is
the purpose of the
 operator within the test
Our operator starts out with some very basic parameter checks, just to make certain that we handle responses for out of bounds requests properly.
Measuring a filter’s impulse response is actually just as simple as creating an input to the filter with one non-zero entry (the first one), and then recording how the filter responds to it, hence the name impulse response. Here we do exactly that, save that we we use the maximum negative value for our impulse. We pick this choice for two reasons: first, because the maximum negative value is larger in magnitude than the maximum positive value and so this becomes a test for overflow. Second, by using a really large number, we can also make certain that our result isn’t lost when/if any bits are dropped at the end.
Once we get our test vector back, the impulse response coefficient we are looking for will be the response from the filter times the value of our impulse. Hence, we’ll divide the initial impulse value back out and record each sample of our impulse response.
Once we’ve determined the impulse response once, we’ll just look it up for future requests–sparing us the cycles of calculating it over and over again.
Checking whether the filter load worked
testload() function is simply a
coefficients function followed by a request for the
and then a comparison of whether or not the
truly matches the one we wanted.
We’ll use the boolean,
mismatch to capture the condition where things
don’t match. This will allow us to place the assertion that the impulse
response matches the one we loaded at the end of the routine.
We’ll now compare the measured coefficient,
(*this)[k], against the
coefficient we expect, found in
We’ll make the assertion here that everything matched above.
We’ll also insist that any coefficients beyond the ones we expect to set should have been read as zero.
This function has been made virtual so that other filter types, such as those whose coefficients are partially fixed (symmetric), half-band, Hilbert, etc), may adjust both how their actual coefficients are loaded, and then also how those loaded coefficients correspond with resulting impulse response.
Checking for overflow
The purpose of the
test_overflow() routine is to test whether or not, if the
is given a carefully constructed input designed to overflow the
that the filter will return the correct result. If the
response doesn’t match what we think it should,
this will be an indication of a potential overflow.
The waveform (test vector) itself is chosen from all maximum values, as shown
in Fig 3. In this figure, a desired
is shown in black. The discretized version of that same response is shown in
blue–those are the results that the
 operator will return. The samples
in red show the test vector we’ll use.
The magnitude of the coefficients in the test vector is full scale–whether
the maximum positive integer that can be represented in
IW bits or the
maximum negative integer. The signs are adjusted so that when the test
sequence lines up with the internal taps of our
as shown in Fig 3,
the individual coefficients times the test vector’s coefficients will produce
the maximum possible positive value. Once all these maximum possible positive
values are accumulated, we’ll have the maximum output value that the
can produce. Any failure to match this maximum output value will be an
indication that the filter
couldn’t keep up with full-scale values.
That will be our approach below.
Now that we’ve put this test signal together, let’s see what the filter does with it.
Now, for every one of those filter outputs, …
It would also be nice to have some confidence that the test vector worked
as desired. This is the purpose of the
If the test vector works as desired, then it should hit (at some point) a
sweet spot where all of the signs line up. If this happens, then we set
all to true.
Any failure of the output to match our calculated (expected) value indicates
that something went wrong–perhaps a
overflow, perhaps something else in the implementation of the
Either way, you’ll want to know.
Here are two ways to find out: the assertion below, which will cause the test
to end abruptly with an error, and a boolean
pass. Hence, if you comment
out the assertion, the
pass boolean will still tell you the answer.
Finally, now that we are done comparing every output with its expected value, let’s clean up any allocated values and return.
As you may have noticed, this test not only checks that the output doesn’t overflow, but it also offers a rudimentary check that the filter even works in general–by comparing the filter’s output to a locally calculated convolution. However, as a general convolution checker, this routine is not sufficient. For this reason, we’ll need some other tests to fully convince us that this filter works.
Saved for Later
and a second function to answer the question of how well did the filters’s frequency response actually match a desired response. As we mentioned above, this second function (as built) will only be relevant to lowpass filters.
Building these two components of our test harness, however, will have to wait for another day and another post.
And David girded his sword upon his armour, and he assayed to go; for he had not proved it. And David said unto Saul, I cannot go with these; for I have not proved them. And David put them off him. (1Sam 39)