Loading

Writing a better noise-reduction algorithm for Arduino

Noise can sometimes be a significant pain when reading voltages using Arduino's analog inputs. By 'noise' I mean unwanted fluctuations in the read signal. When the voltage of a noisy line is read via repeated calls to analogRead(), the returned values will vary up and down a little. A good circuit layout can help immensely, but occasionally you may need to read a voltage where you don't have control over the cleanliness of the signal. While there's no shortage of Arduino helper classes to help clean up analog input values, I wasn't able to find any that met my specific requirements:

  1. Be able to reduce large amounts of noise when reading a signal. So if a voltage is unchanging aside from noise, the values returned should never change due to noise alone.
  2. Be extremely responsive (i.e. not sluggish) when the voltage changes quickly.
  3. Have the option to be responsive when a voltage stops changing - when enabled the values returned must stop changing almost immediately after.
  4. The returned values must avoid 'jumping' up several numbers at once, especially when the input signal changes very slowly. It's better to transition smoothly as long as that smooth transition is short.
  5. A very small decrease in accuracy is permissible only if the option in point #3 is enabled.

The following documents the thought process behind building the algorithm use in my ResponsiveAnalogRead library for Arduino.

Visualising the problem

Here's a simulation of a noisy input voltage. Imagine this is the signal read from a potentiometer that you can control.

See the Pen Responsive Analog Read with noise reduction #0 (demo analogRead) by Damien Clarke (@dxinteractive) on CodePen.

That's a good benchmark for the amount of noise I want to filter out. It's quite a lot.

First things first - let's try eliminating most of the noise from an unchanging signal. How do we do this?

Some people make it so that if the input value doesn't change enough, then the output value doesn't change at all. While it can remove noise it also makes the output values feel very 'clicky'. They jump up 3 or 4 values at a time, and if used on potentiometers it can make fine tune adjustments infuriating.

You might think about averaging out the last few input values. This is definitely on the right track, small noisy fluctuations are smoothed over, but the result feels quite laggy and sluggish.

The tool of choice for these kinds of noise reducing problems is often an exponential moving average. That's what I'm going to use.

The exponential moving average

This algorithm goes by a few different names depending on the field you're in, and it appears in a few different forms. It's a very simple algorithm that behaves similar to an average of the last few input values, but it weights recent input values much more strongly, and the influence of older input values gradually moves toward zero. You end up with a smooth elastic kind of behaviour as the output drifts toward the current value of the input.

The code:

output += (input - output) * a;

Input is the value to be smoothed, output is the smoothed value, and the variable a is a value between 0 and 1 that controls how smooth the output is. a=1 applies no smoothing, while values of a that are close to zero results in a very smooth. output. If we were to use this with an analog input then the code may look something like this:

float output = 0;
function loop()
{
  output += (analogRead(A1) - output) * 0.5;
  Serial.println(output);
  sleep(100);
}

From here on in I'm going to refer to a as snap. A snap of 1 is extremely snappy, a snap of 0.5 is elastic, a snap of 0.1 gets pretty loose, and a snap of 0 never moves anywhere.

Give it a go. Snap is set to 0.4.

See the Pen Responsive Analog Read with noise reduction #1 (exponential moving average) by Damien Clarke (@dxinteractive) on CodePen.

Not bad, you can imagine that a lower snap value might remove nearly all the noise. This solution satisfies points 1 and 4 on the list of "things this algorithm should do". It comes at a cost though, the output values become slower and slower to reach the actual value of the input voltage. We'll have to start getting creative to also satisfy points 2 and 3.

Being responsive

To be responsive and have a quick accurate reading, obviously we can't have a low snap value, it's too delayed. But on the other hand we need a low snap value to filter out noise. It sounds like we can't win...

But! Consider that we don't need both those things at the same time. We don't need to filter out noise when the voltage is changing madly up or down because the negative effects of noise can't even be noticed in those cases. We also don't need to be responsive when the input is unchanging. So a trick to try is to change the amount of snap based on the speed of the change.

We need a function to turn the speed of a voltage change into an appropriate snap value. But which function to start with? We want no change in speed to result in an output close to zero, so when the smooth value is close to the input value it'll smooth out noise aggressively by responding slowly to sudden changes. And we want medium and higher speeds to get closer and closer to (but not go over) a snap value of 1. Remembering your high school maths classes, is there a function / graph that fits a similar description, where a line starting with a slope and gets flatter and flatter as it approaches a limit? Yep, a hyperbola.

f(x) = 1/x

The first issue with using the hyperbola is that it also has a vertical asymptote. We can "remove" this by adding 1 to x in the equation, because x will never been negative (as speed is never negative).

f(x) = 1/(1+x)

Next problem - right now when x is at 0, f(x) is at 1, and when x gets larger f(x) gets closer to zero. It's upside down for our purposes. So we must flip it by subtracting the result from 1:

f(x) = 1 - (1/(1+x))

With that as a basis to turn speed into snap, and a couple of minor adjustments we now have this, an exponential moving average that is adjusted depending on the speed of the voltage change. It really works quite well, sticking to quick bursts of motion easily, but also avoiding noise when stopped. I think point 2 has been taken care of.

Also you can check out the Javascript that makes the demo run if you're interested in the details of what else had to be changed to make it work well.

See the Pen Responsive Analog Read with noise reduction #2 (variable snap) by Damien Clarke (@dxinteractive) on CodePen.

Applying the brakes

The above is probably good for most cases, but I've specifically included point 3. One of my main use cases is to read changes in the rotation of a set of potentiometers, and display the current changing value on a screen. Whenever any potentiometer changes a value, it'll show that value on the screen. Suddenly we need to stop every little erroneous flicker of a change on any input, to ensure it only happens when the potentiometer is being actively turned by a person. If any value changes are to sluggish or pop out of nowhere due to noise, it becomes really noticeable.

So we need to put a stop to things. We need to make that output value decide when it's done sliding into position and when it's time to cease moving, to minimise the amount of responsive value changes over time. This was originally done using a few things:

  1. Introduce the idea of "sleep" - when the output value decides to ignore increasingly small changes.
  2. When it sleeps, make it less likely to start moving again, and make it able to wake up and begin responding as normal.
  3. Classify changes in the input voltage as being "active" or not, so we can set a timer and tell when it hasn't been sufficiently active for a while. That lack of activity can tell it to sleep.
  4. Require a threshold of movement so we can define just how much movement needs to occur to count as being "active".

Finally this means we can stop the output value on a dime, while retaining all the other behaviours we want from the previous steps. As always, there's trade-offs, this time brought about by the act of sleeping.

  • The output value is not necessarily as precise as it was before. Your true average voltage might be a few mV higher or lower than the output value, because now the algorithm can decide that it can stop moving when it's close enough, instead of always drifting toward that true precise value. If accuracy is more important to you than minimising the amount of value change over time, then it's likely better if you don't enable sleep.
  • Very slow sweeping movements on the input voltage will be a little bit stop-and-start on the output. Try it for yourself below. I found this to be fine for my purposes because the output still moves smoothly when it decides to stop and start again. It doesn't jump up 5 values in a single step which is the main thing I wanted to avoid surrounding this part of the problem, and it continues to minimise the amount of value changes over time.
  • Sleeping can make it difficult to hit values very close to either extreme (0 or 1023) because the responsive value will tend to sleep before it reaches those end points. Compensating for this is quite easy - values close to either end are exaggerated outward, so movements in those small end regions are more likely to trigger a wake up and pull the responsive value further to the ends. Finally the output values are capped so they don't fall outside normal bounds. The result feels very natural given the size of the regions and the amount of extra influence they exert.

For those reasons, sleep is an optional feature in the ResponsiveAnalogRead library.

Read the comments in the Javascript source on the demo below to see the exact changes.

See the Pen Responsive Analog Read with noise reduction #3 (complete with sleeping) - to be ported to C++ for Arduino by Damien Clarke (@dxinteractive) on CodePen.

There you have it. That's how I built a responsive noise-reducing algorithm. All the features above have been implemented as part of my ResponsiveAnalogRead library which is available on Github.

Download ResponsiveAnalogRead on Github