Get Help
Signal Filtering Techniques
Introduction
As you may have noted from previous lessons, sometimes the sensor reading function returns strange values. For example, an ultrasonic range finder reads (erroneous) really large distances from time to time, even when there is a near object in front of it. In this lesson we will learn some simple techniques that will help us to reduce the effects of the noise and false readings from the robot’s sensors. Also, we will introduce here some basic concepts about the analog to digital signal conversions used in Sparki.What You’ll Need
- A Sparki.
Analog to Digital Signal Conversions
Before start to work with Sparki’s sensors, let’s see some basic concepts about the way that our small robot receives information from the external world. As you may know, Sparki’s brain is a small digital microcontroller (MCU), specifically an Atmel ATMega32U4. And by digital we mean that in its inside, it deals with digital signals composed of only ones and zeros (where a digital one is represented by 5 Volts and an digital zero is represented by 0 Volts). Thus, in order to work with analog signals, which in our context are basically signals that can vary from 0 to 5 V in a continuous way, Sparki’s MCU has to translate them into ones and zeros. There are several techniques to do this conversion and Sparki’s MCU includes from factory an Analog to Digital Coverter (ADC). Let’s see first how it works in a generic way, so after having a basic understanding, we can deal with the specific details of our ADC. Please take a look to the following figure:Original image from Wikimedia Commons.
There, the green line represents the continuous analog signal, while the yellow surface is the area under the curve. As our small MCU can’t do much with that curve, the ADC enters in scene, taking samples of only a subset of the signal’s points (or values), and expressing each of these values with a binary number (an integer). As you may have guessed, the blue points in the previous figure are representing the sampled points. So here, a few questions appear. For example: how many samples should the ADC take in order to represent the original signal? That’s a good question, so let’s talk about it a bit more.ADC Sampling Rate
We are not selecting the ADC hardware here. In fact fact we already have the ADCs in our Sparki’s MCU. And each ADC has some specifications given by the manufacturer. So, the question about how many samples should we take, could be also asked as how many samples can we take with our current ADC hardware. And the samples that our ADC can take per second is a feature called Sampling Rate. In general, higher sampling rates will let us to convert faster changing analog signals. Think about it: if a signal changes a lot between 2 sample points, the ADC will loose all that intermediate information. On the other hand, if we need to store the signal’s digitized representation, more sampled points per second will result in the need of more storage space. Thus, a higher sampling rate is not always necessary. In fact, most modern ADCs has a configurable sampling rate (if you are curious about the low level details of the ADC configuration, you can take a look to the ATMega32U4 complete datasheet’s Analog to Digital Converter chapter). So, to help answering our original question there is a theorem (surprised?), called Nyquist–Shannon sampling theorem. Although it’s a bit complex, the final conclusion from that theorem, in everyday language is that our sampling frequency should be “twice the highest frequency of interest”. Sparki’s ADC has a maximum sampling rate of 15 KSPS (Kilo Samples Per Second), often called a 15 KHz sample frequency (being the KSPS a much more accurate term for describing ADC sample rates). So, make your conclusions about the maximum frequency of the signal that can be converted with this hardware. A note here: Although the sampling rate in theory should be higher, the specific ATMega32U4 ADC works really better with frequencies under 4 KHz, due to the way in which it’s gain stage has been designed. Happily, this is well enough for most of our needs using Sparki’s sensors.ADC Resolution
Another important question about the conversion of a signal to the digital domain is the size of the numbers used to store each sampled point. For example, if we use just 8 bits to store a sampled value, we can represent it with only 256 different numbers. So, any point between the possible representations will be rounded to an approximate value. In the following figure, for example, we are showing an ADC technique (with 3 bits of resolution) where each sampled value (blue points) from the original signal (red curve) is mapped to a binary number below the sampled point, resulting in the blue signal. We can see clearly that the approximation is not really good with just 3 bits in this case: The quantity of bits used to store the sampled values is called the resolution of the ADC. As with the sample rate, a higher resolution means in general a better representation of the signal, but it also takes more memory space when storing it. And will need more processing power if we want to work mathematically with that signal. Sparki’s ADC features a 10 bits (maximum) resolution. This means that the analogRead function used inside the libraries for sensors like the Infrared Reflectance Sensors, or the Light Sensors can return numbers between 0 and 1023 (since 2^10 = 1024). Again, that’s good enough, as you may have seen in the previous lessons when working with Sparki’s sensors!Averaging Ultrasonic Range Finder Readings
Now that we have learned a few things about signal conversions, let’s start to work with real signals from the Sparki sensors. If you try the following code snippet, adapted from the Ultrasonic Ranger Finder page code, you will note that if you move your hand oscillating quickly towards and from the robot, you may see some sensor readings which are clearly false readings (with numbers like 150 cm, for example):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <Sparki.h> // include the sparki library void setup() { } void loop() { sparki.clearLCD(); int cm = sparki.ping(); // measures the distance with Sparki's eyes sparki.print("Distance: "); sparki.print(cm); // tells the distance to the computer sparki.println(" cm"); if(cm != -1) // make sure its not too close or too far { if(cm < 10) // if the distance measured is less than 10 centimeters { sparki.beep(); // beep! } } sparki.updateLCD(); delay(50); // wait 0.1 seconds (100 milliseconds) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <Sparki.h> // include the sparki library void setup() { } int average(unsigned int numberOfReadings) { float result = 0; for (unsigned int i=0; i < numberOfReadings; i++) { result += sparki.ping(); delay(20); } return result / numberOfReadings; } void loop() { float cm = average(3); // read 3 times the sensor and return the average (or arithmetic mean) sparki.clearLCD(); sparki.print("Average: "); sparki.print(cm); // tells the distance to the computer sparki.println(" cm"); if(cm != -1) // make sure its not too close or too far { if(cm < 10) // if the distance measured is less than 10 centimeters { sparki.beep(); // beep! } } sparki.updateLCD(); } |
1 2 3 4 5 6 7 8 9 10 |
int average(unsigned int numberOfReadings) { float result = 0; for (unsigned int i=0; i < numberOfReadings; i++) { result += sparki.ping(); delay(20); } return result / numberOfReadings; } |
A Generic Average Function
But what if we want to apply the average function to other sensors? Sparki has plenty of sensors, and calculating the arithmetic mean of their readings could be useful not just for the ultrasonic distance ranger, but for all of them. Even digital readings could be averaged (try it!). So, why not write a generic average function that we can use with any of these useful sensors? Let’s take a look to the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#include <Sparki.h> // include the sparki library float pingCallBack() { return (float)(sparki.ping()); } float accelXCallBack() { return sparki.accelX(); } float accelYCallBack() { return sparki.accelY(); } float accelZCallBack() { return sparki.accelZ(); } float average(float (*callBack)(void), unsigned int numberOfReadings, unsigned int readingsDelay) { float result = 0; for (unsigned int i=0; i < numberOfReadings; i++) { result += callBack(); if (readingsDelay) //Some sensors do not require a delay between readings, while others do. delay(readingsDelay); } return result / numberOfReadings; } void setup() { } void loop() { sparki.clearLCD(); sparki.print("Ping: "); sparki.println((int)average(pingCallBack, 2, 15)); sparki.print("Accel X: "); sparki.println(average(accelXCallBack, 30, 0)); sparki.print("Accel Y: "); sparki.println(average(accelYCallBack, 30, 0)); sparki.print("Accel Z: "); sparki.println(average(accelZCallBack, 30, 0)); sparki.updateLCD(); } |
1 2 3 4 5 6 7 8 9 10 11 |
float average(float (*callBack)(void), unsigned int numberOfReadings, unsigned int readingsDelay) { float result = 0; for (unsigned int i=0; i < numberOfReadings; i++) { result += callBack(); if (readingsDelay) //Some sensors do not require a delay between readings, while others do. delay(readingsDelay); } return result / numberOfReadings; } |
1 2 |
if (readingsDelay) //Some sensors do not require a delay between readings, while others do. delay(readingsDelay); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void loop() { sparki.clearLCD(); sparki.print("Ping: "); sparki.println((int)average(pingCallBack, 2, 15)); sparki.print("Accel X: "); sparki.println(average(accelXCallBack, 30, 0)); sparki.print("Accel Y: "); sparki.println(average(accelYCallBack, 30, 0)); sparki.print("Accel Z: "); sparki.println(average(accelZCallBack, 30, 0)); sparki.updateLCD(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
float pingCallBack() { return (float)(sparki.ping()); } float accelXCallBack() { return sparki.accelX(); } float accelYCallBack() { return sparki.accelY(); } float accelZCallBack() { return sparki.accelZ(); } |
1 |
float average(float (*callBack)(void), unsigned int numberOfReadings, unsigned int readingsDelay) |
1 |
result += callBack(); |