Sensorwerte glätten mit Arduino: Rauschen filtern mit Mittelwert, EMA & Median
Der Bodenfeuchte-Sensor springt zwischen 512 und 547, das OLED flackert bei jedem Refresh, und das Relais klackert genau an der Schaltschwelle im Sekundentakt. Willkommen im echten Leben – jeder analoge Messwert rauscht. Die gute Nachricht: Du brauchst weder Mathematikstudium noch schwere Bibliotheken. Vier kleine Filter reichen für fast jedes Hobbyprojekt, und in diesem Tutorial lernst du alle vier kennen – inklusive der wichtigen Frage, welchen du wann nimmst.
Woher das Rauschen kommt
Bevor du filterst, lohnt es sich zu wissen, was du überhaupt bekämpfst. Die Ursachen sind fast immer eine dieser vier:
- Quantisierungsrauschen: Der ADC des ATmega328P hat 10 Bit. Bei 5 V Referenz entspricht ein einzelner Schritt rund 4,9 mV. Liegt dein Signal genau zwischen zwei Stufen, kippt der Wert ständig hin und her.
- Versorgungsrauschen: Ein USB-Port oder ein einfaches Schaltnetzteil liefert keine saubere Spannung. Da der Arduino standardmäßig VCC als ADC-Referenz nutzt, wandert dein Messwert mit jeder Schwankung mit.
- Einstreuung: Lange Sensorkabel wirken wie Antennen. Besonders 50-Hz-Brumm aus dem Stromnetz ist ein häufiger Gast.
- Echte Ausreißer: Ein einzelner völlig falscher Wert – typisch bei Ultraschallsensoren, wenn ein Echo verloren geht, oder bei ToF-Sensoren an dunklen Oberflächen.
ℹ️ Filtern ersetzt keine saubere Hardware. Ein 100-nF-Kondensator zwischen Sensorausgang und GND, kurze Leitungen und eine ordentliche Stromversorgung über ein LM2596-Modul bringen dich oft weiter als jeder Softwarefilter. Erst aufräumen, dann glätten.
Das brauchst du
1Verkabelung und Referenzmessung
| Bauteil | Pin am Sensor | Arduino Nano | ESP32 |
|---|---|---|---|
| Bodenfeuchte-Sensor | AOUT | A0 | GPIO34 |
| Bodenfeuchte-Sensor | VCC / GND | 5V / GND | 3V3 / GND |
| ALS-PT19 Lichtsensor | OUT | A1 | GPIO35 |
| HC-SR04 | TRIG / ECHO | D3 / D2 | GPIO5 / GPIO18 |
| Relais KY-019 | IN | D7 | GPIO26 |
Starte mit dem simpelsten Sketch, den es gibt – er zeigt dir, wie schlimm es wirklich ist. Der Serial Plotter der Arduino IDE (Werkzeuge → Serieller Plotter) macht das Rauschen sofort sichtbar:
#define SENSOR_PIN A0
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.println(analogRead(SENSOR_PIN));
delay(20); // 50 Messungen pro Sekunde
}
💡 Erst messen, dann filtern. Notiere dir, wie stark der Wert bei ruhendem Sensor schwankt – zum Beispiel ±18 Digits. Diese Zahl ist deine Messlatte. Nur so erkennst du später, ob ein Filter wirklich hilft oder ob du nur den Ausschlag versteckst und dafür eine träge Reaktion einhandelst.
2Filter 1: Der gleitende Mittelwert
Der Klassiker. Du merkst dir die letzten N Messwerte in einem Ringpuffer und gibst deren Durchschnitt aus. Jeder neue Wert verdrängt den ältesten. Wichtig: Du addierst nicht jedes Mal alles neu, sondern rechnest die Summe fort – das spart Rechenzeit:
#define SENSOR_PIN A0
#define FENSTER 16 // Zweierpotenz -> Division wird zur Bitschiebung
int puffer[FENSTER];
uint8_t index = 0;
long summe = 0;
bool gefuellt = false;
void setup() {
Serial.begin(115200);
// Puffer mit echten Messwerten vorbelegen, statt bei 0 zu starten
int start = analogRead(SENSOR_PIN);
for (uint8_t i = 0; i < FENSTER; i++) {
puffer[i] = start;
summe += start;
}
gefuellt = true;
}
int gleitenderMittelwert(int neuerWert) {
summe -= puffer[index]; // aeltesten Wert abziehen
puffer[index] = neuerWert; // ueberschreiben
summe += neuerWert; // neuen addieren
index = (index + 1) % FENSTER;
return summe / FENSTER;
}
void loop() {
int roh = analogRead(SENSOR_PIN);
int gefiltert = gleitenderMittelwert(roh);
// Beide Kurven im Serial Plotter vergleichen
Serial.print(roh);
Serial.print("\t");
Serial.println(gefiltert);
delay(20);
}
Im Plotter siehst du sofort: Die zweite Kurve ist deutlich ruhiger, folgt einer echten Änderung aber mit Verzögerung. Bei 16 Werten und 20 ms Abstand beträgt die Verzögerung etwa 160 ms – die halbe Fensterlänge.
| Fenstergröße | Rauschunterdrückung | Verzögerung bei 20 ms Takt | RAM-Bedarf (int) |
|---|---|---|---|
| 4 | Faktor 2 | ca. 40 ms | 8 Byte |
| 8 | Faktor 2,8 | ca. 80 ms | 16 Byte |
| 16 | Faktor 4 | ca. 160 ms | 32 Byte |
| 64 | Faktor 8 | ca. 640 ms | 128 Byte |
⚠️ Rauschen sinkt nur mit der Wurzel. Vervierfachst du das Fenster, halbiert sich das Rauschen – aber die Verzögerung vervierfacht sich. Wer Fenster von 256 nutzt, misst am Ende Vergangenheit. Und Achtung beim Nano: Er hat nur 2 KB RAM. Mehrere große Puffer parallel bringen ihn ins Straucheln.
3Filter 2: Der exponentielle Filter (EMA)
Mein Favorit für die meisten Projekte, weil er keinen Puffer braucht – nur eine einzige Variable. Die Formel ist eine Zeile:
gefiltert = alpha * neu + (1 - alpha) * gefiltert
alpha liegt zwischen 0 und 1 und bestimmt, wie stark der neue Wert zählt. 0,1 heißt: Jeder neue Messwert schiebt das Ergebnis nur um 10 % in seine Richtung. Auf dem AVR ist die Ganzzahlvariante angenehm schnell:
#define SENSOR_PIN A0
// --- Float-Variante: gut lesbar, ideal auf ESP32 ---
float emaWert = 0;
const float ALPHA = 0.1; // 0.01 = sehr traege, 0.5 = kaum Wirkung
bool emaInit = false;
float ema(float neu) {
if (!emaInit) { emaWert = neu; emaInit = true; } // sauber starten
emaWert = ALPHA * neu + (1.0 - ALPHA) * emaWert;
return emaWert;
}
// --- Ganzzahlvariante mit Festkomma: schnell auf dem AVR ---
// Interner Wert wird um 8 Bit hochskaliert (Faktor 256)
int32_t emaFix = 0;
bool fixInit = false;
const uint8_t SCHIEBUNG = 4; // alpha = 1/16 = 0.0625
int emaGanzzahl(int neu) {
if (!fixInit) { emaFix = (int32_t)neu << 8; fixInit = true; }
emaFix += (((int32_t)neu << 8) - emaFix) >> SCHIEBUNG;
return (int)(emaFix >> 8);
}
void setup() {
Serial.begin(115200);
}
void loop() {
int roh = analogRead(SENSOR_PIN);
Serial.print(roh);
Serial.print("\t");
Serial.print(ema(roh), 1);
Serial.print("\t");
Serial.println(emaGanzzahl(roh));
delay(20);
}
ℹ️ Alpha praktisch wählen. Faustregel: alpha ≈ 2 / (N + 1) verhält sich ähnlich träge wie ein gleitender Mittelwert über N Werte. Für N = 16 also rund 0,12. Der EMA braucht dafür aber 4 Byte statt 32 – und du kannst alpha zur Laufzeit ändern, etwa über einen Drehgeber zum Experimentieren.
⚠️ Der Startwert 0 ist eine Falle. Initialisierst du emaWert mit 0, kriecht die Kurve nach dem Einschalten sekundenlang von unten an den echten Wert heran. Setze den ersten Messwert direkt als Startwert – genau das macht das emaInit-Flag oben.
4Filter 3: Der Medianfilter gegen Ausreißer
Mittelwert und EMA haben eine gemeinsame Schwachstelle: Ein einziger völlig falscher Wert zieht das Ergebnis mit. Misst dein HC-SR04 viermal 42 cm und einmal 380 cm, verschiebt der Mittelwert über fünf Werte das Ergebnis um satte 67 cm. Der Median ignoriert solche Ausreißer komplett – er sortiert die Werte und nimmt den mittleren:
#define TRIG_PIN 3
#define ECHO_PIN 2
#define MEDIAN_N 5 // ungerade Anzahl waehlen!
long messeAbstandRoh() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long dauer = pulseIn(ECHO_PIN, HIGH, 30000UL); // 30 ms Timeout
if (dauer == 0) return -1; // kein Echo
return dauer / 58; // in cm
}
int medianAbstand() {
long werte[MEDIAN_N];
uint8_t gueltig = 0;
for (uint8_t i = 0; i < MEDIAN_N; i++) {
long m = messeAbstandRoh();
if (m > 0) werte[gueltig++] = m;
delay(30); // HC-SR04 braucht Pause zwischen Pings
}
if (gueltig == 0) return -1;
// Insertion Sort - bei 5 Werten voellig ausreichend
for (uint8_t i = 1; i < gueltig; i++) {
long schluessel = werte[i];
int8_t j = i - 1;
while (j >= 0 && werte[j] > schluessel) {
werte[j + 1] = werte[j];
j--;
}
werte[j + 1] = schluessel;
}
return (int)werte[gueltig / 2]; // der mittlere Wert
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
void loop() {
int abstand = medianAbstand();
if (abstand < 0) {
Serial.println(F("kein gueltiges Echo"));
} else {
Serial.print(abstand);
Serial.println(F(" cm"));
}
delay(100);
}
💡 Die stabilste Kombination: erst Median gegen Ausreißer, dann EMA gegen das verbleibende Zittern. Der Median räumt die groben Fehler weg, der EMA glättet den Rest – und du brauchst dafür keine einzige Bibliothek. Genau diese Kette nutze ich in Abstandsprojekten wie der Ultraschall-Einparkhilfe.
5Filter 4: Hysterese gegen das klackernde Relais
Ein Sonderfall, der streng genommen kein Filter ist, dir aber garantiert schon Nerven gekostet hat: Dein Relais soll ab Feuchtigkeit 500 einschalten. Der Wert pendelt um 500 – und das Relais klackert im Sekundentakt. Die Lösung sind zwei Schwellen statt einer:
#define SENSOR_PIN A0
#define RELAIS_PIN 7
#define SCHWELLE_EIN 520 // trockener als das -> Pumpe an
#define SCHWELLE_AUS 470 // feuchter als das -> Pumpe aus
#define MIN_SCHALTABSTAND 5000UL
bool pumpeLaeuft = false;
unsigned long letzteSchaltung = 0;
float emaWert = 0;
bool emaInit = false;
float ema(float neu, float alpha) {
if (!emaInit) { emaWert = neu; emaInit = true; }
emaWert = alpha * neu + (1.0 - alpha) * emaWert;
return emaWert;
}
void setup() {
Serial.begin(115200);
pinMode(RELAIS_PIN, OUTPUT);
digitalWrite(RELAIS_PIN, LOW);
}
void loop() {
float wert = ema(analogRead(SENSOR_PIN), 0.05);
unsigned long jetzt = millis();
// Hysterese: zwei Schwellen statt einer
if (!pumpeLaeuft && wert > SCHWELLE_EIN) {
if (jetzt - letzteSchaltung > MIN_SCHALTABSTAND) {
pumpeLaeuft = true;
letzteSchaltung = jetzt;
digitalWrite(RELAIS_PIN, HIGH);
Serial.println(F("Pumpe AN"));
}
} else if (pumpeLaeuft && wert < SCHWELLE_AUS) {
if (jetzt - letzteSchaltung > MIN_SCHALTABSTAND) {
pumpeLaeuft = false;
letzteSchaltung = jetzt;
digitalWrite(RELAIS_PIN, LOW);
Serial.println(F("Pumpe AUS"));
}
}
Serial.print(wert, 1);
Serial.print("\t");
Serial.println(pumpeLaeuft ? 600 : 400); // Schaltzustand mitplotten
delay(100);
}
Zwischen 470 und 520 passiert nun schlicht nichts – der Zustand bleibt, wie er ist. Zusätzlich verhindert der Mindestabstand von fünf Sekunden, dass die Pumpe bei einer schnellen Änderung hektisch taktet. Wie du solche Zeitprüfungen ohne delay() sauber aufbaust, zeigt der Artikel zu millis() statt delay().
Eine wiederverwendbare Filter-Klasse
Damit du nicht in jedem Projekt neu tippst, hier eine kleine Klasse, die Median und EMA kombiniert. Du kannst sie direkt oben in deinen Sketch kopieren oder als eigene .h-Datei ablegen:
class SensorFilter {
public:
SensorFilter(float alpha = 0.1, uint8_t medianN = 5)
: _alpha(alpha), _n(medianN > 9 ? 9 : (medianN | 1)), _init(false) {}
// Ein Rohwert rein, gefilterter Wert raus
float update(float rohwert) {
// Ringpuffer fuer den Median fuellen
_puffer[_idx] = rohwert;
_idx = (_idx + 1) % _n;
if (_anzahl < _n) _anzahl++;
float sortiert[9];
for (uint8_t i = 0; i < _anzahl; i++) sortiert[i] = _puffer[i];
for (uint8_t i = 1; i < _anzahl; i++) {
float s = sortiert[i];
int8_t j = i - 1;
while (j >= 0 && sortiert[j] > s) { sortiert[j + 1] = sortiert[j]; j--; }
sortiert[j + 1] = s;
}
float median = sortiert[_anzahl / 2];
if (!_init) { _wert = median; _init = true; }
_wert = _alpha * median + (1.0 - _alpha) * _wert;
return _wert;
}
float wert() const { return _wert; }
void reset() { _init = false; _anzahl = 0; _idx = 0; }
void setAlpha(float a) { _alpha = constrain(a, 0.001, 1.0); }
private:
float _alpha;
uint8_t _n;
bool _init;
float _wert = 0;
float _puffer[9] = {0};
uint8_t _idx = 0;
uint8_t _anzahl = 0;
};
// ---------------- Anwendung ----------------
SensorFilter feuchte(0.08, 5);
SensorFilter licht(0.20, 3); // schneller, weil Licht sich schnell aendert
void setup() {
Serial.begin(115200);
}
void loop() {
float f = feuchte.update(analogRead(A0));
float l = licht.update(analogRead(A1));
Serial.print(f, 1);
Serial.print("\t");
Serial.println(l, 1);
delay(50);
}
Welcher Filter wofür?
| Anwendung | Empfehlung | Begründung |
|---|---|---|
| Temperatur, Luftdruck, Feuchte | EMA mit alpha 0,05–0,1 | ändert sich langsam, Trägheit stört nicht |
| Bodenfeuchte für Bewässerung | EMA + Hysterese | verhindert Relais-Klackern zuverlässig |
| Ultraschall- und ToF-Abstand | Median 5 + EMA | einzelne Fehlmessungen sind hier die Regel |
| Lichtsensor für Displayhelligkeit | EMA mit alpha 0,2 | darf reagieren, soll aber nicht flackern |
| Strommessung mit ACS712 | gleitender Mittelwert über 32 | mittelt Netzbrummen sauber weg |
| Beschleunigung / Neigung | Komplementärfilter | kombiniert Gyro und Beschleunigung |
| Taster und Schalter | Entprellung, kein Filter | digitale Signale brauchen Zeitlogik |
Bonus: Komplementärfilter für den MPU-6050
Beim GY-521 hast du zwei unvollkommene Quellen: Der Beschleunigungssensor kennt die Schwerkraft, rauscht aber stark. Das Gyroskop ist ruhig, driftet aber weg. Der Komplementärfilter nimmt von jedem das Beste – in einer Zeile:
#include <Wire.h>
#define MPU_ADR 0x68
float winkel = 0;
unsigned long letzteZeit = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.beginTransmission(MPU_ADR);
Wire.write(0x6B); Wire.write(0); // aufwecken
Wire.endTransmission(true);
letzteZeit = millis();
}
void loop() {
Wire.beginTransmission(MPU_ADR);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADR, 14, true);
int16_t ax = Wire.read() << 8 | Wire.read();
int16_t ay = Wire.read() << 8 | Wire.read();
int16_t az = Wire.read() << 8 | Wire.read();
Wire.read(); Wire.read(); // Temperatur ueberspringen
int16_t gx = Wire.read() << 8 | Wire.read();
float dt = (millis() - letzteZeit) / 1000.0;
letzteZeit = millis();
float winkelAcc = atan2(ay, az) * 180.0 / PI; // rauscht, driftet nicht
float rateGyro = gx / 131.0; // Grad pro Sekunde
// 98 % Gyro (kurzfristig ruhig) + 2 % Accel (langfristig korrekt)
winkel = 0.98 * (winkel + rateGyro * dt) + 0.02 * winkelAcc;
Serial.print(winkelAcc, 1);
Serial.print("\t");
Serial.println(winkel, 1);
delay(10);
}
Troubleshooting
⚠️ Der gefilterte Wert kriecht nach dem Einschalten langsam hoch: Der Filter wurde mit 0 statt mit dem ersten Messwert initialisiert. Immer den ersten echten Wert als Startwert setzen.
⚠️ Der Wert reagiert viel zu träge: Fenster zu groß oder alpha zu klein. Halbiere das Fenster oder verdopple alpha – und prüfe, ob du wirklich Filterung brauchst oder ob ein 100-nF-Kondensator am Sensorausgang das Problem an der Wurzel löst.
⚠️ Der Mittelwert springt gelegentlich weit weg: Ein Ausreißer im Puffer. Setze einen Medianfilter davor.
⚠️ Auf dem Nano wird der Sketch instabil, sobald mehrere Filter laufen: RAM-Überlauf. 2 KB sind schnell voll. Nutze int16_t statt float in den Puffern, verkleinere die Fenster oder wechsle auf einen ESP32 mit 520 KB RAM.
⚠️ Der EMA hängt bei Ganzzahlen fest und erreicht den Zielwert nie: Klassischer Rundungsfehler bei reiner Integer-Rechnung. Nutze die Festkommavariante mit der Verschiebung um 8 Bit aus Schritt 3.
⚠️ Trotz Filter bleibt ein regelmäßiges Zittern im Takt von 20 ms: Das ist 50-Hz-Netzbrummen. Miss ein Vielfaches von 20 ms und mittle darueber – dann löscht sich der Brumm rechnerisch aus.
🚀 Bauteile zum Nachbauen
Alles, was du für die Beispiele brauchst:
Fazit
Vier Werkzeuge, und du bist für fast jedes Hobbyprojekt gerüstet: Der gleitende Mittelwert ist leicht zu verstehen, kostet aber RAM. Der EMA leistet praktisch dasselbe mit einer einzigen Variablen und ist deshalb meist die beste Wahl. Der Median ist unschlagbar gegen einzelne Ausreißer, und Hysterese beendet jedes Relais-Klackern an der Schaltschwelle.
Der wichtigste Gedanke zum Schluss: Filtern heißt immer, Ruhe gegen Reaktionszeit zu tauschen. Es gibt keinen Filter ohne Verzögerung. Stell dir also bei jedem Projekt die Frage, wie schnell dein Wert wirklich reagieren muss – und filtere genau so viel, wie nötig ist. Der Serial Plotter mit Roh- und Filterkurve nebeneinander ist dabei dein bester Freund.