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

Kapazitiver Bodenfeuchte-Sensor – ein schön verrauschter Analogwert zum Üben
ALS-PT19 Lichtsensor – zeigt Netzbrummen besonders deutlich
HC-SR04 Ultraschallsensor – der Klassiker für echte Ausreißer
GY-521 MPU-6050 – optional, für den Komplementärfilter am Ende
Relais-Modul KY-019 – um Hysterese live zu hören

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.

Zurück zum Blog