Digitaal filteren van analoge sensoren

Last van meetruis of van een te grove meetresolutie? Het middelen van meetwaardes kan helpen. Maar dan niet simpelweg optellen en delen van een korte burst aan meetwaardes, want dat heeft zo zijn beperkingen. Een Moving Average Filter werkt beter; een eenvoudig en efficient digitaal filter dat niet zo maf is als de afkorting doet vermoeden. En ruis is eigenlijk ook niet zo slecht als het lijkt, je kunt er namelijk ook je voordeel mee doen. Hoe dat werkt? Deze blog legt het je uit.

Ongefilterd

ruwe meetwaardes, interval=100[ms]

Stel je hebt een TMP36 temperatuursensor aangesloten op een Arduino, een veel voorkomende (deel-)schakeling. Toen ik daarmee voor de eerste keer een meting deed viel me op dat de meetresolutie slechts 0,5[ºC] was, zie figuur. Vreemd eigenlijk want de datasheet van de TMP36 sensor geeft aan dat een nauwkeurigheid van 0,1[ºC] prima haalbaar is. De verklaring is simpel; de temperatuursensor heeft een gevoeligheid van 10[mV/ºC] en de Arduino ADC waarmee de meetwaarde wordt ingelezen gebruikt 10 bits voor een meetbereik van 5[V]. Dit resulteert een resolutie van ongeveer (5,0[V]/1024=) ~5[mV/bit] oftewel 0,5[ºC] per bit. Wat verder opvalt is dat er zo nu en dan ook ‘spikes’ in het signaal voorkomen ter grootte van +/-0,5[ºC], zeer waarschijnlijk een kleine signaalruis waardoor de ADC meetwaarde zo af en toe gewoon 1 bit hoger of lager uitvalt.

Referentie-code:

loop ()
{
  meetwaarde = analogRead(0);
  delay(100);
}

Recht-toe-recht-aan middelen van meetwaardes

middelen van 16 meetwaardes

Een vaak gebruikte manier om de meetresolutie te verbeteren en de ruis te verminderen en is het middelen van enkele samples. De theorie leert immers dat de meetresolutie evenredig is met het aantal meetwaardes waarover je middelt; met elke verdubbeling van het aantal meetwaardes verdubbelt ook de resolutie. Door het middelen van 16 meetwaardes krijg je dus een temperatuurmeetresolutie van 0,03[ºC].

Potentieel, want zoals de figuur hiernaast laat zien heeft de middeling inderdaad enig effect, maar lang niet op alle gewenste plekken, de ‘plateaus’ verraden nog steeds een effectieve meetresolutie van 0,5[ºC]. De verbeterde resolutie is alleen waarneembaar rondom de flanken tussen de plateaus en ook sommige spikes zijn kleiner geworden, maar zeker niet allemaal! Filteren heeft nl alleen een effect als de meetwaardes waarover je middelt van elkaar verschillen. En verschillen treden in dit voorbeeld vooral op bij de flanken tussen de plateaus en rondom spikes en niet halverwege de plateaus.

Het toevoegen van een klein beetje analoge ruis bovenop het echte temperatuursignaal, dus aan de ingang van de ADC, kan helpen om de resolutie te verbeteren. Want, aangenomen dat de extra ruis een gemiddelde waarde van ‘0’ heeft dan zal de gemiddelde analoge meetwaarde dus onveranderd blijven, maar door de extra ruis ontstaat er wel een (kleine) variatie rondom dat gemiddelde. Door die kleine variatie wordt het oorspronkelijke patroon van constante meetwaarden doorbroken en krijgt het middelingsproces alsnog een kans om waarde toe te voegen. In de audio- en videotechniek wordt dit proces ook wel ‘dithering‘ genoemd.

Helaas is het toevoegen van ‘nette’ ruis niet eenvoudig, maar reeds aanwezige ruis onderdrukken met een analoog filter levert dus niet perse een voordeel op.

Referentie-code:

const int N = 16; 
loop ()
{
  meetwaarde = 0;
  for (int i=0; i<N; i++)
    meetwaarde += analogRead(0);
  meetwaarde /= N;
  delay(100);
}

Middelen met slim gekozen intervaltijd

middelen met langere intervaltijden

Om beter te kunnen filteren heb je dus meetwaardes nodig die onderling van waarde verschillen anders heeft het middelen geen effect. Om variatie in meetwaardes te bereiken dien je het interval tussen de meetwaardes slim te kiezen; dwz, een waarde die in goede verhouding staat tot de tijdconstante (τ) van je systeem (τ is de tijd die nodig is om ca 65% van de beoogde verandering teweeg te brengen). Zonder dit verder wetenschappelijk te willen onderbouwen is een filtersnelheid van ~0.1*τ een goede vuistregel. Voorbeeld: veronderstel een systeem met een tijdconstante van τ=15[sec]. Daarbij past prima een filter met een filtersnelheid van 1.6[sec]; dwz een filter waarbij we telkens 16 meetwaardes middelen met een meetinterval van ~100[msec]. In het figuur is te zien dat met een dergelijk filter alle spikes verdwenen zijn en dat de flanken tussen de plateaus mooi geleidelijk verlopen. Uiteraard is een vuistregel slechts een vuistregel en is tunen op de praktijksituatie altijd aan te raden. Een sneller filter zorgt ervoor dat de het filtereffect steeds minder wordt en het gedrag steeds meer gaat lijken op de eerder besproken filterimplementatie. Een trager filter zorgt ervoor dat de gefilterde meetwaarde steeds langzamer reageert op snelle temperatuursveranderingen.

Deze manier van middelen heeft wel een belangrijk nadeel. De tijd dat de Arduino bezig is met de meting is nl behoorlijk lang en zolang de meting loopt is het voor een gemiddelde programmeur best lastig om de Arduino ook nog andere taken te laten uitvoeren. Denk aan het updaten van een display, het aansturen van een servo, etc, etc.

Referentie-code:

const int N = 16; 
loop ()
{
  meetwaarde = 0;
  for (int i=0; i<N; i++) {
    meetwaarde += analogRead(0);
    delay(100);
  }
  meetwaarde /= N;
}

Middelen met een “Moving Average Filter”

Middelen met een Moving Average Filter

Het “Moving Average Filter” biedt een oplossing voor het bovengenoemde probleem. In plaats van telkens opnieuw 16 metingen uit te voeren maken we slim hergebruik van 15 voorgaande metingen en voeren we telkens slechts 1 nieuwe meting uit. Hierdoor kost een meting per keer slechts 100[msec] en niet telkens 1600[msec] zoals in het vorige voorbeeld.

Bovendien vertoont het filter een nog vloeiender verloop van de gefilterde meetwaardes omdat de opeenvolgende meetintervallen ditmaal telkens een (100*15/16 =) 94% overlap hebben.

Referentie-code: 
const int N = 16;
int meetwaardes[N];
int i = 0;

loop ()
{
  meetwaardes[i] = analogRead(0);      // vervang oudste meetwaarde
  i++; i %= N;                         // teller loopt van 0 tot N-1

  // bereken filterwaarde
  int filterwaarde = 0;                
  for (int j=0; j<N; j++)
    filterwaarde += meetwaardes[j];
  filterwaarde /= N;

  delay(100); 
}

Bonus:

Tenslotte kan er algoritmisch nog een kleine optimalisatie uitgevoerd worden waardoor de Arduino minder rekentijd nodig heeft voor het uitrekenen van de gemiddelde waarde. Vanwege het “moving average” karakter van het filter kan de standaardberekening:

gemiddelde_waarde = som(meetwaarde) / N

vervangen worden door:

gemiddelde_waarde = (vorige_somwaarde – oudste_meting + nieuwste_meting) / N

Hierdoor worden in plaats van 16 telkens slechts 2 optelberekeningen uitgevoerd. Marginaal voor zo’n kort MA-filter, maar zeer rekentijd besparend naarmate het filter ‘langer’ gekozen wordt.

Rekentijd geoptimaliseerde referentie-code:

const int N = 16;           // aantal te middelen meetwaardes
int meetwaardes[N];         // opslag van individuele meetwaardes
int filteraccumulator = 0;  // optelsom van alle individuele meetwaardes
int i = 0;                  // meetwaardeteller

loop ()
{
  filteraccumulator -= meetwaardes[i]; // vergeet oudste meetwaarde
  meetwaardes[i] = analogRead(0);      // bewaar nieuwe meetwaarde
  filteraccumulator += meetwaardes[i]; // accumuleer nieuwste meetwaarde
  i++; i %= N;                         // teller loopt van 0 tot N-1

  filterwaarde = filteraccumulator/N;  // bereken de gemmiddelde waarde

  delay(100); 
}