When you are working on an embedded system and sampling data from a sensor, sometimes you just need a quick and simple low-pass filter to smooth out a noisy signal or a high-pass filter to remove a (perhaps slightly varying) DC offset. If you are not particularly concerned about the exact characteristics of the filter response (such as cutoff frequency, stop-band attenuation, pass-band ripple, etc.) but just need something that is halfway decent and can be implemented quickly, then you might find the following two filters interesting.
The two filters you will learn about in this article are the exponential moving average (low-pass) filter and the DC blocker (high-pass) filter. They are both dead-simple to implement. You just need to pick a smoothing/blocking constant and keep track of the previous output value, and for the DC blocker also the previous input value.
Exponential moving average filter
You might already be familiar with the simple moving average filter, which is basically a FIR filter of a length N where all coefficients are equal to 1/N. This requires you to keep track of the latest N input samples – and the more smoothing you want, the more samples you have to store. Since all coefficients are the same we can say that all previous input samples are equally weighted.
The exponential moving average, or exponentially weighted moving average, is quite similar to the simple moving average. But instead of giving equal weight to all previous input, the weight of previous samples decreases exponentially as the samples get older. Because of this property the filter is also known as a fading memory filter, which I think is very descriptive. The filter is implemented with the following difference equation:
where y(n) is the output, x(n) is the current input, y(n – 1) is the previous output and α is a constant with a value between 0 and 1. For a more descriptive name, we can refer to α as the smoothing factor. Since the output depends on a previous output, the filter is by definition an IIR filter. Just by looking at the difference equation, we can see that choosing a smoothing factor close to 1 will give more weight to the current input sample and less weight to previously computed output values (which in turn are computed from previous input samples). Giving more weight to the current input value (i.e. a higher α), means that sudden fluctuations in the input will have a larger influence on the output. Thus choosing a lower smoothing factor will lead to a smoother output.
To understand the filter a bit better, let us transform the difference equation to a Z-domain transfer function (see this for a brush-up on the Z-transform) which is defined as:
We will start by grouping all y terms on the left hand side of the equation and x terms on the right:
Now taking the Z-transform on both sides yields:
Now, we can factor out Y(z) and X(z):
Finally, solving for Y(z)/X(z) gives us the transfer function:
The transfer function shows that the filter has no finite zeros, but it has a pole at z = 1 – α where the denominator equals zero and the transfer function goes towards infinity. Moving this pole closer to unity (by choosing a smaller α) will lead to a steeper roll-off. We can visualize this by plotting the magnitude of the transfer function in Python (using scipy.signal.freqz) for different values of α:
Notice that the filter is best suited for signals where the frequencies of interest are rather close to DC, where we can choose a low smoothing factor which gives us a steeper roll-off and higher stop-band attenuation.
To implement the filter in C, we basically copy the difference equation shown earlier. However, instead of y(n) and x(n) I have given the variables some more descriptive names. We need to keep track of the previously computed output value and update this after each iteration. For example, reading samples from a sensor and applying the filter might look something like this:
static float const alpha = 0.3f;
static float prev_output = 0;
int main()
{
while (1)
{
if (sensor_is_ready())
{
float input = sensor_read();
float output = alpha * input + (1 - alpha) * prev_output;
prev_output = output;
// ... do something with output
}
}
}
In the plots below I have taken a low frequency signal with high frequency noise added to it, and run it through the filter with three different values of α:
Here we clearly see the smoothing effect of the filter. You might need to fiddle around a bit with the smoothing factor to get it just right.
DC blocking filter
Next up is the DC blocking filter which is a first order high-pass filter. In its most basic form it is implemented with the following difference equation:
where y(n) and y(n – 1) are the current and previous output, x(n) and x(n – 1) are the current and previous input values, and β is the blocking factor which is a number between 0 and 1. To get an intuitive sense of how the filter works, we can look at the difference equation and see that the output is given as the change in input added to the previous output. The effect of this is that the change in the input is accumulated in the output – thus we might also call the filter a change accumulator. If there is no change in the input (i.e. a DC), the output will converge towards zero. The blocking factor determines how much of the previous output we should keep in the next output. Setting it close to zero results in the output predominantly consisting of the change in input (i.e. high-frequency content), while setting it close to unity keeps more of the accumulated change (i.e. low frequency content) in the output.
Following the same procedure as with the exponential moving average filter, we find the transfer function of the filter to be:
Here we a have a zero at DC when z = 1 and a pole at z = β. Placing the pole close to DC (i.e. β close to 1) should lead to a sharper transition. Let us plot the magnitude response in Python for different values of β:
As expected, β values close to 1 leads to a sharp transition around DC keeping most of the low frequency content, while β values closer to 0 attenuates the low frequencies more and also results in a bit of gain at high frequencies. We can correct for this by adding a gain factor, G, to ensure that the filter has unity gain at the Nyquist frequency:
The difference equation with gain correction is then:
And the transfer function becomes:
Plotting the magnitude response for the new transfer function we get this:
If our signal has an absolutely fixed DC offset that we wish to remove, we probably want β close to unity in order to keep as much of our desired signal as possible. However, this also leads in a slower filter. If we have a wandering DC offset and the frequencies of interest are far from DC, we can set β a bit lower to attenuate some more of the low frequencies with the added benefit of a faster filter response. Let us visualize this by applying the filter with different β values to the same signal as we did with the exponential moving average filter, which has a DC offset. The implementation in C (with gain correction) could look something like this:
static float const beta = 0.99f;
static float const gain = (1.0f + beta) / 2.0f;
static float prev_output = 0;
static float prev_input = 0;
int main()
{
while (1)
{
if (sensor_is_ready())
{
float input = sensor_read();
float output = gain * (input - prev_input) + beta * prev_output;
prev_output = output;
prev_input = input;
// ... do something with output
}
}
}
And gives the following results:
Notice how β = 0.99 keeps almost all of the original signal, but removes the DC offset rather slowly. Lower β values removes the DC offset faster but also attenuates the low frequency content of the signal. If we are only interested in the high frequency content the signal, we could probably set β even lower.