Eleminating noise from sensor readings on Arduino with digital filtering

Posted on Dec 24, 2011 in Arduino, C++, Electronics, Programming, UberFridge | 8 comments

I use two LM35 temperature sensors to measure beer and fridge temperature in my UberFridge project. These sensors output 10 mV/°C, so for my fridge the output ranges between 40 and 300 mV.

I wanted to be able to measure differences of 0.1 °C, so 1 mV. That is a very small voltage difference to measure in the presence of noise, so I implemented multiple filters to get rid of noise and increase accuracy.

The sensor circuit

I have connected the output of the sensors directly to the A/D converter of the Atmega328. The A/D converter is a capacitive load, which the sensors do not like, so I added an RC-damper to ground as the LM35 datasheet suggested.

I have put a bypass capacitor at the supply voltage of the sensors with a 100 Ohm resistor to the main VCC. This has two reasons:

  • Surpressing power noise
  • When the 3.5mm jack plugs are inserted into their connectors, they create a short circuit when they are halfway in. The 100 Ohm resistor prevents the connectors from short circuiting VCC to ground and resetting the Arduino.

For the full circuit, check the schematics post.

Analog to digital conversion

The Atmega328 has a 10 bit A/D converter, which by default is mapped on 0-5V. To gain better accuracy for a smaller input voltage range, the reference voltage can be set to an internal 1.1V source. Arduino has a function to do this for you:

analogReference(2); //set analog reference to internal 1.1V

Setting the reference voltage to 1.1V means that 1 mV almost corresponds to 1 bit. It just needs to be scaled by 1100/1024.

int value = analogRead(sensorpin)*1100/1024;

Adding a mode filter for more accuracy and less noise

The sensors are connected to the Arduino with long wires that act as antennas and pick up noise. This caused the temperature readings to fluctuate as much as 1.5 degrees (which is still only 15mV).

A first step towards repressing this noise could be to do 100 A/D conversions and taking the average. While this is a very straight forward approach, there are better options.

A median filter is much better in reducing the impact of noise than an averaging filter. It works like this: you take 100 measurements, sort them from small to large and take the middle one. A median filter is much less influenced by outliers, since they end up at the beginning or the end.

A mode filter is a combination of a median filter and an averaging filter: you sort the values and take the average of the ones in the middle. In my case I take the average of the 10 center values. The benefit of doing this is that the output of the filter can have a 10 times higher resolution than the A/D converter. With a median filter the output can fluctuate between for instance 200*1100/1024 = 21.48 °C and 201*1100/1024 = 21.59 °C. The mode filter gives you 10 steps in between.

The final code to read one sensor:

#define NUM_READS 100
float readTemperature(int sensorpin){
   // read multiple values and sort them to take the mode
   int sortedValues[NUM_READS];
   for(int i=0;i<NUM_READS;i++){
     int value = analogRead(sensorpin);
     int j;
     if(value<sortedValues[0] || i==0){
        j=0; //insert at first position
     }
     else{
       for(j=1;j<i;j++){
          if(sortedValues[j-1]<=value && sortedValues[j]>=value){
            // j is insert position
            break;
          }
       }
     }
     for(int k=i;k>j;k--){
       // move all values higher than current reading up one position
       sortedValues[k]=sortedValues[k-1];
     }
     sortedValues[j]=value; //insert current reading
   }
   //return scaled mode of 10 values
   float returnval = 0;
   for(int i=NUM_READS/2-5;i<(NUM_READS/2+5);i++){
     returnval +=sortedValues[i];
   }
   returnval = returnval/10;
   return returnval*1100/1023;
}

Addional filtering: IIR Butterworth low pass filters

I have changed the filters from second order to third order on 23-11-2012. This gives better filtering and therefore the delay for the slow beer temperature can be lower.

The filtering above still was not enough to make the temperatures smooth enough for the control algorithm. To suppress noise even more I added 4 third order Butterworth low pass filters. A butterworth filter is an inifinite impulse response filter (IIR). This means that every output value of the filter is calculated from a history of input values and earlier filter output values. (Because the filter output is used to calculate future values, the influence of any input sample on future outputs is a regression to infinity, hence the name).

The implementation of a software Butterworth filter is actually very easy, see the code snippet below.

void updateTemperatures(void){ //called every 200 milliseconds
  fridgeTempFast[0] = fridgeTempFast[1]; fridgeTempFast[1] = fridgeTempFast[2]; fridgeTempFast[2] = fridgeTempFast[3];
  fridgeTempFast[3] = readTemperature(fridgePin); 
 
  // Butterworth filter with cutoff frequency 0.033*sample frequency (FS=5Hz)
  fridgeTempFiltFast[0] = fridgeTempFiltFast[1]; fridgeTempFiltFast[1] = fridgeTempFiltFast[2]; fridgeTempFiltFast[2] = fridgeTempFiltFast[3];
  fridgeTempFiltFast[3] =   (fridgeTempFast[0] + fridgeTempFast[3] + 3 * (fridgeTempFast[1] + fridgeTempFast[2]))/1.092799972e+03
              + ( 0.6600489526    * fridgeTempFiltFast[0]) + (  -2.2533982563     * fridgeTempFiltFast[1]) + ( 2.5860286592 * fridgeTempFiltFast[2] ); 
 
  fridgeTemperatureActual = fridgeTempFiltFast[3];
 
  beerTempFast[0] = beerTempFast[1]; beerTempFast[1] = beerTempFast[2]; beerTempFast[2] = beerTempFast[3];
  beerTempFast[3] = readTemperature(beerPin); 
 
  // Butterworth filter with cutoff frequency 0.01*sample frequency (FS=5Hz)
  beerTempFiltFast[0] = beerTempFiltFast[1]; beerTempFiltFast[1] = beerTempFiltFast[2]; beerTempFiltFast[2] = beerTempFiltFast[3];
  beerTempFiltFast[3] =   (beerTempFast[0] + beerTempFast[3] + 3 * (beerTempFast[1] + beerTempFast[2]))/3.430944333e+04
              + ( 0.8818931306    * beerTempFiltFast[0]) + (  -2.7564831952     * beerTempFiltFast[1]) + ( 2.8743568927 * beerTempFiltFast[2] ); 
 
  beerTemperatureActual = beerTempFiltFast[3];
}
 
void updateSlowFilteredTemperatures(void){ //called every 10 seconds
  // Input for filter
  fridgeTempSlow[0] = fridgeTempSlow[1]; fridgeTempSlow[1] = fridgeTempSlow[2]; fridgeTempSlow[2] = fridgeTempSlow[3];
  fridgeTempSlow[3] = fridgeTempFiltFast[3]; 
 
  // Butterworth filter with cutoff frequency 0.01*sample frequency (FS=0.1Hz)
  fridgeTempFiltSlow[0] = fridgeTempFiltSlow[1]; fridgeTempFiltSlow[1] = fridgeTempFiltSlow[2]; fridgeTempFiltSlow[2] = fridgeTempFiltSlow[3];
  fridgeTempFiltSlow[3] =   (fridgeTempSlow[0] + fridgeTempSlow[3] + 3 * (fridgeTempSlow[1] + fridgeTempSlow[2]))/3.430944333e+04
              + ( 0.8818931306    * fridgeTempFiltSlow[0]) + (  -2.7564831952     * fridgeTempFiltSlow[1]) + ( 2.8743568927 * fridgeTempFiltSlow[2] ); 
 
  beerTempSlow[0] = beerTempSlow[1]; beerTempSlow[1] = beerTempSlow[2]; beerTempSlow[2] = beerTempSlow[3];
  beerTempSlow[3] = beerTempFiltFast[3]; 
 
   // Butterworth filter with cutoff frequency 0.01*sample frequency (FS=0.1Hz)
  beerTempFiltSlow[0] = beerTempFiltSlow[1]; beerTempFiltSlow[1] = beerTempFiltSlow[2]; beerTempFiltSlow[2] = beerTempFiltSlow[3];
  beerTempFiltSlow[3] =   (beerTempSlow[0] + beerTempSlow[3] + 3 * (beerTempSlow[1] + beerTempSlow[2]))/3.430944333e+04
              + ( 0.8818931306    * beerTempFiltSlow[0]) + (  -2.7564831952     * beerTempFiltSlow[1]) + ( 2.8743568927 * beerTempFiltSlow[2] );
}

As you can see I have 2 fast filters and 2 slow filters. The fast filters are updated 5 times per second and have corner frequencies of 0.165 Hz  and 0.05Hz. The slow filters are updated every 10 seconds and have corner frequencies of 0.001 Hz. These slow filters take their input from the fast filters.

Why a fast and a slow filter?

Every causal filter (a filter that cannot see into the future) causes a delay from input to output: if you to take an average of x values, it takes x updates for the output to fully show the input change.

An IIR low pass filter takes an average of infinite previous values, but with a decreasing multiplier for older inputs. A filter with a lower corner frequency averages more samples and therefore causes a longer delay.

A delay of 10 minutes in the fridge temperature is unacceptable when you need to turn off the fridge when the desired temperature is reached.

A filter with a very low corner frequency is needed to filter the fridge signal for peak detection in the control algorithm. The delay doesn’t matter, because the peaks are only used to slowly adapt control parameters and not to take immediate decisions. For more information, see the temperature control article.

For the beer temperature the story is similar. I want to display the actual beer temperature, without a delay. But the control algorithm adjusts the fridge temperature setting based on the error in beer temperature. The error is multiplied by 30 by the algorithm, so very small differences cause a huge swing in the temperature setting. To eliminate fast changing settings, the beer temperature is filtered heavily.

Because I could not have 1 corner frequency that would satisfy my delay and filtering requirements I had to use 2 filters for each temperature.

Slope calculation

The slope of the beer temperature is calculated as the difference the the beer temperature 30 minutes ago. Calculating it from measurements close together has much higher noise, because two adjacent measurements differ very little. 30 values of the slow filtered beer temperature are kept in a circular buffer and the slope is updated every minute.

void updateSlope(void){ //called every minute
  beerTempHistory[beerTempHistoryIndex]=beerTempFiltSlow[3];
  beerSlope = beerTempHistory[beerTempHistoryIndex]-beerTempHistory[(beerTempHistoryIndex+1)%30];
  beerTempHistoryIndex = (beerTempHistoryIndex+1)%30;
}

Filter initialization

All filter variables are global in my code, so they are initialized to zero. To have the filters start in a nice steady state, I initialize all the variables to the current temperature and then update the filters a few times.

void initFilters(void){
  beerTemperatureActual = readTemperature(beerPin);
  fridgeTemperatureActual = readTemperature(fridgePin);
  for(int i=0;i

8 Comments

  1. This an awesome project! Thanks for sharing the details and open-sourcing your code. I’m working on similar to design except it will use an Ethernet Shield to output data to pachube.com periodically. Also, I’m using a DS18B20 1-wire temperature sensor with 12-bit resolution, so I’m skipping the “fast” temperature updates.

    One question: How are you calculating your Butterworth filter coefficients? I found the “Interactive Digital Filter Design” website that generates code similar to yours but I have not been able to re-create your coefficients.

    • Hi Jeff,
      I will switch to the DS18B20′s myself soon, I already have them but still have to update my code. I will do that when I have a bit more time and lump it together with some other big updates I am working on. Skipping the fast filter is probably okay with the digital sensors. Probably you can also filter less to get a smaller delay.

      I calculated the filter coefficients here: http://www-users.cs.york.ac.uk/~fisher/mkfilter/trad.html

  2. I want to know if u are changing your sensor from DS18B20 to LM35,Do u have to also change your code?

    Regards Sam

    • Yes, I am working on it. I will change the temperature reading function and update the filters for the lower amount of noise.

  3. According to the DS18B20 datasheet, the max time to read a temperature at 12bit resolution is 750 ms. I assume that when you switch to using the digital temp sensors you would slow down the fast filter update to about 750 instead of every 200ms, as well as possibly get rid of the mode filter since the digital sensors are very stable (at least in my limited experience)

    • Yes, I will redesign the whole filtering algorithm. Less filtering, fixed point filters and no mode filtering.

  4. Hi Elco, I am from New Zealand and love to brew my own beer. I am not technically minded enough for this project I think, but would love to be able to control my fermentation fridge in this way… Is your control box available for purchase? Thanks, and awesome work!!

    • Hi William,

      Not yet, but I plan to sell a plug and play kit when BrewPi is a bit more mature. It will probably be in the form of an Arduino shield, so you still have to plug some things together, but it should be fairly easy.

      Elco

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>