Filtervergleich

Filter in Arduino Teil 2

Fortsetzung von Teil 1
Im ersten Teil habe ich geschrieben, wozu ein Filter in Arduino überhaupt gut ist und einige Filtertypen und deren Vor- bzw. Nachteile vorgestellt.

In diesem Teil wird es etwas praktischer zugehen. Versprochen … vielleicht … 😉

Das richtige Filter zum richtigen Zweck:

In Teil 1 habe ich festgestellt, dass alle Filtertypen Vor- und Nachteile haben. Aber welchen nun benutzen? Dazu muss man sich den Verwendungszeck genau anschauen und abwägen. Beispielsweise ergibt es keinen Sinn einen Mittelwertfilter, der über Durchschnittsbildung in einer Schleife funktioniert, in einer zeitkritischen Anwendung zu benutzen.

Ich lege mich also fest:

Die Anwendung soll mittels einem Accelerometer einen Neigungswinkel messen. Möglichst auf eine Stelle hinter dem Komma genau (Ein ehrgeiziges Ziel, ich weiß. Aber dazu evtl. mehr in einem anderen Artikel).

Die Ausgabe soll gut lesbar über ein LCD-Display erfolgen. Hier hat sich für mich eine Bildwiederholfrequenz von 0,3 bis 0,5 Hertz als angenehm erwiesen. Zur Erinnerung: Ab ca. 24 Hz, also Bildern pro Sekunde, werden Änderungen als flüssige Bewegung wahrgenommen. Wechseln die anzuzeigenden Winkel also sehr schnell, können wir die Änderungen nicht mehr mit dem Auge erfassen.

Der verwendete Sensor ist ein NXP MMA8451 Link mit 14 bit Auflösung.

Das Ziel ist also, die Werte, die der Sensor liefert, so zu filtern, dass eine flüssige Ausgabe auf das Display möglich ist und gleichzeitig das Rauschen aus dem Signal entfernt wird.

Der Praxistest:

Um zu sehen wie die rohen Sensorwerte und die gefilterten Daten dann aussehen, werde ich die Ausgabe mit der Programmiersprache Processing sichtbar machen.

Dazu verwende ich folgende Hardware:

  • 1 x Arduino UNO
  • 1 x MMA8451 Sensor Modul auf einen Holzwürfel montiert
  • USB Kabel
MMA8451 Breakout
MMA8451 Breakout auf Würfel montiert

und folgende Software:

Im Arduino werden die Werte die dargestellt werden sollen, per Serial.print(), getrennt durch ein Komma, ausgegeben. Um die Filter zu testen, gebe ich die Rohdaten aus dem Sensor aus. Gefolgt von den gefilterten Daten. Dann kompiliere ich, und Lade den Sketch

Wichtig: in der Arduino IDE darf kein serieller Monitor geöffnet werden. Diesen braucht man gleich für die Ausgabe in Processing.
Im Processing passe ich den Sketch an unsere Bedürfnisse an:

int numValues = 1; // number of input values or sensors
// * change this to match how many values your Arduino is sending *

Und den ersten Block in dem die Eingangssignale konfiguriert werden:

// *edit these* to match how many values you are reading, and what colors you like  
values[0] = 1000; //Auflösung der X-Achse 
min[0] = -300; //Bsp. 13 bit signed = -4095 
max[0] = 300; // Bsp. 13 bit signed = 4095 
valColor[0] = color(255, 0, 0); // RGB Farbwert Rot 

Die restlichen Blocks im Sketch müssen auskommentiert werden. Sonst kommt eine Fehlermeldung.

Zuletzt habe ich noch eine Zeile Processing im Sketch eingefügt, die einen Screenshot des Graphen speichert.

save("noise.png");    //save screenshot

Der Code befindet sich in Zeile 124 des Sketches. In dieser „If-Verzweigung“ wird geprüft, ob der Plot sich am Ende der Range befindet und ggf. ein Löschen des Screens und ein Rücksprung auf Position X=0 ausgeführt. Genau der richtige Moment also um einen Screenshot zu speichern.
Wird der Sketch nun ausgeführt, während der Arduino vor sich hin werkelt, plottet uns Proscessing diesen Graph. Die Graphen zeigen jeweils ein Zeitfenster von ca. 20 Sekunden.

Noise
Grundrauschen des MMA8451

Der Sensor lag dabei in Ruhe auf dem Tisch. Man sieht deutlich das Grundrauschen des Sensors.
Prima, der Testaufbau funktioniert also.

Mittelwertfilter:

Als Erstes möchte ich testen wie eine einfache Mittelwertbildung im Graph aussieht. Dazu füge ich in meinem „Filter in Arduino“ Sketch folgende Zeilen im Mainloop hinzu.

Mainloop:

void loop()
{
    for(average=0; average <=50; average++){
        sensor_A.read();
        filtered_average += sensor_A.x;
        delay(1);
        }
    filtered_average /=average;
    
    sensor_A.read();
    raw_AX = sensor_A.x;    
    
    Serial.print(raw_AX);
    Serial.print(",");
    Serial.println(filtered_average);
}

Der Gesamte Sketch:

/******************************************************************************************
TESTING FILTER ALGORITHMS 

MAX KAISER 2018 

************************************ INCLUDES ********************************************/

#include "Wire.h"
#include <Adafruit_MMA8451.h>
#include <Adafruit_Sensor.h>
/************************************ DEFINED VARIABLES ************************************/

int16_t raw_AX;    //raw data from sensor A
long filtered_average = 0L;    //averaged value
int average;    //counting variable for averaging loop
/*********************************************************************************************/

Adafruit_MMA8451 sensor_A = Adafruit_MMA8451(); //create a sensor object

/************************************ DEFINED FUNCTIONS ************************************/

void setup()
{
    Serial.begin(9600);
    sensor_A.begin(0x1D);
    sensor_A.setRange(MMA8451_RANGE_2_G);
}

void loop()
{
    for(average=0; average <=50; average++){
        sensor_A.read();
        filtered_average += sensor_A.x;
        delay(1);
        }
    filtered_average /=average;
    
    sensor_A.read();
    raw_AX = sensor_A.x;    
    Serial.print(raw_AX);
    Serial.print(",");
    Serial.println(filtered_average);
}

Während der „Aufzeichnung“ liegt der Sensor zuerst ruhig auf dem Tisch, anschliessend schüttle ich den Sensor leicht.

Auswertung:

Der Graph sieht so aus:

Mittelwert Filter in Arduino
Oben: Original, Unten: Mittelwert

Die Schleife zur Mittelwertbildung hat eine Verzögerung von einer ms. Darum werden die werte nur alle 50 ms ausgegeben. Der Graph wird stufig.
Wir sehen allerdings an der geraden Linie, dass das Rauschen nun fast verschwunden ist.
Man könnte Sagen: „Fall abgeschlossen! Rauschen weg. Ausgabe eh nur alle 300 ms nötig.“
Wenn man aber ein Filter braucht, das die Laufzeit nicht beeinflusst? Außerdem wäre der Artikel dann zu Ende 😉
Ich probiere also weiter.

Filters.h:

Ich verwende die Library Filters und daraus den OnePole Tiefpass. Ich füge dem Sketch folgende Zeilen hinzu:

float filtered_lowpass1pole=0;   //filtered value returned in float
float filterFrequency = 5.0;    // filters out changes faster that 5.0 Hz
FilterOnePole lowpassFilter_OnePole( LOWPASS, filterFrequency );    // create a one pole (RC) lowpass filter

Und im Mainloop:

filtered_lowpass1pole = lowpassFilter_OnePole.input(sensor_A.x);
Serial.println(filtered_lowpass1pole);

Die Mittelwertschleife und die Ausgabe des Wertes wird aus Performancegründen gelöscht.

Auswertung:

Der Graph des „Filter in Arduino“ Sketch sieht nun so aus:

Raw vs Lowpass filter in Arduino with 5 Hz
Oben: Original Rohdaten; Unten: Tiefpass mit Cutoff Frequenz von 5Hz

Hm, das Filter ist kaum zu erkennen. Ich passe also die Variable „filterFrequency“ an.

float filterFrequency = 0.2;    // filters out changes faster that 0.2 Hz 
Raw vs Lowpass filter in Arduino with 0,2 Hz
Oben: Original Rohdaten; Unten: Tiefpass mit Cutoff Frequenz von 0,2 Hz

Schon besser! Es dauert allerdings relativ Lange bis sich der Wert bei einer schnellen Änderung stabilisiert hat.

Ein Wert zwischen den beiden oberen wirds also sein.

Raw vs Lowpass filter in Arduino with 0,2 Hz
Oben: Original Rohdaten; Unten: Tiefpass mit Cutoff Frequenz von 0,4 Hz

Ich denke mit diesem Ergebniss kann ich in die weitere Entwicklung der eigentlichen Anwendung gehen.

Aber halt! Was ist mit der Library aus dem ersten Teil? Microsmooth?

Exponentieller gleitender Mittelwert (EMA Filter)
Mit der Microsmooth Library

Beim Einbinden der Library ist mir ein Nachtteil aufgefallen. Der Filter input ist als „unsigned int“ deklariert. Die Rohdaten des Sensors allerdings kommen allerdings „signed“, also die untere Hälfte als Negativwerte. Ungünstig. Um dies zu lösen, habe ich von der map() funktion gebrauch gemacht:

int ms_ema = map(sensor_A.x,-8191,8191,0,16383);

Die „signed“ Werte von Sensor_A werden auf 0 bis 16383 „umgebogen“. Ich bin mir fast sicher, dass es hier eine elegantere Lösung gibt. Mir als Programmieranfänger fällt aber auf die schnelle nichts Besseres ein.
Das Ergebnis sieht so aus:

Vergleich verschiedener Filter in Arduino
Oben: Raw-Daten, Mitte: Lowpass aus Filters.h, Unten: Microsmooth EMA

Hoppla! Zwischen dem Lowpass-Filter aus der Filters.h und der Microsmooth.h scheint der Unterschied nur marginal zu sein. Und das Lowpass filter ist sogar in „float“ deklariert. Außerdem sind so keine wilden Typecasts nötig.

Es ist gut möglich, dass das Lowpass-Filter ebenfalls auf dem EMA als Algorithmus basiert.

Fazit:

Eine Aussage wie „Dies oder jenes Filter ist das beste“ lässt sich nicht treffen. Es ist immer eine Frage der Anwendung, welches das Passende ist. Ich für mich konnte jedoch durch diesen Artikel einen Überblick über die einfacheren Filter in Arduino gewinnen und meine Wahl wird mir bei zukünftigen Projekten sicher leichter fallen.

  • Um das passende Filter zu finden, muss man die Anforderungen erst einmal spezifizieren.
  • Um Werte zu glätten, reicht oft eine Mittelwertbildung aus. Für zeitkritische Anwendungen ist dies allerdings ungeeignet.
  • Bei anderen Filter Librarys müssen trotzdem die Parameter sorgfältig an die Anwendung angepasst werden.

Ich hoffe, ich konnte dem Leser dieses Artikels einen kleinen Überblick über das Thema Filter in Arduino verschaffen und einfach anzuwendende Mittel an die Hand geben, um verrauschte Sensorwerte zu glätten.

Bald werde ich hier ein Projekt vorstellen, bei dem die Anwendung von Filtern eine wichtige Rolle spielt.

 

2 Kommentare

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert