Verständnisfrage Code Messwerte glätten

Liebe Arduino-/C-Experten,
ich habe gerade als Programmierneuling mein erstes Projekt abgeschlossen. Es handelt sich um eine einfache Temperatursteuerung (Thermostat). Da mein Code sicherlich alles andere als “elegant” ist, versuche ich ihn nun nach und nach aufzuhübschen. Bei meiner Internetsuche nach geeigneten Codeschnipseln bin ich zum Thema “Messwerte glätten” auf diese Methode gestoßen. Bei dieser ergaben sich bei mir etliche Fragezeichen und ich würde mich sehr freuen, wenn mir jemand meine Fragen (siehe unten) zu diesem Code beantworten könnte.

/* Glaettung, einfaches Beispiel

  Liest Werte eines analogen Eingangs und bildet einen Mittelwert
  
  Inspiriert durch: http://arduino.cc/en/Tutorial/Smoothing
  von David A. Mellis  <dam@mellis.org>
  modifiziert 2012-09-04 durch Tom Igoe
  modifiziert 2013-02-07 durch fribbe http://macherzin.net

  Code ist in der Public Domain
*/

const int anzahlMessungen = 10; // Mittelwert wird aus 10 Werten gebildet
const int intervall = 10; // Messungen alle 10 mS, ggf. anpassen
int messungen[anzahlMessungen]; // Array fuer Messwerte
int zeiger = 0; // Zeiger des aktuellen Messwerts
int gesamtSumme = 0; // aktueller Gesamtwert
int durchschnitt = 0; // Mittelwert
int dieseMessung = 0; // aktueller Messwert
boolean status = 0; // Flag fuer ersten Durchlauf
const int sensorPin = A0; // Sensor an Pin 0 analog

void setup()
{
  Serial.begin(9600); // starte serielle Kommunikation  
  while (!Serial) // warte auf Verbindung serieller Port, nur für Leonardo usw.
  {
    ;
  }
}

void loop()
{
  gesamtSumme = gesamtSumme - messungen[zeiger]; // substrahiere letzte Messung
  messungen[zeiger] = analogRead(sensorPin); // lese Sensor
  gesamtSumme = gesamtSumme + messungen[zeiger]; // addiere Wert zur Summe   
  zeiger = zeiger + 1; // zur naechsten Position im Array                
  if (zeiger >= anzahlMessungen) // wenn Ende des Arrays erreicht ... zurueck zum Anfang
  {
    if (status == 1) // wenn Array erstmalig aufgefuellt ...
    {
       Serial.println(durchschnitt); // geglaettete Ausgabe auf serieller Konsole
    }
    status = 1; // erste 10 Werte eingelesen, Array ist gefüllt, Status geaendert
    zeiger = 0; // und wieder von vorn                        
  }
  durchschnitt = gesamtSumme / anzahlMessungen; // berechne Mittelwert  
  delay(intervall); // zur Stabilisierung       
}

// haben fertig und HaLoF

Ich gehe davon aus, dass das “int dieseMessung” in der loop die Variable “messung” sein soll.

Frage 1:
Wozu wird das hier gemacht?
gesamtSumme = gesamtSumme - messungen[zeiger];

Frage2:
Laufen nicht irgendwann die Variablen vom Typ int “gesamtSumme” und “durchschnitt” über? Ich kann nirgends erkennen, dass sie irgendwann auf Null zurückgesetzt werden.

Frage 3:
was hat es genau mit dem “status” auf sich? Warum wird erst beim nächsten Durchlauf der Mittelwert ausgespuckt?

Frage 4:
Gibt es eine Diskrepanz zwischen “int messungen[anzahlMessungen];” und if (zeiger >= anzahlMessungen) (Array hat 10 Positionen, der Zeiger fängt aber bei Null an)

Ich entschuldige mich im Voraus für meine dämlichen Fragen (der Code wird ja wohl funktionieren) und bin dankbar, wenn mir jemand meine Knoten im Gehirn löst.

Vielen Dank und Gruß,
nescius

Wenn fragen über eine Methode auftauchen, einfach eine eigene Schreiben. Da hat man dann eigentlich auch keine Fragen mehr.

Den Begriff "zeiger" hätte ich jetzt in dem Beispiel nicht gewählt. Da er mit einem pointer (dt. zeiger) verwechselt werden können. index trifft es besser.

Frage
1: Mit gesamtSumme - messung(zeiger) wird der älteste Wert entfernt. Könnte man auch anders lösen. In dem man im Array alles durchlaufend von 0 - ende voll schreibt und sobald array einmal gefüllt wurde, jedesmal den mittelwert bildet.

2: Nein! Das Array "messungen" wird ja immer wieder nur überschrieben.
Von der Gesamtmessung wird

3:

4:die 10.Position ist messung[9]. Wenn man Arrays deklariert, gibt man die Größe an, wieviele Werte enthalten sind. Der index fängt in C aber bei 0 an.

5:

Hallo sschultewolter,
vielen Dank für deine Lückentextantwort :wink:

Aber sie hat (hoffentlich) gereicht. Ich glaube es jetzt verstanden zu haben:
Die Messwerte bleiben ja im Array erhalten. Ab dem zweiten Durchlauf der loop subtrahiert die erste Zeile gesamtSumme - messung(zeiger) den Messwert entsprechend dem Index. So wird nach und nach das Array neu beschrieben bis wieder 10 neue/frische Messwerte im Array vorhanden sind. Somit gibt es natürlich weder bei "gesamtSumme" noch bei "durchschnitt" einen Überlauf.
Stimmt meine Annahme so?

Eine kleine Frage bleibt noch übrig. Und zwar zu den Zeilen beim Erreichen des Index (im Code "zeiger") 9:

zeiger = zeiger + 1;             
  if (zeiger >= anzahlMessungen)

Der Index zählt doch noch eins weiter, also bis 10. Erst dann wird der Index auf 0 zurückgesetzt. Beim Index 10, also Messwert 11 (den es nicht gibt) würde er doch auf einen undefinierten Speicherplatz verweisen. Oder mach das nichts, weil er danach gleich resettet wird.

Vielen Dank und Gruß,
nescius

Ich denke verstanden hast du es jetzt, aber diese Methode hat den Nachteil, dass sie viel Speicher braucht, vor allem, wenn du stärker filtern willst, also den Mittelwert von mehr als 10 Werten bilden, zum Beispiel 100 werte.

Es gibt eine bessere Methode:

du bildest einen Tiefpass nach und hast eine Art gleitenden Mittelwert:

void Filtern(float &FiltVal, int NewVal, int FF){
  FiltVal= ((FiltVal * FF) + NewVal) / (FF +1.0);  
}

das funktioniert so:
Mittelwert = letzter Mittelwert*filterfaktor + Messwert * (1-filterfaktor)

mit filterfaktor = 0 wird garnicht gefiltert,
mit filterfaktor = 0,5 besteht jeder neue Mittelwert je zur Hälfte aus dem neuen Meßwert und dem alten Mittelwert
mit filterfaktor = 0,1 besteht jeder neue Mittelwert zu 1/10tel aus dem neuen Meßwert und zu 9/10teln aus dem alten Mittelwert.

So kannst du fast unendlich lange Mittelwerte bilden, ohne mehr Speicher zu verbrauchen.

Codebeispiel für die Filterung eines Einheitssprungs:

float Val;
int Eingangswert,j;

void setup() {  
  Serial.begin(115200);
  Serial.print("IN\t"); Serial.print("OUT\t\n"); 
}

void loop() {    
  Serial.print(Eingangswert); Serial.print("\t"); 
  Filtern(Val, Eingangswert, 100);
  Serial.print(Val); Serial.print("\t\n"); 
  if (j++ > 10) Eingangswert=1;  // nach 10 Werten Sprung auf 1  
}


/*************************************************************************************************
** Funktion Filtern()  by GuntherB								**
**************************************************************************************************
** Bildet einen Tiefpassfilter (RC-Glied) nach.							**
** FF = Filterfaktor;  Tau = FF / Aufruffrequenz						**
**  												**
**  												**
**  											   	**
**  Input: FiltVal der gefilterte Wert, NewVal der neue gelesene Wert; FF Filterfaktor		**
**  Output:	FiltVal										**
**  genutzte Globale Variablen: 	keine							**
**************************************************************************************************/
void Filtern(float &FiltVal, int NewVal, int FF){
  FiltVal= ((FiltVal * FF) + NewVal) / (FF +1.0);  
}

Es gibt hier im Forum einen Tread, in ich die Funktionsweise schon mal erklärt hatte, aber die SuFu funktioniert noch nicht wieder.

Edit: du kannst die Ausgabe im SerMon ins Excel kopieren, dann siehst du schön die Grafik

Allerdings verwendet Gunther einen (unsigned) int filterfaktor

mit FF = 0 wird garnicht gefiltert,
mit FF = 1 besteht jeder neue Mittelwert zur Hälfte aus dem neuen Meßwert und zur anderen Hälfte aus dem alten Mittelwert.
mit FF= 9 besteht jeder neue Mittelwert aus 90% altem Mittelwert und 10% neuem Mittelwert.

Man kann natürlich die Formel auch umschreiben für einen Filter (0.0 kein .. 0.99 stark )

Man sollte übrigens den anfänglichen Mittelwert mit einem ersten Rohwert initialisieren, statt ihn ganz undefiniert lassen (ist bei Arduino zum Glück 0)

michael_x:
Allerdings verwendet Gunther einen (unsigned) int filterfaktor
...
Man kann natürlich die Formel auch umschreiben für einen Filter (0.0 kein .. 0.99 stark )

Kann man.

Ich finde an dieser Ausprägung der Formel besonders schön, dass ich mit FF * Aufruftakt gleich das equivalent des Tau eines RC-Tiefpasses, bzw mit Aufruffrequenz/FF die Grenzfrequenz des Tiefpasses habe.

Wenn die Funktion mit FF = 50 in einer 10ms Schleife aufrufen, dann ist die Grenzfrequenz des Tiefpasses 2Hz.
Dies leicht erkennbare physikalische Analogie macht mir diese Variante sympathischer als die mathmatisch elegantere Lösung mit 0 .. FF .. 1.

Und, ja, man kann/sollte initialisieren. Aber es soll auch ein leicht verständlicher Beispielcode sein, der sich auf den wesentlichen Kern beschränkt.

Hallo Gunther,

mit Deiner damaligen wunderschönen Erklärung kann ich aushelfen. Denn das hast Du mir mal groß und breit erklärt. Ich nutze das bei all meinen Temperatur- und Spannungsmessungen. Geile Sache! :slight_smile:

und die Exceltabelle im Post #21 nicht vergessen ...

Vielen Dank an alle für die Hilfe und Anregungen.

Das mit der Tiefpass/dem gleitenden Mittelwert ist auf jeden Fall sehr interessant.
Ich weiß zwar was eine Tiefpass-/gleitende Mittelwertfunktion ist, bzw. was diese macht, werde aber wohl noch etwas Zeit brauchen, um diese programmiertechnisch zu verstehen (will in meinem Code nur Funktionen/Methoden haben, die ich auch selber verstehe).

Vielen Dank nochmal und Gruß!

Hallo,
Messwert-Beruhigung mit FIFO-Speicher (First In, First Out)
einfach zu verstehen.

int Messwert1 = 0;
int Messwert2 = 0;
int Messwert3 = 0;
int Messwert4 = 0;
int Messwert5 = 0;

Messwert5 = Messwert4;
Messwert4 = Messwert3;
Messwert3 = Messwert2;
Messwert2 = Messwert1;
Messwert1 = neuer Messwert;

beruhigter Messwert = ( Messwert1 + Messwert2 + Messwert3 + Messwert4 + Messwert5 ) / 5

so etwas habe ich mit 10 Werten realisiert, funktioniert ganz wunderbar

Nachteil: nach Neustart / Reset braucht es 10 Messungen bis der Wert stimmt
Vorteil: einfach zu verstehen, Störungen schlagen nur mit 10 Prozent auf den Messwert durch

am besten ... testen

Gruss
Kurti

nescius:
Ich weiß zwar was eine Tiefpass-/gleitende Mittelwertfunktion ist, bzw. was diese macht, werde aber wohl noch etwas Zeit brauchen, um diese programmiertechnisch zu verstehen.

Das ist doch nur eine Zeile:

FiltVal= ((FiltVal * FF) + NewVal) / (FF +1.0);

Ich behaupte mal: wenn du das mathematisch verstanden hast, ist die Programmzeile auch kein Problem.

Vorteil: einfach zu verstehen

Eine Zeile Code ist einfacher als 5, wenn mans erstmal verstanden hat :wink:

Nachteil: kann man nur mühsam von 5 auf 10 auf 50 erweitern. Braucht viel Speicher.

Ein wirklicher Vorteil: Nach n Messungen ist ein Ausreisser komplett weg, beim Tiefpass wird ein Ausreisser nur gedämpft.

Tiefpass 1. Ordnung ist also für verrauschte Messwerte gut weil super-einfach,
um z.b. Übertragungsstörungen auszufiltern sind evtl. noch viel kompliziertere statistische Verfahren als eine Mittelwertbildung angesagt.