ESP32 – Warum der Chip nicht im Kreis rennen muss

Der ESP32 kann mehr, als viele Tutorials zeigen: einen Hilfsprozessor für den Tiefschlaf, umverdrahtbare Pins, Speicher, der den Schlaf überlebt, und sogar einen Mittelwellensender. Zehn dieser ESP32 Funktionen nehmen wir uns einzeln vor, jeweils mit Codebeispiel, Erklärung und den typischen Stolperfallen.

(Lesedauer: ca. 50 Minuten)

Ein ESP32 kostet rund vier Euro, hat zwei Kerne mit 240 MHz und verbringt sein Leben trotzdem oft damit, alle paar Sekunden einen Sensor zu lesen und danach im Kreis zu rennen, bis die nächste Sekunde um ist. Vom alten ESP8266 kannte ich dieses Arbeitsleben ziemlich gut, und die Tutorials, die ich bisher durchgearbeitet habe, zeigen im Grunde dasselbe: Relais schalten, Webseite ausliefern, Messwert verschicken.

Wer beim ESP32 abseits der Tutorials im Internet stöbert, stolpert über Dinge, die ich vom ESP8266 nicht kannte und die in den Tutorials, die ich kenne, nicht vorkamen: einen zweiten, winzigen Prozessor, der im Tiefschlaf (Deep Sleep) Wache hält, Pins, die sich im Code umverdrahten lassen, Speicher, der den Schlaf überlebt, bis hin zum eingebauten Hall-Sensor und einem Mittelwellensender. Einiges davon habe ich schon selbst ausprobiert, vieles lese ich hier zum ersten Mal richtig nach. Den Anstoß gab ein YouTube-Video von Tech Panda.

Wir schauen uns zuerst an, was in dem Chip eigentlich steckt (Kapitel 1). Dann nehmen wir die zehn Funktionen einzeln vor: was sie tun, wie sie intern funktionieren, ein Codebeispiel und die Stolperfallen (Kapitel 2). Am Schluss setzen wir mehrere davon zusammen, und du bekommst eine Checkliste für das nächste Projekt (Kapitel 3). Ob dein ESP32 danach noch im Kreis rennt, hängt allein von dir ab.

In diesem Artikel:

1. Einleitung zum ESP32

1.1. Der ESP32 im Maker-Alltag: ein Ferrari in der Spielstraße

Hand aufs Herz: Wie sieht ein typisches ESP32-Projekt in freier Wildbahn aus?

Meistens läuft es so ab: Man bestellt für rund vier Euro ein Entwicklungsboard beim Händler des Vertrauens. Nach zwei Wochen Vorfreude landet das gute Stück auf dem Schreibtisch. Was folgt, ist das Standard-Repertoire:

  1. Ein digitaler Ausgang schaltet ein Relais (digitalWrite(RELAY_PIN, HIGH), ein technologischer Meilenstein).
  2. Ein DHT22 oder BME280 liefert alle fünf Sekunden Temperaturwerte.
  3. Der Chip baut eine Verbindung zum Heim-WLAN auf.
  4. Eine minimalistische HTML-Webseite oder ein MQTT-Broker (ein Nachrichtenserver für Messwerte) meldet stolz: „Wohnzimmer: 21,4 °C“.

Verstehen wir uns nicht falsch: Daran ist absolut nichts verwerflich. Es funktioniert, es macht Spaß, und das Bier im Kühlschrank wird pünktlich überwacht. Aber ehrlich: Wir nutzen einen 240-MHz-Dual-Core-Prozessor mit Koprozessor, Direct Memory Access und Funk im Grunde wie einen besseren Arduino Uno von 2010. Wir kaufen einen Ferrari und schieben ihn dann im ersten Gang durch eine verkehrsberuhigte Spielstraße.

Das rächt sich spätestens dann, wenn das Projekt das heimische Steckdosen-Kabelparadies verlassen soll: Man klemmt eine handelsübliche Batterie an das Board, schaut nach drei Tagen voller Stolz wieder in den Garten und stellt fest, dass der Akku restlos leergesaugt ist. Warum? Weil der Chip mit vollen 240 MHz im Kreis gerannt ist, nur um auf die nächste Sekunde zu warten. Oder das TFT-Display ruckelt wie eine Diashow aus den Neunzigern, weil die CPU jedes einzelne Pixel mühsam per Hand durch die Leitungen tragen muss.

1.2. Was unter dem Blechdeckel steckt

Unter dem kleinen silbernen Abschirmblech des ESP32-Moduls verbirgt sich nicht einfach nur ein Mikrocontroller; dort haust ein hochintegriertes System-on-a-Chip (SoC). Espressif hat in dieses Silizium Funktionen eingegossen, für die man bei klassischen Mikrocontroller-Plattformen früher eine ganze Handvoll Zusatzplatinen, Pegelwandler und Spezialbausteine auf dem Breadboard verdrahten musste.

Damit wir uns nicht im Datenblatt-Nirwana verirren, teilen wir die zehn verborgenen Funktionen in vier Gruppen ein:

1.2.1. Energie und Tiefschlaf

Das Trio für Projekte, die nicht nach zwei Tagen an der Powerbank verhungern sollen:

  • (1) ULP-Co-Prozessor (Ultra Low Power): Ein winziger Hilfsprozessor, der im Tiefschlaf (Deep Sleep) wach bleibt, Sensoren überwacht und die Haupt-CPU nur weckt, wenn draußen tatsächlich die Hütte brennt.
  • (7) RTC-Speicher (RTC_DATA_ATTR): Ein Tiefschlaf löscht das RAM restlos, der 8 KB große RTC-Speicher bleibt jedoch unter Strom. Zähler, Zustände und Messwerte überstehen so die Schlafphasen.
  • (10) Dynamische Taktfrequenz (DFS): Warum mit 240 MHz im Kreis rennen, wenn man nur auf einen Tastendruck wartet? Per Dynamic Frequency Scaling schaltet der ESP32 zwischen 240, 80 oder 40 MHz um, und voller Dampf gibt es nur, wenn wirklich gerechnet wird.

1.2.2. Pins und Datenfluss

Hier geht es darum, dem Entwickler das Leben leichter und die Firmware schneller zu machen. Die GPIO-Matrix (2) ist der Grund, warum man beim ESP32 Pins im Code umbiegen kann: Zwischen den Peripheriemodulen (I2C, UART, SPI, PWM) und den Gehäuse-Pins sitzt ein programmierbarer Kreuzschienenverteiler, während auf herkömmlichen MCUs die Hardware den Code bestimmt. Verhedderte Leiterbahnen auf der Platine? Einfach in Software umstecken. DMA (4), also Direct Memory Access, nimmt der CPU das Schaufeln jedes einzelnen Farbbytes zum TFT-Display ab. Der DMA-Controller übernimmt den Transfer im Hintergrund, während die CPU bereits die nächsten Frames rendert: flüssige 60 FPS statt Daumenkino.

1.2.3. Sensoren im Chip

Diese drei sitzen bereits im Silizium:

  • (3) Kapazitive Touch-Pins: 10 Pins des ESP32 messen Kapazitätsänderungen. Ein Stück Alufolie oder eine Leiterbahn hinter Acrylglas genügt, und man hat unsichtbare, berührungsempfindliche Tasten ohne mechanischen Verschleiß (die den Chip sogar aufwecken können).
  • (6) Interner Hall-Effekt-Sensor: Ein echter Magnetfeldsensor unter dem Metallgehäuse. Zwar verrauscht wie ein alter Röhrenfernseher, aber für die Erkennung von „Deckel auf / Deckel zu“ via Neodym-Magnet simpel, null Zusatzbauteile.
  • (8) Interner Chip-Temperatursensor: Achtung, der misst nicht dein Wohnzimmer, sondern das Fieber des ESP32-Siliziums. Gut geeignet für die Eigendiagnose in geschlossenen Gehäusen oder bei extremer WLAN-Dauerlast.

1.2.4. Funk und Hochfrequenz

Für alle, die wissen wollen, was die Funk- und Analogbaugruppen abseits von Standard-WLAN treiben: Der Promiscuous Mode (5) ist der „Lausch-Modus“. Statt nur brav auf Pakete für die eigene IP zu warten, fängt der Chip alle 802.11-Frames in der Luft ab, die Basis für Paketanalysatoren, Raumbelegungs-Monitore und Signalstärken-Maps. Die AM-Radio-Experimente (9) machen aus dem ESP32 den kleinsten (und vermutlich schlechtesten) Mittelwellensender der Welt. Wird der interne Digital-Analog-Wandler (DAC) oder ein Sigma-Delta-Pin (siehe Abschnitt 2.9.2) mit Hochfrequenz getaktet, entsteht eine amplitudenmodulierte Trägerwelle, die ein altes Transistorradio neben dem Schreibtisch zum Klingen bringt.

1.3. Ziel und Aufbau

Wir befreien den ESP32 von seinem Dasein als überdimensionierter Schalter und schließen die Lücke zwischen Maker-Sketchen und Hardware-Architektur. Damit wir dabei nicht im Datenblatt-Dickicht steckenbleiben, folgt jede der zehn Funktionen demselben Vier-Schritt-Schema:

  1. Was macht die Funktion? (Verständnis, Einsatzzweck, Nutzen).
  2. Wie funktioniert sie intern? (Register, Stromversorgungsdomänen, Bus-Strukturen).
  3. Praxisnahes Codebeispiel (kompakt und lauffähig, für Arduino IDE oder ESP-IDF).
  4. Hinweise und Fallstricke (Einschränkungen bei S2, S3 oder C3, elektrische Grenzen, Stolperfallen für schlaflose Nächte).

Am Ende dieser Reihe verbringt dein ESP32 nicht mehr 99 % seiner Zeit mit Warten und Stromverheizen verbringen.

2. Die zehn Funktionen

Der ESP32 versauert oft als unterfordertes Relais-Anhängsel, also lüften wir jetzt die Motorhaube. Die zehn Funktionen kommen nacheinander dran, jeweils nach dem Schema aus Abschnitt 1.3.

2.1. Funktion 1: ULP-Co-Prozessor

2.1.1. Was macht die Funktion?

Stellen wir uns ein klassisches Maker-Dilemma vor: Wir wollen eine batteriebetriebene Frostwarnung für das Gewächshaus bauen. Unser Code muss eigentlich nur alle 60 Sekunden prüfen, ob die Temperatur unter den Gefrierpunkt gerutscht ist.

Wenn wir dafür den normalen ESP32 wachhalten, verbrät der Dual-Core-Bolide mit aktivem Funkteil zwischen 100 und 240 mA. Ein typischer 18650-Lithium-Ionen-Akku (etwa 3.000 mAh) kapituliert da nach rund einem Tag. Schicken wir den Chip stattdessen in den klassischen Tiefschlaf und wecken ihn jede Minute per RTC-Timer auf, muss die Haupt-CPU jedes Mal das komplette Boot-Prozedere inklusive Takteinschwingen und Speicherinitialisierung durchlaufen, nur um festzustellen: „Immer noch 18 Grad, ich leg mich wieder hin.“

Hier kommt der ULP-Co-Prozessor ins Spiel. ULP steht für Ultra Low Power, und der winzige Hilfsprozessor spielt den genügsamen Nachtwächter: Während die beiden stromhungrigen Xtensa-Kerne im Tiefschlaf ruhen, wacht der ULP in frei konfigurierbaren Abständen auf, liest Sensoren an analogen oder digitalen Pins aus, vergleicht Werte mit Schwellenwerten und weckt die Haupt-CPU nur dann auf, wenn tatsächlich Handlungsbedarf besteht.

2.1.2. Wie funktioniert sie intern?

Damit der ULP arbeiten kann, während der Rest des Chips stromlos ist, residiert er in einer isolierten Hardware-Insel: der RTC-Stromversorgungsdomäne (Real-Time Clock Power Domain). Wie Daten dort einen Tiefschlaf überstehen, zeigt Abschnitt 2.7.

  • Architektur-Varianten: Auf dem klassischen ESP32 ist der ULP als kompakte Finite State Machine (FSM) ausgeführt. Er besitzt vier 16-Bit-Allzweckregister und versteht einen reduzierten Satz an 32-Bit-Assemblerbefehlen. Die ESP32-S2 und ESP32-S3 bringen zusätzlich einen RISC-V-Kern als ULP mit, der sich komfortabel in Standard-C programmieren lässt. Beide ULP-Arten lassen sich beim Kompilieren aktivieren, zur Laufzeit läuft aber immer nur eine. Der ESP32-C5, ESP32-C6 und ESP32-P4 haben statt des ULP einen LP-Core (ebenfalls RISC-V). Der ESP32-C3 hat keinen ULP (Stand: November 2026).
  • Speicherzugriff: Der ULP besitzt keinen Zugriff auf das normale 520-KB-SRAM. Sein gesamtes Universum beschränkt sich auf das RTC Slow Memory (typischerweise 8 KB beim klassischen ESP32). In diesem Speicher liegen sowohl der Binärcode des ULP-Programms als auch alle geteilten Variablen.
  • Peripherie-Anbindung: Der ULP kann über dedizierte RTC-MUX-Register auf die RTC-fähigen GPIOs zugreifen und den internen Analog-Digital-Wandler (SAR-ADC, ein Wandler mit sukzessiver Approximation) ansteuern, um Sensorspannungen direkt abzutasten.
  • Aufwachmechanismus: Gesteuert wird der ULP von einem internen RTC-Timer. Nach Abarbeitung seines Programms schickt sich der ULP per HALT-Befehl selbst schlafen. Der Timer weckt ihn nach Ablauf der konfigurierten Periode wieder auf. Überschreitet ein Messwert die definierte Schwelle, setzt der ULP ein internes Wakeup-Signal ab, das die Stromversorgung der Haupt-CPU hochfährt.

2.1.3. Praxisnahes Codebeispiel

Da der ULP auf dem ESP32-S2/S3 dank RISC-V direkt in C programmiert werden kann (dieses Beispiel gilt nur für diese beiden Chips, nicht für den klassischen ESP32) und nicht mehr den harten Weg über kryptische FSM-Makros erfordert, demonstriert das folgende, aufeinander abgestimmte ESP-IDF-Projekt die typische Arbeitsweise: Der ULP tastet einen analogen Schwellenwert ab und weckt die schlafende Haupt-CPU.

2.1.3.1. Das ULP-Programm (main/ulp/ulp_sensor.c)

Dieser Code wird mit der ULP-Toolchain kompiliert und läuft vollständig auf dem Hilfskern:

#include "ulp_riscv.h"
#include "ulp_riscv_utils.h"
#include "ulp_riscv_adc.h"

// Variable im RTC Slow Memory: überlebt den Schlaf und ist für die Haupt-CPU lesbar
volatile int32_t ulp_letzter_adc_wert = 0;

// Schwellenwert: ca. 2400 Counts (entspricht z. B. Überhitzung/Kritischer Pegel)
#define SCHWELLENWERT_ADC 2400

int main(void)
{
    // 1. Analogen Sensorwert an RTC-ADC-Kanal einlesen
    ulp_letzter_adc_wert = ulp_riscv_adc_read_channel(ADC_UNIT_1, ADC_CHANNEL_0);

    // 2. Schwellenwertprüfung
    if (ulp_letzter_adc_wert > SCHWELLENWERT_ADC) {
        // Wecksignal an die ruhende Haupt-CPU senden!
        ulp_riscv_wakeup_main_processor();
    }

    // 3. ULP beendet seinen Durchlauf und wartet auf den nächsten RTC-Timer-Tick
    return 0;
}
2.1.3.2. Die Hauptanwendung (main/main.c)

Die Haupt-CPU initialisiert den ULP, übergibt die Schlafdauer und schaltet sich ab:

#include <stdio.h>
#include <inttypes.h>
#include "esp_sleep.h"
#include "ulp_riscv.h"
#include "ulp_main.h" // Vom Build-System automatisch aus den ULP-Symbolen generiert

// Symbole für den ULP-Binär-Blob (vom Linker bereitgestellt)
extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_sensor_bin_start");
extern const uint8_t ulp_main_bin_end[]   asm("_binary_ulp_sensor_bin_end");

void app_main(void)
{
    esp_sleep_wakeup_cause_t weckgrund = esp_sleep_get_wakeup_cause();

    if (weckgrund == ESP_SLEEP_WAKEUP_ULP) {
        // Wir wurden vom Hilfsprozessor alarmiert!
        printf("[ALARM] Aufgewacht durch ULP!\n");
        printf("Kritischer ADC-Messwert: %" PRId32 "\n", ulp_letzter_adc_wert);

        // Hier: WLAN kurz starten, MQTT-Warnung abfeuern, WLAN wieder aus...
    } else {
        printf("[INIT] Erster Kaltstart. ULP wird geladen...\n");

        // 1. ULP-Programm in das RTC Slow Memory laden
        ESP_ERROR_CHECK(ulp_riscv_load_binary(
            ulp_main_bin_start,
            (ulp_main_bin_end - ulp_main_bin_start)
        ));

        // 2. ULP als Weckquelle für den Deep Sleep autorisieren
        ESP_ERROR_CHECK(esp_sleep_enable_ulp_wakeup());

        // 3. Aufwachperiode des ULP-Timers definieren (z. B. alle 5 Sekunden)
        ulp_set_wakeup_period(0, 5000000); // Angabe in Mikrosekunden

        // 4. ULP starten
        ESP_ERROR_CHECK(ulp_riscv_run());
    }

    // Haupt-CPU verabschiedet sich in den Deep Sleep (~10 µA)
    printf("Haupt-CPU geht schlafen. ULP übernimmt die Wache...\n");
    esp_deep_sleep_start();
}

2.1.4. Hinweise und Fallstricke

  • Kein Arduino-Einzeiler: Wer gehofft hat, den ULP mit einem gemütlichen ulp.begin() in der Standard-Arduino-IDE abzufrühstücken, wird enttäuscht. Zwar existieren experimentelle Community-Bibliotheken, doch der offizielle, stabile Weg führt über ESP-IDF oder PlatformIO.
  • Kompilier-Konfiguration (menuconfig): Damit das ULP-Subsystem überhaupt mitkompiliert wird, müssen in der Projektkonfiguration die Flags CONFIG_ULP_COPROC_ENABLED=y sowie (bei S2/S3) CONFIG_ULP_COPROC_TYPE_RISCV=y aktiviert sein. Zudem muss über CONFIG_ULP_COPROC_RESERVE_MEM ausreichend RTC-RAM (z. B. 2048 Bytes) reserviert werden.
  • FSM-Registergrenze (Klassischer ESP32): Wer auf dem klassischen ESP32 noch ULP-FSM-Assembler schreibt, muss beachten, dass die internen Register nur 16 Bit breit sind. Schreibt der ULP einen 16-Bit-Wert in das 32-Bit-breite RTC-RAM, werden die oberen 16 Bits mit Adress-Informationen gefüllt. Die Haupt-CPU muss den Wert beim Auslesen mit & 0xFFFF maskieren
  • Pin-Restriktionen: Der ULP kann im Tiefschlaf ausschließlich Pins ansteuern, die zur RTC-Peripherie gehören (RTC_GPIOs). Ein Zugriff auf normale GPIOs scheitert kläglich, da deren IO-MUX-Power-Domain im Tiefschlaf schlicht abgeschaltet ist.
  • Sensoren während des Schlafs bestromen? Ein beliebter Anfängerfehler: Der ULP kommt im Tiefschlaf auf rund 100 bis 150 µA (Datenblatt), aber der angeschlossene analoge Temperatursensor hängt an dauerhaften 3,3 V und zieht munter 2 mA. Praxistipp: Den Sensor über einen RTC-GPIO mit Strom versorgen (VCC), kurz vor der Messung einschalten, sampeln und sofort wieder auf LOW ziehen.

2.2. Funktion 2: GPIO-Matrix

2.2.1. Was macht die Funktion?

Jeder, der schon einmal eine eigene Leiterplatte (PCB) geroutet oder ein komplexes Breadboard-Projekt verdrahtet hat, kennt diesen Moment der totalen Resignation: Der I2C-Sensor sitzt links unten, das SPI-Display rechts oben, und die dafür vorgesehenen Hardware-Pins des Mikrocontrollers liegen genau spiegelverkehrt auf der anderen Seite des Chips. Das Ergebnis auf der Platine gleicht einem Teller Spaghetti: Leiterbahnen müssen mit Dutzenden Durchkontaktierungen (Vias) akrobatisch übereinander springen.

Auf klassischen Mikrocontrollern wie einem ATmega328P hieß es damals: „Friss oder stirb.“ Hardware-I2C lag starr auf A4 und A5. Beim ESP32 hingegen lautet das Motto: Wir biegen die Pins einfach in Software um.

Die GPIO-Matrix ist im Grunde ein voll programmierbarer Kreuzschienenverteiler (Crossbar-Switch) im Herzen des Chips. Sie ermöglicht es, fast jedes interne digitale Signal (seien es die Daten- und Taktleitungen von I2C, die TX/RX-Pins einer UART, PWM-Kanäle oder SPI-Signale) auf nahezu jeden beliebigen physischen GPIO-Pin nach außen zu routen. Wenn sich auf deiner Platine zwei Leitungen kreuzen würden, tauschst du im Code einfach die Pins, und die Leiterbahnen laufen schnurgerade nebeneinander her.

2.2.2. Wie funktioniert sie intern?

Hinter dieser Magie steckt ein zweistufiges Hardware-Konzept aus IO_MUX und der eigentlichen GPIO-Matrix:

  1. Die GPIO-Matrix: Sie verfügt über separate Eingangs- und Ausgangs-Matrizen.
    • Für Ausgänge: Jedes Peripheriesignal (z. B. UART1_TXD_OUT oder I2CEXT0_SDA_OUT) besitzt eine eindeutige Signal-ID. Im Konfigurationsregister eines physischen Pins (GPIO_FUNCx_OUT_SEL_CFG_REG) trägt man einfach die Signal-ID ein, schon steuert diese Peripherie den Pin.
    • Für Eingänge: Jedes Peripheriemodul kann per Register (GPIO_FUNCy_IN_SEL_CFG_REG) angewiesen werden, sein Eingangssignal von einem frei gewählten physischen GPIO-Pad abzugreifen. Sogar Signal-Invertierungen lassen sich hier hardwareseitig und ohne CPU-Last aktivieren.
  2. Der IO_MUX (Pin-Multiplexer): Direkt an den physikalischen Gehäuse-Pads sitzt der IO_MUX. Jedes Pad verfügt über mehrere Betriebsmodi (Funktionen 0 bis 5). Typischerweise ist eine dieser Funktionen die Verbindung zur GPIO-Matrix, während andere Modi Direktverbindungen (Bypässe) zu bestimmten Peripheriegeräten darstellen.
  3. High-Speed-Bypass: Warum gibt es den IO_MUX überhaupt noch, wenn die Matrix so flexibel ist? Wegen der Signallaufzeit. Das Durchlaufen der Matrix kostet etwa ein bis zwei Taktzyklen Verzögerung. Für I2C (400 kHz) oder UART (115200 Baud) ist das völlig vernachlässigbar. Möchte man jedoch ein SPI-Display oder externen Flash mit sehr hohem Takt betreiben, verbietet sich der Umweg über die Matrix: Hier muss das Signal über den IO_MUX auf die fest verdrahteten Standard-Pins gelegt werden, um Signalintegrität und Timings einzuhalten.

2.2.3. Praxisnahe Codebeispiele

Das Schöne am ESP32-Ökosystem: Man muss die Register nicht manuell manipulieren. Sowohl Arduino als auch ESP-IDF bieten saubere Abstraktionen.

2.2.3.1. Beispiel A: I2C-Pins frei verlegen (Arduino IDE)

Im Arduino-Core nimmt die Wire-Bibliothek für den ESP32 schlicht Argumente für die Wunsch-Pins entgegen:

#include <Wire.h>

// Wir haben keine Lust auf die Standard-Pins 21/22,
// sondern wollen Pins 16 und 17 direkt neben unserer Steckerleiste nutzen:
#define MEIN_SDA_PIN 16
#define MEIN_SCL_PIN 17

void setup() {
  Serial.begin(115200);

  // Einfach die Wunsch-Pins an begin() übergeben – die Matrix erledigt den Rest:
  Wire.begin(MEIN_SDA_PIN, MEIN_SCL_PIN, 400000); // 400 kHz Fast-Mode

  Serial.println("I2C erfolgreich auf benutzerdefinierten Pins gestartet!");
}

void loop() {
  // Hier ganz normale I2C-Kommunikation...
}
2.2.3.2. Beispiel B: Hardware-UART frei mappen (ESP-IDF)

In professionellen Projekten schätzen Entwickler die uart_set_pin()-Funktion, um GPS-Module oder Modbus-Transceiver an freie Pins anzubinden:

#include "driver/uart.h"

#define MEIN_UART_NUM      UART_NUM_2
#define MEIN_TX_PIN        25  // Völlig frei gewählter TX-Pin
#define MEIN_RX_PIN        26  // Völlig frei gewählter RX-Pin

void init_custom_uart(void)
{
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
    };

    // 1. Treiber installieren
    uart_driver_install(MEIN_UART_NUM, 1024, 0, 0, NULL, 0);
    uart_param_config(MEIN_UART_NUM, &uart_config);

    // 2. Die GPIO-Matrix scharfschalten: Pins dynamisch zuweisen!
    // (RTS und CTS setzen wir auf UART_PIN_NO_CHANGE, da ungenutzt)
    uart_set_pin(MEIN_UART_NUM, MEIN_TX_PIN, MEIN_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

2.2.4. Hinweise und Fallstricke

So verlockend die Freiheit der GPIO-Matrix ist, so zielsicher kann man sich damit ins eigene Knie schießen, wenn man die Datenblatt-Details ignoriert:

  • Die „Input-Only“-Falle (GPIO 34 bis 39): Auf fast jedem klassischen ESP32-Board findest du die Pins GPIO 34, 35, 36 (VP), 37, 38, 39 (VN). Diese Pins besitzen keine internen Ausgangstreiber und keine programmierbaren Pull-up- oder Pull-down-Widerstände Wer versucht, hier ein I2C-SDA-Signal (das zwingend bidirektional ist und Pull-ups benötigt) oder eine UART-TX-Leitung aufzulegen, wird scheitern. Sie eignen sich ausschließlich als reine Sensoreingänge oder ADC-Pins.
  • Finger weg von den Flash-Pins (GPIO 6 bis 11): Auf vielen Entwicklungsboards sind diese Pins nach außen geführt, und wirken wie eine Einladung. Aber Vorsicht: Daran hängt intern das SPI-Flash-Modul, aus dem der ESP32 seinen Programmcode ausführt. Sobald du versuchst, an GPIO 6 eine LED blinken zu lassen, crasht der Chip sofort in einer Endlos-Boot-Schleife (Illegal Instruction).
  • Strapping-Pins beachten (GPIO 0, 2, 5, 12, 15): Diese Pins entscheiden beim Einschalten des Chips über den Boot-Modus (z. B. Download-Modus vs. SPI-Boot) oder interne Spannungsregler (z. B. GPIO 12 für Flash-Spannung). Verwendest du sie als Ausgänge für externe Peripherie, die beim Start einen Pegel erzwingt, bootet der ESP32 unter Umständen gar nicht mehr.
  • Taktfrequenz-Bremse bei SPI: Wie in der Funktionsweise erwähnt: SPI über die flexible GPIO-Matrix kommt bei hohen Takten an Grenzen (Signallaufzeit). Wer ein hochauflösendes Farbdisplay mit sehr hohem Takt befeuern will, nutzt am besten die festen, nativen IO_MUX-Pins (beim klassischen ESP32: HSPI auf 14/12/13/15 bzw. VSPI auf 18/19/23/5).

2.3. Funktion 3: Kapazitive Touch-Pins

2.3.1. Was macht die Funktion?

Mechanische Taster sind der natürliche Feind jedes eleganten Gehäusedesigns: Man bohrt schiefe Löcher in das frisch gedruckte 3D-Gehäuse, feilt an wackelnden Knöpfen herum, und nach einem halben Jahr im feuchten Keller oder im Garten korrodieren die Metallkontakte im Inneren der Mikrotaster fröhlich vor sich hin.

Wer jetzt denkt: „Kein Problem, ich kaufe mir einfach externe TTP223-Touch-Sensor-Module aus Fernost“, übersieht das Offensichtliche. Der ESP32 hat diese Funktionalität bereits zehnfach ab Werk im Silizium eingebrannt.

Die integrierten Touch-Pins des ESP32 reagieren auf elektrische Ladungsänderungen in ihrer unmittelbaren Umgebung. Ein einfacher Draht, eine auf die Innenseite des Gehäuses geklebte Alufolie oder eine freie Kupferfläche auf der Platine genügen. Der Benutzer berührt lediglich die Außenseite des isolierenden Gehäuses (Acrylglas, Kunststoff oder Glas), und der ESP32 registriert zuverlässig die Fingerannäherung.

2.3.2. Wie funktioniert sie intern?

Physikalisch betrachtet verwandelt der ESP32 deinen Finger in eine Kondensatorplatte.

  1. Die RC-Messtechnik: An jedem Touch-Pin sitzt eine interne Konstantstromquelle mit RC-Oszillator. Der Chip lädt das angeschlossene Pad periodisch auf eine definierte Spannungsschwelle auf und entlädt es wieder. Dabei zählt ein interner Taktgeber, wie viele Ladezyklen innerhalb eines festen Messfensters abgeschlossen werden können.
  2. Die Kapazitäts-Inversion (Warum kleinere Werte „mehr Touch“ bedeuten): Hier stolpert fast jeder Einsteiger: Nähert sich ein Finger dem Pad, erhöht die Masse des menschlichen Körpers die Gesamtkapazität des Systems ($C_{\text{ges}} = C_{\text{pad}} + C_{\text{finger}}$). Eine größere Kapazität braucht länger zum Laden. Infolgedessen schafft die Schaltung im selben Zeitfenster weniger Ladezyklen. Das bedeutet: Je stärker die Berührung, desto kleiner wird der Messwert
    • Ruhezustand (kein Finger): ca. 70 bis 100 Counts.
    • Berührung (Finger auf Gehäuse): stürzt ab auf ca. 10 bis 30 Counts.
  3. Hardware-Verdrahtung (Die 10 Kanäle des klassischen ESP32): Der klassische ESP32 stellt zehn dieser Sensoren bereit, die fest an bestimmte Pins gekoppelt sind:
Touch-KanalPhysischer GPIO-PinBesonderheit / Einschränkung
T0GPIO 4Beliebter Universal-Touch-Pin
T1GPIO 0Strapping-Pin! Im Ruhezustand intern HIGH; Vorsicht beim Booten
T2GPIO 2Strapping-Pin & On-Board-LED auf vielen Boards
T3GPIO 15Strapping-Pin
T4GPIO 13RTC-fähig
T5GPIO 12Strapping-Pin (Flash-Spannung! Heikel beim Booten)
T6GPIO 14RTC-fähig
T7GPIO 27Sehr gut nutzbar, keine Strapping-Konflikte
T8GPIO 33RTC-fähig
T9GPIO 32RTC-fähig

2.3.3. Praxisnahe Codebeispiele

2.3.3.1. Beispiel A: Einfacher Touch-Taster mit Schwellenwert (Arduino IDE)

Das folgende Beispiel demonstriert das Auslesen im Normalbetrieb. Sinkt der Wert unter die experimentell ermittelte Schwelle, schaltet die interne LED ein:

// Wir nutzen T0, der auf GPIO 4 herausgeführt ist
const int touchPin = 4;
const int ledPin   = 2;  // Typische On-Board-LED

// Schwellenwert: Ruhe liegt meist bei 70-100, Berührung bei < 30.
// Fällt der Zähler unter 40, werten wir das als sicheren Tastendruck:
const int SCHWELLE = 40;

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(ledPin, OUTPUT);
  Serial.println("ESP32 Kapazitiver Touch bereit!");
}

void loop() {
  // touchRead() liefert die Roh-Counts des internen Oszillators
  int touchWert = touchRead(touchPin);

  Serial.print("Aktueller Touch-Wert an GPIO 4: ");
  Serial.println(touchWert);

  if (touchWert < SCHWELLE) {
    digitalWrite(ledPin, HIGH);
    Serial.println("-> BERÜHRUNG ERKANNT!");
  } else {
    digitalWrite(ledPin, LOW);
  }

  delay(200); // Kurze Pause, um den Serial Monitor nicht zu fluten
}
2.3.3.2. Beispiel B: Aufwachen aus dem Tiefschlaf per Gehäuse-Touch

Jetzt kombinieren wir Touch mit Stromsparen: Der ESP32 geht schlafen und wacht erst auf, wenn jemand an das Gehäuse fasst:

#include <Arduino.h>

#define SCHWELLE 40 // Auslöseschwelle für das Aufwachen

void touchCallback() {
  // Muss für den Interrupt-Mechanismus existieren, bleibt aber leer
}

void setup() {
  Serial.begin(115200);
  delay(500);

  // Weckgrund analysieren
  esp_sleep_wakeup_cause_t grund = esp_sleep_get_wakeup_cause();
  if (grund == ESP_SLEEP_WAKEUP_TOUCHPAD) {
    Serial.println("Guten Morgen! Aufgewacht durch Berührung des Gehäuses.");
  } else {
    Serial.println("Kaltstart oder Reset. Ich lege mich schlafen...");
  }

  // 1. Touch-Interrupt an T0 (GPIO 4) scharfschalten
  touchAttachInterrupt(T0, touchCallback, SCHWELLE);

  // 2. Touchpad als Weckquelle für den Deep Sleep registrieren
  esp_sleep_enable_touchpad_wakeup();

  Serial.println("Gehe jetzt in den Deep Sleep. Berühre GPIO 4 zum Wecken!");
  Serial.flush();

  // 3. Tiefschlaf starten (zieht auf nacktem Chip nur ~10 µA)
  esp_deep_sleep_start();
}

void loop() {
  // Bleibt leer, da der Chip im Tiefschlaf liegt
}

2.3.4. Hinweise und Fallstricke

  • Der „Nackter-Finger-auf-Kupfer“-Fehler (ESD-Gefahr): Ein verbreiteter Irrtum: Viele klemmen eine offene Büroklammer oder ein nacktes Kabel an den Pin und patschen mit den Fingern direkt darauf. Tu das nicht! Ein kapazitiver Sensor misst ein Feld durch ein Dielektrikum (Isolator). Berührst du den metallischen Pin direkt mit statisch aufgeladenen Wollsocken, entlädt sich eine elektrostatische Spitze (ESD) direkt in die Eingangstransistoren des ESP32. Klebe immer ein Stück Isolierband, Papier, Acrylglas oder die Plastikwand deines Gehäuses dazwischen.
  • Umwelt-Einflüsse (Drift & Feuchtigkeit): Da die Messung von der Umgebungskapazität abhängt, ändert sich der Ruhewert, wenn es im Raum feuchter wird oder du das Anschlusskabel anders verlegst. Ein starrer Schwellenwert von 30 kann im Winter funktionieren und im feuchten Sommer zum Dauerauslöser werden. Für robuste Serienprojekte empfiehlt sich ein gleitender Mittelwert (Exponential Moving Average, EMA), der den Ruhewert dynamisch nachführt.
  • Strapping-Pin-Minenfeld (T1, T2, T3, T5): Was Strapping-Pins sind, steht in Abschnitt 2.2.4. Wie die Tabelle oben zeigt, liegen etliche Touch-Pins auf Strapping-Pins. Hängst du ein großes Kupferpad an GPIO 12 (T5) oder GPIO 0 (T1), kann die zusätzliche Kapazität oder ein unbedachter Pull-Widerstand beim Booten verhindern, dass der Chip überhaupt startet. Empfehlung: Nutze bevorzugt T0 (GPIO 4), T7 (GPIO 27) oder T8/T9 (GPIO 33/32).
  • Unterschiede bei ESP32-S2 / ESP32-S3: Auf neueren Chipsätzen wie dem ESP32-S2 und S3 hat Espressif das Messverfahren überarbeitet. Dort arbeiten die Register oft mit umgekehrter Logik oder völlig anderen Wertebereichen (Werte im Bereich von mehreren Tausend statt 0–100). Achte bei Portierungen immer darauf, welches Board-Target im Code aktiv ist.

2.4. Funktion 4: DMA

2.4.1. Was macht die Funktion?

Es ist der Klassiker jedes ambitionierten Display-Projekts: Man schließt ein schickes 320×240-Pixel-Farb-TFT an seinen ESP32 an, möchte eine flüssige Animation oder ein Dashboard mit Messwerten anzeigen, und stellt mit Schrecken fest: Das Display baut sich Zeile für Zeile im Zeitlupentempo auf wie damals ein Bild im 56k-Modem-Internet.

Noch frustrierender: Während der Bildschirm mühsam Pixel für Pixel mit Daten gefüttert wird, reagieren Taster nicht mehr, die Sensorabfrage stockt und die serielle Konsole friert ein. Warum? Weil die arme Haupt-CPU wie ein Paketbote fungiert, der jedes einzelne 16-Bit-Farbbyte eigenhändig aus dem RAM abholt und per SPI-Bus zum Display trägt. Bei einem Display mit 76.800 Pixeln und 2 Bytes pro Pixel sind das 153.600 Transfers, pro Einzelbild

Genau dieses Nadelöhr beseitigt DMA (Direct Memory Access). Bei aktivem DMA klinkt sich die CPU vollständig aus dem eigentlichen Kopiervorgang aus. Der Prozessor sagt der Hardware lediglich: „Hier ist mein fertiges Bild im Speicher, es ist 150 Kilobyte groß, schaufle das bitte selbstständig über den SPI-Bus zum Display und sag mir per Interrupt Bescheid, wenn du fertig bist“.

Während der DMA-Controller die Daten autonom austaktet, rechnet die CPU schon am nächsten Frame oder liest Sensoren aus.

2.4.2. Wie funktioniert sie intern?

Hinter DMA steckt ein eigenständiger Hardware-Master auf dem internen Chip-Bus, der direkten Lese- und Schreibzugriff auf das RAM besitzt.

  1. Deskriptor-Ketten (Linked Lists): Der DMA-Controller des ESP32 arbeitet mit sogenannten Scatter-Gather-Deskriptoren. Das sind kleine Kontrollstrukturen im Speicher, die Startadresse, Blocklänge und einen Zeiger auf den nächsten Block enthalten. So kann der DMA-Controller riesige Datenmengen ohne CPU-Eingriff Block für Block abarbeiten.
  2. Double-Buffering (Ping-Pong-Prinzip): Die wahre Stärke von DMA entfaltet sich erst im Teamplay mit zwei separaten Puffern (Double-Buffering):
    • Während der DMA-Controller Puffer A über SPI an das Display schickt,
    • zeichnet die CPU völlig ungestört die nächste Szene in Puffer B.
    • Ist der DMA-Transfer fertig, tauschen beide die Rollen (Ping-Pong).
    • Kein Ruckeln, kein Tearing (Zerreißeffekte bei schnellen Bewegungen) und 100 % CPU-Nutzungsgrad.
  3. Einsatzbereiche abseits von Displays: DMA beschränkt sich keineswegs nur auf SPI-Displays. Auch die I2S-Schnittstelle für Audio nutzt zwingend DMA-Puffer, um kontinuierliche Audio-Streams ohne Knackser an externe DAC-Verstärker auszugeben. Ebenso greifen High-Speed-ADC-Abtastungen oder SD-Karten-Übertragungen auf DMA zurück.

2.4.3. Praxisnahe Codebeispiele

2.4.3.1. Beispiel A: TFT-Display mit Double-Buffering und DMA (Arduino IDE)

Für Arduino ist die beliebte Bibliothek TFT_eSPI der De-facto-Standard. Das folgende Beispiel demonstriert, wie man mit zwei separaten DMA-Puffern zeichnet und asynchron austaktet:

#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();

// Blockgröße für das Rendering (z. B. 64x64 Pixel = 4096 Pixel à 2 Bytes = 8 KB)
#define BLOCK_BREITE 64
#define BLOCK_HOEHE  64
#define PUFFER_GROESSE (BLOCK_BREITE * BLOCK_HOEHE)

// Zwei getrennte Puffer im internen SRAM für Ping-Pong-DMA
uint16_t dmaPuffer1[PUFFER_GROESSE];
uint16_t dmaPuffer2[PUFFER_GROESSE];

uint16_t* aktiverRenderPuffer = dmaPuffer1;
bool pufferAuswahl = false;

void setup() {
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);

  // 1. DMA-Hardware-Kanal der SPI-Schnittstelle scharfschalten:
  // Zwingend erforderlich, sonst bleibt pushImageDMA() wirkungslos!
  tft.initDMA();

  Serial.println("TFT mit SPI-DMA erfolgreich initialisiert!");
}

void loop() {
  // Puffer umschalten (Double-Buffering)
  if (pufferAuswahl) {
    aktiverRenderPuffer = dmaPuffer2;
  } else {
    aktiverRenderPuffer = dmaPuffer1;
  }
  pufferAuswahl = !pufferAuswahl;

  // 1. CPU-Arbeit: Farbverlauf / Muster in den aktiven Puffer berechnen
  static uint16_t farbe = 0x001F; // Start mit Blau
  farbe += 0x0100;
  for (int i = 0; i < PUFFER_GROESSE; i++) {
    aktiverRenderPuffer[i] = farbe;
  }

  // 2. DMA-Transfer asynchron starten:
  // pushImageDMA() blockiert die CPU NICHT, solange der vorherige
  // Block bereits fertig übertragen wurde! Die Hardware taktet den Puffer aus.
  tft.pushImageDMA(50, 50, BLOCK_BREITE, BLOCK_HOEHE, aktiverRenderPuffer);

  // 3. Hier kann die CPU parallel rechnen, ohne auf das Display zu warten!
  // delay(10) dient hier nur zur Veranschaulichung der Framerate
  delay(16); // ca. 60 FPS
}
2.4.3.2. Beispiel B: Asynchroner SPI-DMA-Transfer (ESP-IDF)

In reinem C mit dem ESP-IDF sieht man die direkte Konfiguration des Busses mit DMA (SPI_DMA_CH_AUTO) und das Einreihen von asynchronen Transaktionen (spi_device_queue_trans):

#include "driver/spi_master.h"
#include "esp_heap_caps.h"
#include "esp_log.h"

#define PIN_MOSI 23
#define PIN_SCLK 18
#define PIN_CS    5

static spi_device_handle_t spi;

void init_dma_spi(void)
{
    // 1. Bus konfigurieren
    spi_bus_config_t buscfg = {
        .mosi_io_num = PIN_MOSI,
        .miso_io_num = -1, // Nur Senden
        .sclk_io_num = PIN_SCLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4096, // Maximale DMA-Paketgröße
    };

    // 2. Bus initialisieren mit automatischer DMA-Kanal-Zuweisung
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));

    // 3. Display-Device anhängen
    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 40 * 1000 * 1000, // 40 MHz Takt
        .mode = 0,
        .spics_io_num = PIN_CS,
        .queue_size = 7, // Schlange für bis zu 7 überlappende DMA-Transfers
    };
    ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &spi));
}

void sende_dma_daten(void)
{
    // DMA-Puffer MÜSSEN im DMA-fähigen internen Speicher allokiert werden!
    size_t pufferGroesse = 2048;
    uint8_t *dma_buffer = heap_caps_malloc(pufferGroesse, MALLOC_CAP_DMA);

    // Testdaten füllen
    for (int i = 0; i < pufferGroesse; i++) dma_buffer[i] = (uint8_t)i;

    spi_transaction_t t = {
        .length = pufferGroesse * 8, // Länge in BITS
        .tx_buffer = dma_buffer,
    };

    // Asynchron in die DMA-Queue einreihen (kehrt sofort zur CPU zurück!)
    ESP_ERROR_CHECK(spi_device_queue_trans(spi, &t, portMAX_DELAY));

    // Die CPU kann JETZT etwas anderes tun...

    // Später (oder vor Wiederverwendung des Puffers) auf Fertigstellung warten:
    spi_transaction_t *fertige_trans;
    ESP_ERROR_CHECK(spi_device_get_trans_result(spi, &fertige_trans, portMAX_DELAY));

    heap_caps_free(dma_buffer);
}

2.4.4. Hinweise und Fallstricke

  • Der „Speicher-Ausrichtungs“-Absturz (32-Bit Alignment & MALLOC_CAP_DMA): Der DMA-Controller ist pingelig: Er kann Daten nicht von beliebigen Adressen fressen. Puffer müssen im internen SRAM liegen und zwingend an einer 32-Bit-Grenze (Word-aligned) ausgerichtet sein. Wer einfach ein globales uint8_t puffer anlegt, riskiert, dass der Compiler den Puffer an einer ungeraden Byte-Adresse platziert, der DMA-Transfer scheitert mit Speicherfehlern oder überschreibt Nachbar-Variablen. In ESP-IDF immer mit heap_caps_malloc(groesse, MALLOC_CAP_DMA) arbeiten
  • Die „Ich zeichne in den aktiven Puffer“-Falle (Race Condition): Sobald du pushImageDMA() aufgerufen oder eine Transaktion in die Queue geschoben hast, gehört der Puffer der Hardware. Wenn du im Code denselben Puffer sofort weiter veränderst, während der DMA-Kanal noch Bits heraustaktet, sendet er eine wilde Mischung aus alten und neuen Pixeln (zerhackte Grafikartefakte). Verwende immer zwei getrennte Puffer (Double-Buffering) oder frage mit spi_device_get_trans_result() ab, ob der Transfer abgeschlossen ist.
  • Kein Halbduplex-Mischbetrieb mit DMA: Laut Espressif-Dokumentation unterstützen die Hardware-SPI-Controller des ESP32 im DMA-Betrieb keine Halbduplex-Transaktionen, die gleichzeitig Lese- und Schreibphasen umfassen. Für Displays (die ohnehin nur Daten empfangen) ist das irrelevant, bei komplexen SPI-Sensoren mit kombinierten Schreib-/Lese-Befehlen muss man diesen Stolperstein jedoch kennen.
  • PSRAM-Restriktionen: Auf ESP32-Modulen mit externem PSRAM (z. B. 4 oder 8 MB SPI-RAM) kann der klassische DMA-Controller nicht ohne Weiteres direkt auf den externen PSRAM-Speicher zugreifen. Der DMA-Puffer muss im internen, schnellen On-Chip-SRAM liegen (oder der Treiber muss aufwändige Zwischenkopien anlegen). Erst neuere Chips wie der ESP32-S3 bieten einen verbesserten General DMA (GDMA) mit erweitertem PSRAM-Support.

2.5. Funktion 5: Promiscuous Mode

2.5.1. Was macht die Funktion?

In Hollywood-Filmen sieht Netzwerkanalyse meist so aus: Ein vermummter Hacker tippt drei Tasten auf einem Laptop, ein grüner Fortschrittsbalken rast über den Bildschirm, und wie von Zauberhand fliegen geheime Chatnachrichten und Passwörter als Klartext durch den Raum.

Die Realität auf unserem ESP32 sieht nüchterner aus, hardwaretechnisch aber kaum weniger faszinierend.

Normalerweise verhält sich der WLAN-Chip eines ESP32 wie ein wohlerzogener Postempfänger: Er lauscht nur auf die Pakete, auf denen exakt seine eigene MAC-Adresse als Empfänger steht. Alles andere, was durch den Äther schwirrt, verwirft der Filter des Funkchips eiskalt direkt in der Hardware.

Aktivieren wir jedoch den Promiscuous Mode (wörtlich: den „freizügigen“ Modus, in der Praxis schlicht Sniffer-Modus genannt), schalten wir diesen internen MAC-Adressfilter komplett ab. Der ESP32 lauscht nun auf die Rohdaten des aktuell eingestellten Funkkanals und übergibt jedes einzelne 802.11-Funkpaket, das seine Antenne auffängt, direkt an eine Callback-Funktion im Code.

Dabei knackt der ESP32 keineswegs magisch fremde Passwörter oder liest private Chats mit (WPA2/WPA3-Nutzdaten bleiben natürlich verschlüsselt). Doch was er sieht, ist das gesamte „Skelett“ des lokalen Äthers: Signalstärken (RSSI), Funkkanäle, Beacons von Routern und vor allem die ständigen Probe Requests von Smartphones, die nach bekannten Netzwerken suchen. Das macht den Chip zur idealen Basis für passive Präsenzerkennungen (z. B. Besucherzählung im Maker-Space ohne Netzwerkanmeldung), WLAN-Scanner oder Signalstärke-Heatmaps.

2.5.2. Wie funktioniert sie intern?

Damit der ESP32 Datenpakete über die Antenne empfangen kann, die gar nicht für ihn bestimmt sind, umgeht er den regulären Netzwerk-Stack des Betriebssystems.

  1. Pakettypen und Hardware-Filterung: Im Promiscuous Mode kann der ESP32 vier Hauptkategorien von IEEE 802.11-Frames empfangen:
    • Management-Frames (WIFI_PROMIS_FILTER_MASK_MGMT): Beacons (Router-Ankündigungen), Probe Requests (Smartphones auf Netzwerksuche), Probe Responses, Deauthentifizierungs-Pakete.
    • Daten-Frames (WIFI_PROMIS_FILTER_MASK_DATA): Tatsächliche Datenpakete (MPDU, AMPDU). Die Nutzdaten verschlüsselter Netze sind unlesbar, aber Header, Sender-, Empfänger- und Router-MAC-Adressen sind im Klartext sichtbar.
    • Kontroll-Frames (WIFI_PROMIS_FILTER_MASK_CTRL): RTS/CTS (Kollisionsvermeidung) und ACKs (Bestätigungen).
    • Fehlerhafte Frames: Pakete mit CRC-Prüfsummenfehlern können zur Störungsanalyse optional erfasst werden.
  2. Die Datenstruktur wifi_promiscuous_pkt_t: Der WLAN-Treiber übergibt dem Callback keinen rohen Bytestrom, sondern verpackt das Paket in eine strukturierte Hülle. Diese enthält zwei elementare Bereiche:
    • rx_ctrl (Metadaten der Funkübertragung): Hier liefert die Hardware wertvolle Funk-Telemetriedaten, ohne dass man das Paket selbst parsen muss: den Empfangspegel rssi (in dBm), den aktuellen channel, die Modulationsart und Signal-Rausch-Verhältnisse.
    • payload (Der rohe 802.11-Frame): Ein Byte-Array, das direkt mit dem IEEE 802.11-Header beginnt (Frame-Control, Duration, Adressfelder 1 bis 3, Sequenznummer).
  3. Kanal-Restriktion (Warum Channel-Hopping Pflicht ist): Ein Funkempfänger kann physikalisch immer nur auf einer einzigen Frequenz gleichzeitig lauschen. Da das 2,4-GHz-Band in 13 Kanäle aufgeteilt ist, sieht der ESP32 auf Kanal 1 nichts von dem, was auf Kanal 6 oder 11 passiert. Wer das gesamte Umfeld scannen will, muss in Software einen Timer laufen lassen, der periodisch im Kreis die Kanäle weiterschaltet (Channel-Hopping, z. B. alle 300 ms einen Kanal weiter).

2.5.3. Praxisnahe Codebeispiele

Das folgende, aufeinander abgestimmte ESP-IDF-Projekt zeigt, wie man den Sniffer sauber initialisiert, nach Management-Frames filtert und die Probe Requests von Geräten in der Umgebung auswertet.

2.5.3.1. Vollständiger ESP-IDF Sniffer mit Probe-Request-Filter

Dieser Code versetzt den Chip in den Promiscuous Mode, fixiert ihn auf Kanal 6 und lauscht gezielt auf Suchanfragen von mobilen Geräten:

#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "SNIFFER";

// Callback-Funktion: Wird vom Wi-Fi-Treiber-Task für JEDES passende Paket aufgerufen!
static void wifi_sniffer_packet_handler(void *buf, wifi_promiscuous_pkt_type_t type)
{
    // Wir werten ausschließlich Management-Frames aus (z. B. Beacons & Probe Requests)
    if (type != WIFI_PKT_MGMT) {
        return;
    }

    const wifi_promiscuous_pkt_t *pkt = (const wifi_promiscuous_pkt_t *)buf;
    const uint8_t *payload = pkt->payload;

    // Funk-Metadaten direkt aus der rx_ctrl-Struktur lesen
    int rssi    = pkt->rx_ctrl.rssi;
    int channel = pkt->rx_ctrl.channel;
    int laenge  = pkt->rx_ctrl.sig_len;

    // 802.11 MAC-Header analysieren:
    // Byte 0 enthält Frame-Typ und Subtyp.
    // 0x40 steht binär für: Type = 00 (Management), Subtype = 0100 (Probe Request)
    uint8_t frame_control = payload[0];

    if (frame_control == 0x40) {
        // Im 802.11 Management-Frame liegt die Sender-MAC (Transmitter Address) ab Offset 10:
        const uint8_t *src_mac = &payload[10];

        ESP_LOGI(TAG, "Probe Request erkannt! RSSI: %3d dBm | Kanal: %2d | Sender-MAC: %02X:%02X:%02X:%02X:%02X:%02X | Laenge: %d Bytes",
                 rssi, channel,
                 src_mac[0], src_mac[1], src_mac[2],
                 src_mac[3], src_mac[4], src_mac[5],
                 laenge);
    }
}

void app_main(void)
{
    // 1. NVS initialisieren (zwingend erforderlich für den ESP32 Wi-Fi-Treiber)
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 2. Wi-Fi-Subsystem initialisieren (Modus STA, aber OHNE uns irgendwo einzuwählen!)
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 3. Callback-Funktion beim Treiber registrieren
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(wifi_sniffer_packet_handler));

    // 4. Hardware-Filter konfigurieren: Nur Management-Frames an den Callback weiterleiten
    wifi_promiscuous_filter_t filter = {
        .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
    };
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter));

    // 5. Promiscuous Mode aktivieren
    ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));

    // 6. Auf einen festen Kanal einstellen (z. B. Kanal 6)
    ESP_ERROR_CHECK(esp_wifi_set_channel(6, WIFI_SECOND_CHAN_NONE));

    ESP_LOGI(TAG, "Sniffer lauscht jetzt auf Kanal 6 nach Probe Requests...");

    // Haupt-Task kann sich schlafen legen oder hier periodisch den Kanal wechseln (Channel-Hopping)
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

2.5.4. Hinweise und Fallstricke

  • Der „Callback blockiert den WLAN-Treiber“-Kollaps: Das ist eine häufige Ursache für scheinbar unerklärliche Guru-Meditation-Abstürze oder Watchdog-Resets: Der wifi_promiscuous_rx_cb-Callback wird direkt im Kontext des internen WLAN-Treiber-Tasks ausgeführt. In belebten Umgebungen (Büro, Bahnhof, Wohnsiedlung) prasseln locker 500 bis 1.500 Pakete pro Sekunde herein. Wer in diesem Callback aufwändige String-Formatierungen, Serial.print() oder gar Speicherallokationen durchführt, bringt den WLAN-Treiber zum Überlaufen. Praxistipp: Im Callback lediglich die nötigsten Bytes (MAC, RSSI) in eine FreeRTOS-Queue schieben und die Auswertung in einen separaten Task auslagern.
  • Kein magisches Klartext-Lesen: Auf WPA2/WPA3-verschlüsselten Netzen bleibt der Nutzdatenbereich eines Daten-Frames unlesbar (Abschnitt 2.5.1). Lesbar sind nur die unverschlüsselten Header: MAC-Adressen, Paketgrößen, Protokoll-IDs.
  • MAC-Address-Randomization moderner Smartphones: Wer eine Anwesenheitserkennung per Smartphone-Tracking bauen will, stößt auf eine harte Nuss: Moderne Betriebssysteme (iOS, Android) verwenden für ihre Probe Requests rotierende, zufällig generierte Fake-MAC-Adressen (MAC Randomization). Ein einzelnes Smartphone taucht daher im Sniffer-Log oft mit zehn verschiedenen Adressen auf. Seriöse Zählungen erfordern ausgefeilte statistische Filterung anhand von Signalstärken und Sequenznummern.
  • Kollision mit regulärem WLAN-Betrieb: Der Promiscuous Mode kann zwar auch aktiviert werden, wenn der ESP32 gleichzeitig als WLAN-Station mit einem Router verbunden ist. Aber Vorsicht: Da der Treiber nun ununterbrochen alle Pakete aus der Luft verarbeiten muss, bricht der Datendurchsatz einer normalen TCP/IP-Verbindung massiv ein. Für stabile Webserver- oder MQTT-Dienste sollte der Promiscuous Mode nicht im Dauerbetrieb nebenher laufen.
  • Rechtlicher Rahmen: Funkwellen machen an Grundstücksgrenzen nicht halt. Das passive Mitschneiden von Funkverkehr im privaten Heimnetzwerk zu Test- und Lernzwecken ist unkritisch. Das Erfassen, Speichern und Verarbeiten von Klarnamen-SSIDs oder Hardware-MAC-Adressen fremder Personen im öffentlichen Raum berührt jedoch datenschutzrechtliche Grenzen (DSGVO / Telekommunikationsgesetze). MAC-Adressen gelten nach Ansicht vieler Aufsichtsbehörden als personenbezogene Daten, die Frage ist aber umstritten (Stand: November 2026). Dazu kommt, dass Smartphones in Probe Requests oft zufällige MAC-Adressen senden. Daher gilt: Für eigene Analysen, Laborversuche und Signalmessungen nutzen, nicht zur Nachbarschaftsüberwachung

2.6. Funktion 6: Hall-Sensor

2.6.1. Was macht die Funktion?

Stellen wir uns ein typisches Maker-Projekt vor: Ein smarter Briefkasten oder ein Kontrollkästchen für den 3D-Drucker. Unser Ziel ist denkbar simpel: Wir möchten wissen, ob die Klappe oder der Gehäusedeckel auf- oder zugeklappt ist.

Normalerweise greift man in die Bastelkiste und lötet entweder einen mechanischen Mikroschalter an (der sich nach drei Wochen verbiegt) oder verdrahtet ein externes Reed-Kontakt- bzw. Hall-Sensor-Modul inklusive Pull-up-Widerständen und Kabelgewirr. Wer jedoch ein klassisches ESP32-Board auf dem Tisch liegen hat, kann sich diesen Bauteile-Aufwand komplett sparen:

Unter dem metallischen Abschirmblech des klassischen ESP32 sitzt ein integrierter Hall-Sensor. Nähert man einen Magneten der Oberfläche des Chips, schlägt das interne Messwerk an.

Seien wir an dieser Stelle aber direkt selbstironisch und ehrlich: Ein hochpräzises Magnetometer ist dieser Sensor beileibe nicht. Fürs Erdmagnetfeld bis auf die vierte Nachkommastelle solltest du die Erwartungen drastisch herunterschrauben. Das interne Signal rauscht wie ein schlecht eingestellter UKW-Empfänger. Aber als cleverer Null-Komponenten-Trick für die reine Schwellenwert-Erkennung, etwa „Liegt der Gehäusedeckel auf?“ oder „Dreht sich das Zahnrad mit dem kleinen Neodym-Magneten noch?“, ist das Feature unschlagbar charmant.

2.6.2. Wie funktioniert sie intern?

Der Hall-Effekt ist ein grundlegendes physikalisches Prinzip: Fließt ein Strom durch einen stromdurchflossenen Halbleiter und wird senkrecht dazu ein Magnetfeld angelegt, lenkt die Lorentzkraft die Ladungsträger seitlich ab. Es entsteht eine winzige Querspannung: die Hall-Spannung.

  1. Das On-Chip-Sensorelement: Das eigentliche Hall-Sensorelement ist direkt in das Silizium des Mikrocontrollers integriert. Seine Grundempfindlichkeit ist naturgemäß gering, weshalb die resultierende Spannung im Mikrovolt-Bereich liegt.
  2. Der rauscharme Verstärker (LNA) und ADC1: Um aus diesem winzigen Signal überhaupt etwas Brauchbares herauszulesen, leitet der Chip das Signal über einen internen rauscharmen Vorverstärker (Low-Noise Amplifier, LNA) und tastet es über den SAR-ADC 1 ab.
  3. Polaritätserkennung (Nord- vs. Südpol): Der Sensor reagiert polaritätsabhängig:
    • Nähert man dem Chip den Nordpol eines Magneten, steigt der Messwert ins Positive.
    • Dreht man den Magneten um und nähert den Südpol, kippt der Messwert ins Negative.
    • Im Ruhezustand ohne Magnet pendelt der Wert, bedingt durch thermisches Rauschen und Fertigungstoleranzen, meistens um die Null herum (typischerweise im Bereich von ca. -30 bis +30).

2.6.3. Praxisnahe Codebeispiele

Da der Sensor von Haus aus stark zappelt, wäre es fahrlässig, einen einzelnen Rohwert unbedarft abzufragen. Das folgende Arduino-Beispiel demonstriert deshalb eine praxistaugliche Schwellenwert-Logik mit gleitender Mittelwertbildung (Software-Filter), um Fehlauslösungen durch das Grundrauschen sicher zu unterbinden:

#include <Arduino.h>

// On-Board-LED auf vielen ESP32-DevKits (meist GPIO 2)
const int ledPin = 2;

// Filter-Konfiguration: Wir mitteln über 10 aufeinanderfolgende Samples
const int ANZAHL_SAMPLES = 10;

// Schwellenwert für den Betrag: Ruhepegel liegt meist bei +/- 20 bis 30.
// Ab einem Ausschlag von |Wert| > 60 gilt ein Magnetfeld als sicher erkannt.
const int SCHWELLE = 60;

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  Serial.println("=========================================");
  Serial.println("  ESP32 Interner Hall-Sensor Test        ");
  Serial.println("=========================================");
  Serial.println("Bewege einen Neodym-Magneten an den Chip!");
}

// Funktion zur Glättung der verrauschten Messwerte
int leseGlaettetenHallWert() {
  long summe = 0;
  for (int i = 0; i < ANZAHL_SAMPLES; i++) {
    // hallRead() ist die native Arduino-Core-Funktion für den Sensor
    summe += hallRead();
    delay(5); // Kurze Einschwingzeit zwischen den ADC-Abtastungen
  }
  return (int)(summe / ANZAHL_SAMPLES);
}

void loop() {
  int gefilterterWert = leseGlaettetenHallWert();

  Serial.print("Gefilterter Hall-Wert: ");
  Serial.print(gefilterterWert);

  // Prüfung anhand des Betrags (erkennt sowohl Nord- als auch Südpol)
  if (abs(gefilterterWert) > SCHWELLE) {
    digitalWrite(ledPin, HIGH);

    if (gefilterterWert > 0) {
      Serial.println("  --> [MAGNET ERKANNT: NORDPOL]");
    } else {
      Serial.println("  --> [MAGNET ERKANNT: SUEDPOL]");
    }
  } else {
    digitalWrite(ledPin, LOW);
    Serial.println("  --> [Ruhezustand: Kein Magnet]");
  }

  delay(200); // Entspannte Wiederholrate
}

Tipp für Visualisierungs-Fans: Wenn du den Sketch in der Arduino IDE hochgeladen hast, schließe den seriellen Monitor und öffne unter Werkzeuge -> Serieller Plotter die Echtzeit-Kurve. Wenn du mit einem Neodym-Magneten über das Metallgehäuse fährst, siehst du die Ausschläge nach oben und unten wie auf einem Mini-EKG.

2.6.4. Hinweise und Fallstricke

Der interne Hall-Sensor ist der „Exot“ unter den ESP32-Funktionen, und genau deshalb ranken sich um ihn die meisten Frustmomente im Forum:

  • Die „Phantom-Pins“-Falle (GPIO 36 und GPIO 39 freihalten): Das ist eine echte Gemeinheit der Chip-Architektur: Der interne Vorverstärker (LNA) des Hall-Sensors teilt sich die Leitungen mit den analogen Eingangspins GPIO 36 (SENSOR_VP) und GPIO 39 (SENSOR_VN). Sobald du den internen Hall-Sensor nutzt, dürfen an diesen beiden Pins keine externen Schaltungen, Taster oder Potis hängen Externe Spannungen an diesen Pins verfälschen das Hall-Signal drastisch oder setzen den internen Sensor komplett außer Gefecht.
  • Kein Sensor auf modernen Chips (S2, S3, C3, C6 etc.): Hier fallen regelmäßig Maker aus allen Wolken, die ihren Code von einem alten NodeMCU-ESP32 auf einen modernen ESP32-S3 oder ESP32-C3 portieren: Espressif hat den internen Hall-Sensor auf allen neueren Chip-Familien ersatzlos gestrichen Den Sensor gibt es ausschließlich auf dem klassischen ESP32 (ESP32-D0WDQ6, ESP32-WROOM-32).
  • Software-Dämmerung (Stand: November 2026): Die offizielle Unterstützung ist in neueren Treiber-Versionen entfallen (Stand: November 2026):
    • Im aktuellen ESP-IDF v5.x / v6.x wurde die Low-Level-Funktion hall_sensor_read() entfernt; seit ESP-IDF 5.0 gilt „Hall sensor is no longer supported on ESP32“.
    • Beim Arduino-Core hängt es von der Version ab, ob hallRead() noch zur Verfügung steht. Mit Version 2.0.x lässt sich der Sensor nutzen; bei neueren Versionen schaust du vor dem Einsatz in die Migrationshinweise des Cores.
  • Hohe Exemplarstreuung & Temperaturdrift: Vergleicht man drei verschiedene ESP32-Boards auf dem Tisch, liefert Board A im Ruhezustand vielleicht +12, Board B -28 und Board C +45. Zudem wandert der Nullpunkt, sobald sich der Chip im laufenden Betrieb erwärmt. Feste Schwellenwerte sollten daher immer großzügig gewählt oder dynamisch beim Booten eingemessen werden.

2.7. Funktion 7: RTC-Speicher

2.7.1. Was macht die Funktion?

Wer sich schon einmal an einem autarken Sensorprojekt die Zähne ausgebissen hat, kennt dieses Phänomen: Man schickt den ESP32 in den wohlverdienten Tiefschlaf, um Strom zu sparen. Der Chip schläft wie ein Murmeltier, verbraucht kaum Energie und wacht zehn Minuten später auf.

Beim Aufwachen gibt es kein sanftes Räkeln, sondern einen Schlag auf den Hinterkopf: Der ESP32 führt einen Kaltstart durch. Die Ausführung beginnt wieder ganz von vorn bei setup(). Das gesamte normale Arbeitsspeicher-Universum (SRAM) wurde im Schlaf stromlos geschaltet und damit restlos ausradiert.

Möchte man einen fortlaufenden Zähler führen, Sensorwerte der letzten Stunden mitteln oder WLAN-Kalibrierungsdaten zwischenspeichern, stand man früher vor zwei schlechten Alternativen:

  1. Den internen Flash-Speicher (NVS/Preferences) nutzen: Das funktioniert zwar, zermürbt aber auf Dauer die Flash-Speicherzellen durch ständige Schreibzyklen und kostet unnötig viel Zeit und Strom beim Booten.
  2. Den Chip gar nicht schlafen legen: Und zusehen, wie der Akku nach zwei Tagen kapituliert.

Hier kommt der RTC-Speicher ins Spiel. Er dient als Überlebensinsel für Daten: Indem man eine Variable schlicht mit dem Compiler-Attribut RTC_DATA_ATTR versieht, landet sie in einem speziellen SRAM-Bereich, der im Tiefschlaf unter Strom bleibt. Die Variable behält ihren Wert über Tausende Schlafzyklen hinweg, ganz ohne Flash-Verschleiß und mit sofortigem Zugriff im Nanosekundenbereich.

2.7.2. Wie funktioniert sie intern?

Auf dem Silizium-Die ist der Speicher nicht ein einziger monolithischer Block, sondern in verschiedene Stromversorgungsdomänen (Power Domains) unterteilt.

  1. Die RTC-Speicherbänke: Der ESP32 besitzt im RTC-Subsystem dedizierten SRAM:
    • RTC Fast Memory (8 KB): Hauptsächlich für schnellen Code gedacht, den die Haupt-CPU direkt nach dem Aufwachen ausführen kann (Wake Stubs, kleine Programme, die noch vor dem eigentlichen Systemstart laufen), bevor der Rest des Systems initialisiert ist.
    • RTC Slow Memory (8 KB): Dient als Daten- und Programmspeicher für den ULP-Co-Prozessor sowie als Ablageort für Variablen der Haupt-CPU, die Schlafzyklen überdauern sollen.
  2. Die Magie des Linker-Skripts (RTC_DATA_ATTR): Was macht RTC_DATA_ATTR eigentlich unter der Haube? Im Header esp_attr.h ist dieses Makro definiert als:
#define RTC_DATA_ATTR _SECTION_ATTR_IMPL(".rtc.data", __COUNTER__)

Es weist den Compiler (GCC) an, die deklarierte Variable nicht im normalen BSS- oder Daten-Segment des Haupt-SRAMs zu platzieren, sondern sie in die Linker-Sektion .rtc.data einzubetten. Beim Flashen wird dieser Bereich einmalig initialisiert; geht der Chip anschließend in den Tiefschlaf, trennt das Power-Management die Spannungsversorgung des großen Haupt-SRAMs, während die Versorgungsschiene des RTC-Speichers mit minimalem Leckstrom weiterläuft.

  1. Aufwachgrund-Analyse (esp_sleep_get_wakeup_cause()): Der RTC-Controller führt Buch darüber, wer den Chip überhaupt geweckt hat. Über die API-Funktion esp_sleep_get_wakeup_cause() lässt sich abfragen, ob das Erwachen durch den RTC-Timer (ESP_SLEEP_WAKEUP_TIMER), einen Taster (ESP_SLEEP_WAKEUP_EXT0), einen Touch-Pin (ESP_SLEEP_WAKEUP_TOUCHPAD) oder den ULP (ESP_SLEEP_WAKEUP_ULP) ausgelöst wurde.

2.7.3. Praxisnahe Codebeispiele

2.7.3.1. Beispiel A: Boot-Zähler & Weckgrund-Diagnose (Arduino IDE)

Das folgende Beispiel zeigt das Zusammenspiel von RTC_DATA_ATTR mit dem internen Schlaf-Timer und der Weckgrund-Auswertung:

#include <Arduino.h>

// Variable wird im RTC-Slow-SRAM platziert und überlebt den Deep Sleep
RTC_DATA_ATTR int bootZaehler = 0;

// Umrechnungsfaktor: Mikrosekunden in Sekunden (zwingend ULL nutzen!)
#define uS_TO_S_FAKTOR 1000000ULL
#define SCHLAFZEIT_SEK  5

void zeigeWeckgrund() {
  esp_sleep_wakeup_cause_t grund = esp_sleep_get_wakeup_cause();

  switch (grund) {
    case ESP_SLEEP_WAKEUP_TIMER:
      Serial.println("-> Weckgrund: Interner RTC-Timer");
      break;
    case ESP_SLEEP_WAKEUP_EXT0:
      Serial.println("-> Weckgrund: Externer Taster an RTC-GPIO");
      break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD:
      Serial.println("-> Weckgrund: Kapazitiver Touch-Pin");
      break;
    case ESP_SLEEP_WAKEUP_ULP:
      Serial.println("-> Weckgrund: ULP-Co-Prozessor");
      break;
    default:
      Serial.println("-> Weckgrund: Erster Start nach Reset / Power-On");
      break;
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000); // Zeit für das Öffnen des Seriellen Monitors

  // Zähler inkrementieren
  bootZaehler++;

  Serial.println("=========================================");
  Serial.printf("ESP32 aufgewacht! Durchlauf Nr.: %d\n", bootZaehler);
  zeigeWeckgrund();
  Serial.println("=========================================");

  // Nach 5 Zyklen setzen wir den Zähler zur Demonstration zurück
  if (bootZaehler >= 5) {
    Serial.println("Limit erreicht: Setze Boot-Zähler zurück auf 0.");
    bootZaehler = 0;
  }

  // 1. Weckquelle konfigurieren: Nach 5 Sekunden wecken
  esp_sleep_enable_timer_wakeup(SCHLAFZEIT_SEK * uS_TO_S_FAKTOR);

  Serial.println("Gute Nacht! Gehe jetzt in den Deep Sleep...");
  Serial.flush(); // Zwingend Puffer leeren, bevor der Takt stirbt!

  // 2. Ab in den Tiefschlaf (Code dahinter wird NIE erreicht!)
  esp_deep_sleep_start();
}

void loop() {
  // Bleibt bei Deep-Sleep-Anwendungen immer leer!
}
2.7.3.2. Beispiel B: Messwerte puffern, WLAN sparen

Der größte Stromfresser ist das WLAN-Radio (bis zu 240 mA). Anstatt bei jedem Aufwachen mühsam eine WLAN-Verbindung aufzubauen, sammeln wir im RTC-Speicher zehn Messwerte und funken sie erst im zehnten Zyklus gebündelt ab:

#include <Arduino.h>

// Puffer-Array im RTC-Speicher für bis zu 10 Messungen
RTC_DATA_ATTR float messwertPuffer[10];
RTC_DATA_ATTR int messIndex = 0;

#define MAX_MESSUNGEN 10
#define SCHLAF_INTERVALL_SEK 60 // Alle 60 Sekunden messen

void sendeDatenAnCloud() {
  Serial.println(">>> 10 Messwerte voll! Starte WLAN und sende Paket... <<<");
  // Hier: WiFi.begin(), MQTT-Paket mit allen 10 Werten senden, WiFi.disconnect(true)...
}

void setup() {
  Serial.begin(115200);
  delay(300);

  // 1. Simulierter Sensorwert (z. B. analogRead oder BME280)
  float aktuelleTemperatur = 20.5 + (random(-20, 20) / 10.0);
  messwertPuffer[messIndex] = aktuelleTemperatur;

  Serial.printf("Messung %d/10 erfasst: %.2f °C\n", messIndex + 1, aktuelleTemperatur);
  messIndex++;

  // 2. Prüfen, ob wir das WLAN-Radio überhaupt wecken müssen
  if (messIndex >= MAX_MESSUNGEN) {
    sendeDatenAnCloud();
    messIndex = 0; // Puffer zurücksetzen
  } else {
    Serial.println("WLAN bleibt AUS. Spare wertvolle Milliampere!");
  }

  // 3. Sofort wieder schlafen legen
  esp_sleep_enable_timer_wakeup(SCHLAF_INTERVALL_SEK * 1000000ULL);
  Serial.flush();
  esp_deep_sleep_start();
}

void loop() {}

2.7.4. Hinweise und Fallstricke

  • Der Unterschied zwischen Tiefschlaf und ESP.restart(): Das ist die Falle, in die fast jeder Entwickler mindestens einmal tappt: Variablen mit RTC_DATA_ATTR überstehen ausschließlich den Tiefschlaf
    • Drückst du den physischen EN-Reset-Taster auf dem Board,
    • verliert der Chip die Betriebsspannung (Power Cycle),
    • schlägt der Watchdog-Timer zu oder
    • rufst du im Code ESP.restart() auf: In all diesen Fällen wird auch die RTC-Sektion neu initialisiert und deine Daten sind futsch Wer Daten braucht, die auch einen Software-Neustart oder Batteriewechsel überleben, muss auf den nichtflüchtigen Flash-Speicher (NVS / Preferences.h) zurückgreifen.
  • Begrenzter Speicherplatz (Maximal 8 KB): Der gesamte RTC-Slow-Memory umfasst typischerweise nur 8 KB. Versuche also nicht, hochauflösende Kamerabilder eines ESP32-CAM-Moduls dort abzulegen. Für Statusflags, Zähler, Konfigurations-Strukturen oder kleine Arrays aus Gleitkommazahlen reicht der Platz jedoch mehr als dicke aus.
  • Die „Ich habe ULL vergessen“-Falle: Bei Zeitberechnungen in Mikrosekunden stößt ein 32-Bit-Integer schnell an seine Grenzen: 3600 * 1000000 (eine Stunde) sprengt den Wertebereich eines long (Überlauf bei 2.147 Sekunden, also knapp 36 Minuten). Schreibe bei der Multiplikation immer 1000000ULL (Unsigned Long Long), sonst schläft dein ESP32 bei längeren Intervallen nur wenige Sekunden oder wacht gar nicht mehr auf
  • Serial Monitor Zeichensalat nach dem Aufwachen: Nach jedem Tiefschlaf startet das interne ROM-Bootloader-Protokoll mit 74.880 Baud (bzw. 115.200 Baud je nach Quarz), bevor dein Sketch mit Serial.begin(115200) greift. Die ersten zwei bis drei wirren Zeichen im Terminal sind völlig normal und kein Zeichen für einen Code-Absturz.
  • Serial.flush() vor dem Schlafen nicht vergessen Die serielle Schnittstelle arbeitet asynchron mit einem Hardware-FIFO-Puffer. Rufst du Serial.println() auf und feuerst unmittelbar danach esp_deep_sleep_start(), wird der Chip mitten im Senden der Bytes schlafen gelegt. Die Konsolenausgabe bricht mitten im Wort ab. Ein Serial.flush() zwingt den ESP32, zu warten, bis das letzte Bit physisch über den TX-Pin gewandert ist.

2.8. Funktion 8: Chip-Temperatursensor

2.8.1. Was macht die Funktion?

Die Idee klingt im ersten Moment verlockend: „Warum soll ich für zwei Euro einen externen DHT22 oder DS18B20 kaufen, wenn im Datenblatt des ESP32 schwarz auf weiß steht, dass er bereits einen integrierten Temperatursensor besitzt?“

Also wird voller Euphorie eine Wetterstation programmiert, der interne Sensor ausgelesen, und das Display verkündet an einem lauen Frühlingsmorgen stolz: „Außentemperatur: 53,4 °C“.

Bevor man nun panisch den Klimanotstand für den eigenen Balkon ausruft, lohnt ein Blick auf die Physik: Dieser Sensor misst keineswegs die Umgebungstemperatur, sondern das Silizium-Fieber des ESP32 selbst.

Der integrierte Sensor misst die sogenannte Die-Temperatur (die Kerntemperatur des Halbleiterkristalls unter dem Blechdeckel). Und dieses Silizium arbeitet wie eine mikroskopisch kleine Herdplatte: Je höher die CPU-Taktfrequenz, je intensiver das WLAN-Modul funkt und je enger das staubdichte 3D-Druck-Gehäuse ist, desto wärmer wird der Chip.

Ist der Sensor damit nutzlos? Ganz im Gegenteil. Für Wetterstationen ist er ungeeignet, für die Selbstdiagnose, Systemstabilität und den Hardware-Schutz ist er jedoch nützlich:

  • Überhitzungsschutz in geschlossenen Gehäusen: Droht der Chip bei 85 °C zu schmelzen, kann die Firmware die Taktfrequenz drosseln oder einen Lüfter anwerfen.
  • Erkennung thermischer Ausreißer: Sitzt ein linearer Spannungsregler (LDO) zu nah am Chip und heizt die Platine auf, schlägt der Sensor Alarm.
  • Lasterkennung: Steigt die Temperatur plötzlich steil an, läuft im Hintergrund vermutlich eine fehlerhafte Endlosschleife auf voller Prozessorkapazität.

2.8.2. Wie funktioniert sie intern?

Physikalisch nutzt der ESP32 die Temperaturabhängigkeit von Halbleiterübergängen (Bandabstandsreferenz).

  1. Messprinzip & Schaltung: Im Silizium ist ein analoger Schaltkreis integriert, dessen Durchlassspannung sich proportional zur absoluten Temperatur (PTAT, Proportional to Absolute Temperature) verhält. Diese Spannung wird über einen internen 8-Bit-Sigma-Delta-Analog-Digital-Wandler (ADC) abgetastet.
  2. Offset-Trimmung per DAC: Da die Kennlinie des Sensors stark streut, schaltet Espressif einen internen Digital-Analog-Wandler (DAC) zur Bereichs- und Offset-Verschiebung dazwischen. Je nachdem, welchen Temperaturbereich man überwachen möchte, wählt die Hard- bzw. Software eine andere DAC-Stufe:
  3. Chip-Generationen & Treiber-Wandel: Hier herrscht im Netz oft Verwirrung:
    • Klassischer ESP32 (ESP32-D0WD / WROOM-32): Besaß in frühen Revisionen eine experimentelle Sensorimplementierung, die in aktuellen IDF-Versionen als deprecated gilt oder nur über inoffizielle Registeraufrufe erreichbar ist.
    • Moderne Serien (ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6): Hier hat Espressif einen komplett überarbeiteten, hardwarekalibrierten Sensor mit dedizierter Treiber-API (driver/temperature_sensor.h) verbaut.

2.8.3. Praxisnahe Codebeispiele

2.8.3.1. Beispiel A: Offizieller High-Level-Treiber (ESP-IDF v5/v6 & moderne Chips wie S2/S3/C3)

Im offiziellen ESP-IDF wird der Sensor über einen Treiber-Handle sauber installiert, auf einen sinnvollen Diagnosebereich (z. B. 20 °C bis 80 °C) konfiguriert und nach der Messung wieder schlafen gelegt:

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/temperature_sensor.h"

static const char *TAG = "THERMO_DIAG";

void app_main(void)
{
    ESP_LOGI(TAG, "Initialisiere On-Chip Temperatursensor...");

    // 1. Messbereich konfigurieren (20 °C bis 80 °C für normale Elektronik-Diagnose)
    // Die Hardware wählt automatisch die DAC-Stufe mit dem geringsten Fehler!
    temperature_sensor_config_t temp_cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
    temperature_sensor_handle_t temp_handle = NULL;

    // 2. Treiber installieren und Messschaltung aktivieren
    ESP_ERROR_CHECK(temperature_sensor_install(&temp_cfg, &temp_handle));
    ESP_ERROR_CHECK(temperature_sensor_enable(temp_handle));

    while (1) {
        float chip_celsius = 0.0f;

        // 3. Temperaturwert abholen
        ESP_ERROR_CHECK(temperature_sensor_get_celsius(temp_handle, &chip_celsius));
        ESP_LOGI(TAG, "Aktuelle Silizium-Kerntemperatur: %.2f °C", chip_celsius);

        // 4. Einfache Schutzlogik (Notabschaltung / Drosselung)
        if (chip_celsius > 75.0f) {
            ESP_LOGW(TAG, "WARNUNG: Chip wird zu heiß! Kuehlung aktivieren oder Last senken!");
        }

        vTaskDelay(pdMS_TO_TICKS(3000));
    }

    // Falls nicht mehr benötigt: Deaktivieren spart wertvolle Milliampere
    // ESP_ERROR_CHECK(temperature_sensor_disable(temp_handle));
    // ESP_ERROR_CHECK(temperature_sensor_uninstall(temp_handle));
}
2.8.3.2. Beispiel B: Schneller Check in der Arduino IDE

In der Arduino-Umgebung hängt der Aufruf vom verwendeten Chip ab:

#include <Arduino.h>

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("=========================================");
  Serial.println("  ESP32 On-Chip Fieberthermometer        ");
  Serial.println("=========================================");
}

void loop() {
  // Auf vielen modernen Kernen (ESP32-S2/S3/C3) liefert temperatureRead()
  // direkt die Chiptemperatur in Grad Celsius zurück:
  float chipTemp = temperatureRead();

  Serial.printf("Die-Temperatur: %.2f °C", chipTemp);

  if (chipTemp > 70.0) {
    Serial.println("  --> [STATUS: SEHR WARM - WLAN-Dauerlast?]");
  } else {
    Serial.println("  --> [STATUS: Normaler Betriebsbereich]");
  }

  delay(2000);
}

2.8.4. Hinweise und Fallstricke

  • Absoluter Messwert vs. Relative Tendenz: Erwarte keine Präzision auf Zehntelgrade. Durch Fertigungstoleranzen auf dem Wafer kann ein Chip ab Werk 48 °C anzeigen, während sein baugleicher Zwillingsbruder auf demselben Tisch 52 °C meldet. Nutze den Sensor primär zur Delta-Messung (Trendanalyse): Wenn die Temperatur im Leerlauf bei 45 °C lag und plötzlich innerhalb von 30 Sekunden auf 78 °C klettert, brennt software- oder kühltechnisch der Baum.
  • Die „Compiler-Fehler 128 / Not Found“-Falle: Kompilierst du das Arduino-Beispiel für einen klassischen ESP32 (z. B. NodeMCU-32S mit ESP32-WROOM-32), bricht der Compiler oft mit Fehlern wie temperatureRead was not declared oder wirft starr den Wert 128 bzw. 53.33 °C aus. Der Grund: Beim klassischen ESP32 wurde der Sensor von Espressif in neueren Core-Versionen als unzuverlässig eingestuft und aus der Standard-API gestrichen. Auf ESP32-S2, S3, C3 und C6 funktioniert er hingegen tadellos.
  • Stromverbrauch im Dauerbetrieb: Die analoge Sigma-Delta-Wandlerschaltung des Sensors zieht dauerhaft ein paar hundert Mikroampere. In extremen Low-Power-Anwendungen sollte der Sensor deshalb nicht dauerhaft im Hintergrund mitlaufen, sondern nach der Stichprobe per temperature_sensor_disable() abgeschaltet werden.
  • Einfluss auf die CPU-Taktung (Test-Tipp): Du willst sehen, wie der Sensor arbeitet? Takte die CPU testweise von 240 MHz auf 80 MHz oder 10 MHz herunter (siehe Kapitel 2.10). Du wirst im seriellen Monitor live beobachten können, wie die Chiptemperatur innerhalb von ein bis zwei Minuten um mehrere Grad Celsius absinkt.

2.9. Funktion 9: AM-Radio-Experimente

2.9.1. Was macht die Funktion?

Wer die Funkfähigkeiten des ESP32 aufzählt, nennt 2,4-GHz-WLAN (802.11 b/g/n) und Bluetooth (Classic & BLE). In den Analog- und Digitalschnittstellen schlummert aber noch etwas, das man auf einem IoT-Chip nicht vermutet: die Erzeugung von amplitudenmodulierten Mittel- und Langwellensignalen (AM-Radio).

Statt für ein paar Euro fertige Oszillator-Module oder externe Senderbausteine zu kaufen, zweckentfremdet man die schnellen internen Digital-Analog-Wandler (DAC) oder Sigma-Delta-Modulatoren des Chips. Der ESP32 erzeugt dabei eigenständig eine Trägerwelle im Bereich von einigen hundert Kilohertz (z. B. 835 kHz Mittelwelle oder 200 kHz Langwelle) und moduliert dieser in Software ein Audiosignal auf.

Um es mit den Worten eines bekannten Projekttitels auf Hackaday zu sagen: Man baut sich damit zwar die „schlechteste Radiostation der Welt“, aber als didaktisches Schaustück für Hochfrequenztechnik und Signaltheorie ist das Experiment lehrreich.

2.9.2. Wie funktioniert sie intern?

Ein Radiosender benötigt im Kern zwei Komponenten: eine hochfrequente Trägerwelle (Carrier) und ein Modulationsverfahren, das die Nutzdaten (Audio) auf diesen Träger aufprägt. Bei der Amplitudenmodulation (AM) wird schlicht die Stärke (Amplitude) der Trägerwelle proportional zur Momentanspannung des Tonsignals variiert.

In der Maker-Szene haben sich dafür zwei Ansätze etabliert:

  1. DAC-Direktsynthese via I2S-DMA (Klassischer ESP32):
    • Der Oszillator im RAM: Der Entwickler bitluni nutzte 2018 die Tatsache, dass der interne 8-Bit-DAC des klassischen ESP32 extrem schnell mit Daten gefüttert werden kann. Im Speicher liegt eine kleine Sinustabelle mit 16 Stützstellen.
    • I2S als Taktknecht: Normalerweise beliefert die I2S-Schnittstelle Audio-Codecs. Schaltet man I2S jedoch in den Modus I2S_MODE_DAC_BUILT_IN und treibt die Abtastrate auf rund 13,4 MHz, taktet der DMA-Controller diese 16 Sinuswerte kontinuierlich aus: 13,4 MHz / 16 ≈ 835 kHz, mitten im Mittelwellen-Rundfunkband
    • Die Modulation: Für jedes Audio-Sample skaliert die CPU die Amplitude der Sinustabelle mathematisch herunter oder herauf (Multiplikation von Träger und Signal). Der I2S-DMA-Kanal streamt das fertige AM-Signal vollkommen unterbrechungsfrei an den DAC-Ausgang (GPIO 25).
  2. Sigma-Delta-GPIO-Modulation (ESP32-S3 / Chips ohne DAC):
    • Neuere Chips wie der ESP32-S3 besitzen keinen klassischen Analog-DAC mehr. Moderne Projekte erzeugen die Trägerwelle direkt auf einem digitalen GPIO: stream2AM auf dem ESP32-S3 mit Sigma-Delta-Modulation (Träger 960 kHz), Gareth Davies‘ Langwellen-Sender (ebenfalls ESP32-S3) mit einer Pulskette auf einem GPIO (200 kHz). Das abgestrahlte Signal enthält Oberwellen, die ein AM-Empfänger als Tonsignal wiedergibt.

2.9.3. Praxisnahes Codebeispiel

Das folgende, an das Konzept von bitluni angelehnte Arduino-Beispiel zeigt die Erzeugung eines 1-kHz-AM-Testtons auf ca. 835 kHz über den internen DAC an GPIO 25:

#include <Arduino.h>
#include "driver/i2s.h"

// Abtastrate des Audiosignals (8 kHz)
#define AUDIO_SAMPLE_RATE 8000
#define AUDIO_SAMPLES_COUNT 8000

// 16 diskrete Sinuswerte für eine Periode der Trägerwelle
// Bei einem I2S-Takt von ~13,4 MHz ergibt 13,4 MHz / 16 ≈ 835 kHz Trägerfrequenz
const int8_t traegerSinus[16] = {
    0, 48, 90, 117, 127, 117, 90, 48,
    0, -48, -90, -117, -127, -117, -90, -48
};

// Puffer für 1 Sekunde Test-Audiosignal (1-kHz-Sinuston)
int8_t audioSignal[AUDIO_SAMPLES_COUNT];

void initialisiereI2SDAC() {
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate = 13000000, // Hohe Taktrate für die HF-Trägerwelle
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_MSB,
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = 1024,
    .use_apll = false
  };

  // I2S-Treiber installieren und festen DAC-Kanal an GPIO 25 aktivieren
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN); // Aktiviert DAC1 an GPIO 25
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("=========================================");
  Serial.println("  ESP32 AM-Radio Labor-Experiment        ");
  Serial.println("=========================================");
  Serial.println("Signal liegt an GPIO 25 an (Reichweite wenige cm).");

  // 1. Synthetisiere 1 Sekunde Test-Audio: 1 kHz Sinusschwingung
  for (int i = 0; i < AUDIO_SAMPLES_COUNT; i++) {
    audioSignal[i] = (int8_t)(100.0 * sin(2.0 * PI * 1000.0 * i / AUDIO_SAMPLE_RATE));
  }

  // 2. I2S für Hochfrequenz-DAC-Ausgabe scharfschalten
  initialisiereI2SDAC();
}

void loop() {
  // DMA-Sendepuffer für modulierte HF-Samples
  int16_t dmaPuffer[1024];
  static unsigned int audioIndex = 0;

  // Puffer mit amplitudenmodulierter HF befüllen
  for (int i = 0; i < 1024; i += 16) {
    if (audioIndex >= AUDIO_SAMPLES_COUNT) audioIndex = 0;

    // Audiosample auf positiven Bereich verschieben (Offset für Hüllkurve)
    int audioWert = audioSignal[audioIndex] + 128; // Wertebereich 28 bis 228

    // Amplitudenmodulation: Trägeramplitude mit Tonpegel multiplizieren
    for (int j = 0; j < 16; j++) {
      // 8-Bit-DAC-Wert in das 16-Bit-I2S-Frame einpassen
      int modulierterWert = ((traegerSinus[j] * audioWert) / 256) + 128;
      dmaPuffer[i + j] = (int16_t)(modulierterWert << 8);
    }
    audioIndex++;
  }

  // Block per DMA unterbrechungsfrei an den DAC austakten
  size_t geschriebeneBytes;
  i2s_write(I2S_NUM_0, dmaPuffer, sizeof(dmaPuffer), &geschriebeneBytes, portMAX_DELAY);
}

2.9.4. Hinweise und Fallstricke

  • Rechtliche Vorschriften & Gesetzgebung (Sehr wichtig): Das Betreiben von Funksendern in den Rundfunkbändern ist gesetzlich geregelt. Klär die Rechtslage vor dem Versuch, denn Sender und Oberwellen können fremde Funkdienste stören. Schließe unter keinen Umständen eine lange Antenne, einen Verstärker oder gar die Dachrinne an das Board an! Dieses Projekt ist ein reines Nahfeld-Experiment für den Labortisch. Die extrem geringe Leistung (wenige Mikrowatt) reicht bei einem kurzen Drahtstummel (5–10 cm) exakt so weit, dass ein Radio in 10 bis 30 Zentimetern Entfernung den Ton empfängt, und dort gehört sie auch hin.
  • Oberwellen und Störstrahlung: Da der interne 8-Bit-DAC kein bandbegrenztes Filtersystem besitzt und die 16 Sinus-Stufen Treppenspannungen erzeugen, ist das Ausgangssignal reich an unerwünschten Oberschwingungen (Harmonischen). Ein professioneller Sender benötigt Tiefpass- und Bandpassfilter. Ohne Filter strahlt der Pin breitbandigen Hochfrequenz-Müll ab, ein weiterer Grund, die Sendeleistung im Mikrobereich zu belassen.
  • Chip-Kompatibilität beachten:
    • Der Code mit I2S_MODE_DAC_BUILT_IN funktioniert ausschließlich auf dem klassischen ESP32 (GPIO 25 / GPIO 26), da nur dieser über echte integrierte Widerstandsnetzwerk-DACs verfügt. Außerdem gibt es die ADC- und DAC-Modi nur noch im alten I2S-Treiber (driver/i2s.h), den neuen I2S-Treiber von ESP-IDF 5.x kennt sie nicht (Stand: November 2026). Mit einem neueren Arduino-Core musst du den Code daher anpassen.
    • Auf dem ESP32-S2, ESP32-S3, C3 oder C6 fehlt dieser integrierte I2S-DAC-Modus entweder ganz oder der Chip besitzt gar keine DACs mehr. Dort muss auf moderne Sigma-Delta-Projekte wie stream2AM zurückgegriffen werden, die GPIO-Pins rein digital pulsen.

2.10. Funktion 10: Dynamische Taktfrequenz

2.10.1. Was macht die Funktion?

Stell dir vor, du kaufst dir ein Auto mit einem 500-PS-V8-Motor. Und jetzt stell dir vor, dieses Auto kennt nur genau zwei Gaspedal-Stellungen: Entweder Vollgas bei 7.000 Umdrehungen im Leerlauf vor der roten Ampel, oder der Motor ist komplett abgewürgt. Klingt nach einer grandiosen Fehlkonstruktion?

Genau so programmieren die meisten von uns standardmäßig ihren ESP32:

Im Auslieferungszustand rennen die beiden Xtensa-Kerne des ESP32 mit 240 MHz. Das ist nützlich, wenn wir rechenintensive Fast-Fourier-Transformationen (FFT) berechnen oder TLS-Verschlüsselungszertifikate aushandeln. Wenn unser Sketch aber 99 % seiner Zeit in einem delay(), beim Warten auf ein I2C-Sensor-Byte oder beim Abfragen eines simplen mechanischen Tasters verbringt, verheizen wir bei 240 MHz, wie der Dauerläufer aus der Einleitung, völlig grundlos zwischen 50 und 70 mA rein für den Prozessortakt

Die Lösung lautet Dynamische Taktfrequenzanpassung (Dynamic Frequency Scaling, DFS). Wir zwingen den Prozessor nicht mehr in ein starres Dauervollgas, sondern passen seinen Herzschlag dynamisch an die aktuelle Last an:

  • Beim gemütlichen Warten auf Benutzereingaben schalten wir den Takt auf 80 MHz, 40 MHz oder gar 10 MHz herunter (der Stromverbrauch der reinen CPU stürzt auf wenige Milliampere ab).
  • Steht plötzlich eine schwere mathematische Aufgabe oder Bildverarbeitung an, schalten wir für ein paar Millisekunden den Turbo auf 240 MHz ein und drosseln danach sofort wieder.

2.10.2. Wie funktioniert sie intern?

Hinter der Taktumschaltung steckt eine Kaskade aus Taktquellen (Oszillatoren), Phasenregelschleifen (PLL) und Frequenzteilern:

  1. Die Taktquellen & Teiler: Auf dem Entwicklungsboard sitzt üblicherweise ein 40-MHz-Quarz (XTAL).
    • Für die hohen Geschwindigkeiten schaltet der Chip die interne Phasenregelschleife (BBPLL) ein, die den Quarztakt auf 480 MHz hochmultipliziert. Ein Teiler durch 2 liefert die bekannten 240 MHz, ein Teiler durch 3 ergibt 160 MHz. Dieser PLL-Schaltkreis ist allerdings selbst ein kleiner Stromfresser.
    • Schalten wir auf 40 MHz oder tiefer, kann der Chip die PLL komplett deaktivieren und den Prozessorkern direkt aus dem Quarz bzw. sparsamen internen RC-Oszillatoren speisen.
  2. Der APB-Bus (Das Peripherie-Nadelöhr): Die CPU arbeitet nicht isoliert. Schnittstellen wie UART, SPI, I2C und Timer hängen am internen APB-Bus (Advanced Peripheral Bus).
    • Läuft die CPU mit 240, 160 oder 80 MHz, bleibt der APB-Bus stabil auf 80 MHz. Alle Baudraten und Timer stimmen auf die Mikrosekunde.
    • Wird der Takt jedoch unter 80 MHz gedrosselt (z. B. auf 40, 20 oder 10 MHz), sinkt der APB-Takt zwangsläufig mit, und genau hier lauern die berüchtigten Fallstricke für serielle Schnittstellen.
  3. Automatisches DFS mit Power-Management-Locks (ESP-IDF): Im professionellen ESP-IDF muss man den Takt nicht mit der Brechstange manuell verstellen. Das integrierte Power-Management-Framework (esp_pm) regelt das vollautomatisch: Treiber (z. B. für WLAN oder SPI) fordern bei Bedarf einen sogenannten Power Management Lock an (esp_pm_lock_acquire()). Solange der Lock aktiv ist, hält der Kernel den Takt auf 80 oder 240 MHz. Gibt der Treiber den Lock frei, schaltet der Kernel den Chip im FreeRTOS-IDLE-Task automatisch auf die minimale Frequenz herunter.

2.10.3. Praxisnahe Codebeispiele

2.10.3.1. Beispiel A: Manuelle Taktumschaltung nach Bedarf (Arduino IDE)

Im Arduino-Core lässt sich die Frequenz mit dem simplen Einzeiler setCpuFrequencyMhz() während der Laufzeit dynamisch manipulieren:

#include <Arduino.h>

void zeigeAktuelleFrequenz() {
  Serial.printf("Aktueller CPU-Takt: %d MHz | APB-Bus: %d MHz\n",
                getCpuFrequencyMhz(),
                getApbFrequency() / 1000000);
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("=========================================");
  Serial.println("  ESP32 Dynamischer Taktwechsel-Test     ");
  Serial.println("=========================================");
}

void loop() {
  // 1. PHASE: Turbo-Modus für schwere Rechenarbeit
  setCpuFrequencyMhz(240);
  zeigeAktuelleFrequenz();
  Serial.println("--> Starte rechenintensive Aufgabe (240 MHz Vollgas)...");

  // Simulierte Rechenlast
  unsigned long start = millis();
  volatile double ergebnis = 0;
  for (long i = 0; i < 500000; i++) {
    ergebnis += sqrt(i) * sin(i);
  }
  Serial.printf("--> Fertig in %lu ms! (Ergebnis: %.2f)\n\n", millis() - start, ergebnis);

  // 2. PHASE: Eco-Modus für gemütliche Sensorabfragen / Warten
  // 80 MHz ist der 'Sweet Spot': PLL bleibt stabil, APB-Bus behält 80 MHz,
  // aber die CPU zieht deutlich weniger Strom als bei 240 MHz!
  setCpuFrequencyMhz(80);
  zeigeAktuelleFrequenz();
  Serial.println("--> Schalte runter auf 80 MHz Sparbetrieb...");
  delay(3000); // Gemütlich warten

  // 3. PHASE: Extremes Stromsparen (40 MHz - direkt vom Quarz, PLL AUS!)
  // Achtung: Weiter unten erklären wir, was dabei mit der Baudrate passiert.
  setCpuFrequencyMhz(40);
  // Bei 40 MHz Serial neu anpassen oder Baudrate beachten:
  Serial.begin(115200);
  Serial.println("--> Minimaltakt 40 MHz: Maximale CPU-Ersparnis!");
  delay(3000);

  Serial.println("\n--- Neuer Zyklus ---\n");
}
2.10.3.2. Beispiel B: Vollautomatisches Dynamic Frequency Scaling (ESP-IDF)

In professioneller ESP-IDF-Firmware überlässt man das Schalten dem Betriebssystem. Die CPU taktet automatisch zwischen 40 MHz (Leerlauf) und 240 MHz (unter Last):

#include <stdio.h>
#include "esp_pm.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "POWER_MGMT";

void init_dynamic_frequency_scaling(void)
{
    // Konfiguration für das automatische Power-Management
    esp_pm_config_t pm_config = {
        .max_freq_mhz = 240, // Bei Volllast hochtakten auf 240 MHz
        .min_freq_mhz = 40,  // Im Leerlauf drosseln auf 40 MHz (XTAL-Takt)
        .light_sleep_enable = true // Darf im IDLE sogar in den leichten Schlaf fallen
    };

    // Systemweites DFS scharfschalten
    ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
    ESP_LOGI(TAG, "DFS erfolgreich aktiviert: Dynamik zwischen 40 und 240 MHz!");
}

void app_main(void)
{
    init_dynamic_frequency_scaling();

    while (1) {
        // Im vTaskDelay() schläft der Task: DFS drosselt sofort auf 40 MHz!
        vTaskDelay(pdMS_TO_TICKS(5000));

        ESP_LOGI(TAG, "Kurzer Arbeits-Burst...");
        // Rechnet der Task hier intensiv, taktet der Kernel automatisch auf 240 MHz hoch!
    }
}

2.10.4. Hinweise und Fallstricke

  • Die „Hilfe, mein WLAN ist tot“-Falle (Mindesttakt 80 MHz): Das ist die eiserne Regel beim ESP32: Das interne WLAN- und Bluetooth-Basisband benötigt zwingend einen stabilen APB-Bustakt von mindestens 80 MHz Wer versucht, die CPU auf 40 MHz, 20 MHz oder 10 MHz herunterzutakten, während eine WLAN-Verbindung aktiv ist, erlebt ein sofortiges Desaster: Die Funkverbindung bricht augenblicklich ab, und der Treiber wirft Timeouts. Willst du unter 80 MHz gehen, muss WLAN vorher mit WiFi.disconnect(true) bzw. esp_wifi_stop() sauber beendet werden.
  • Serieller Zeichensalat bei Takten unter 80 MHz: Da der Takt der Standard-UART vom APB-Bus abgeleitet wird, ändert sich die Baudrate, wenn der APB-Takt bei Frequenzen unter 80 MHz einknickt. Schaltest du im laufenden Arduino-Sketch einfach auf setCpuFrequencyMhz(40), stimmen die internen Teiler der seriellen Schnittstelle oft nicht mehr, im Monitor erscheinen Hieroglyphen. Abhilfe: Nach dem Taktwechsel unter 80 MHz die serielle Schnittstelle kurz mit Serial.begin(115200) neu initialisieren.
  • Hardware-Timer und Delay-Genauigkeit: Einige hardwarenahe Timer (alte GPTimer oder PWM-Frequenzen) sind an den CPU- bzw. APB-Takt gekoppelt. Wenn du im laufenden Betrieb den Takt halbierst, kann ein unbedacht konfigurierter Hardware-Timer plötzlich doppelt so lange brauchen wie geplant. Die Standard-Arduino-Funktionen millis() und delay() sind im ESP32-Core jedoch gegen Taktwechsel kompensiert.
  • Der „Sweet Spot“ für Batterieprojekte (80 MHz): Für die allermeisten Standardprojekte (Sensoren auslesen, Displays ansteuern, Web-Requests senden) sind 80 MHz ein guter Kompromiss:
    • Der APB-Bus bleibt stabil bei 80 MHz.
    • WLAN und Bluetooth funktionieren einwandfrei ohne Zicken.
    • Sämtliche Schnittstellen (I2C, SPI, UART) behalten ihre gewohnten Timings.
    • Der Strombedarf der CPU sinkt gegenüber 240 MHz laut Datenblatt grob um ein Drittel bis die Hälfte (Modem-Sleep, Dual-Core: 20 bis 31 mA bei 80 MHz gegen 30 bis 68 mA bei 240 MHz).

3. Die Funktionen kombinieren

Jetzt setzen wir die Einzelteile zusammen: Zuerst bauen wir ein Batterieprojekt, in dem mehrere Funktionen ineinandergreifen (3.1), dann klären wir, was die Hardware der CPU abnehmen kann (3.2). Eine Checkliste (3.3) und ein Entscheidungsbaum (3.4) helfen dir beim nächsten Projekt.

3.1. Kombinieren: das Ultra-Low-Power-Orchester

Nachdem wir die zehn Funktionen einzeln seziert haben, wird es Zeit für die Frage: Wie baut man daraus ein echtes System?

Isoliert betrachtet ist jede Funktion ein nettes Gimmick. Richtig mächtig wird der ESP32 jedoch erst, wenn die Hardware-Blöcke wie Zahnräder ineinandergreifen. Nehmen wir den Albtraum jedes IoT-Entwicklers: Ein autarker Außensensor, der jahrelang mit einer einzigen Batterieladung laufen soll.

Wer hier einfach im loop() alle fünf Minuten das WLAN anwirft, delay(300000) tippt und hofft, dass der Akku durchhält, hat nach spätestens drei Tagen ein totes Stück Silizium im Garten liegen.

Verschalten wir stattdessen ULP-Co-Prozessor (1), Touch-Wakeup (3), RTC-Speicher (7) und DFS (10), entsteht eine neue Systemarchitektur:

Zwei Begriffe aus dem Diagramm: Die BSSID ist die MAC-Adresse des Access Points, mit dem sich der ESP32 verbindet. Kennt er Kanal und BSSID, kann er das Absuchen der Kanäle überspringen. ESP-NOW ist ein Protokoll von Espressif für Funkverbindungen zwischen ESP-Geräten ohne Router.

3.1.1. Warum die Kombination effizient ist

  1. Verhinderung sinnloser Boot-Zyklen: Die Haupt-CPU wacht nicht mehr stur nach Uhrzeit auf, um festzustellen, dass sich am Sensor nichts geändert hat. Der ULP filtert das Rauschen im Koma-Modus bei wenigen Mikroampere heraus.
  2. Minimierung der teuren HF-Laufzeit: Das Einschalten und Einbuchen des WLAN-Radios verbraucht mit Abstand die meiste Energie (bis zu 240 mA Spitzenstrom). Indem wir Messwerte im RTC-Speicher (RTC_DATA_ATTR) sammeln und nur jedes zehnte Mal senden, senken wir die mittlere Leistungsaufnahme drastisch.
  3. Reaktionsschnelligkeit ohne Polling: Trotz Tiefschlafs bleibt das Gerät über kapazitive Touch-Pins oder RTC-GPIOs jederzeit für den Menschen bedienbar, ohne dass dafür 240 MHz im Kreis rennen müssen.

3.2. Hardware statt Software: die CPU entlasten

Eines der wichtigsten didaktischen Prinzipien beim ESP32 lautet: Lass die Software nur das tun, wozu die Hardware nicht in der Lage ist. Viele schreiben seitenweise Code für Dinge, die das Silizium ohne CPU-Zyklen erledigt.

3.2.1. DMA: der CPU-Befreier

Wenn du ein Display mit 60 Bildern pro Sekunde aktualisierst, Farbverläufe berechnest oder Audio streamst, hat die CPU bei der eigentlichen Bus-Übertragung nichts verloren. Sie füllt nur einen Puffer im internen Speicher und übergibt ihn an den DMA-Kanal, der die Daten mit hohem Takt in die Peripherie taktet. Den Vergleich mit und ohne DMA zeigt die Grafik in Abschnitt 2.4.1.

3.2.2. GPIO-Matrix: Entlastung fürs Platinenlayout

Die GPIO-Matrix (Abschnitt 2.2) entflechtet außerdem die Leiterbahnen: Sie müssen nicht quer über das Board laufen, nur weil die Standard-Pins eines UART- oder I2C-Controllers an einer ungünstigen Gehäusekante liegen.

3.3. Checkliste: welche Funktion wofür

Diese Entscheidungsmatrix bewertet die zehn Funktionen nach Komplexität, Chip-Kompatibilität und Einsatzzweck:

FunktionNutzen / MehrwertKomplexitätHardware-Abhängigkeit (SoC)Typischer Maker-Einsatzzweck
1. ULP-Co-ProzessorAutonome Sensorüberwachung im Tiefschlaf⭐⭐⭐⭐ (Hoch)Klassischer ESP32 (FSM), S2/S3 (FSM oder RISC-V), C5/C6/P4 (LP-Core); nicht C3Autarke Frostwächter, Bodenfeuchte-Monitore, Alarmanlagen
2. GPIO-MatrixPins frei im Code umbiegen; perfektes PCB-Routing⭐ (Sehr gering)Fast alle ESP32-SoCs (für sehr hohe SPI-Takte die IO_MUX-Pins nutzen)Leiterplatten-Design, Entknoten von Breadboard-Leitungen
3. Kapazitive Touch-PinsUnsichtbare Tasten hinter Gehäusen, Wakeup-Trigger⭐ (Gering)Klassischer ESP32 (10 Pins), S2/S3 (bis zu 14 Kanäle)Wasserdichte Gehäuse, Vandalismus-sichere Schalter
4. DMARuckelfreie Displays und Audio ohne CPU-Belastung⭐⭐⭐ (Mittel)Alle Varianten (Puffer muss im internen SRAM/DMA-RAM liegen)TFT-Gamer, Audio-Player, LED-Matrix-Panels
5. Promiscuous ModeEmpfang aller 802.11-Raw-Frames (Sniffer)⭐⭐⭐ (Mittel)Alle Varianten mit 2,4-GHz-WLANBesucherzähler, Signal-Stärke-Maps, Netzwerk-Diagnose
6. Hall-SensorMagneterkennung ohne ein einziges Zusatzbauteil⭐ (Gering)Nur klassischer ESP32! (Auf S2, S3, C3, C6 gestrichen)Simple Deckelüberwachung, Sabotagekontakte
7. RTC-Speicher (RTC_DATA_ATTR)Variablen überleben Tiefschlaf ohne Flash-Verschleiß⭐ (Gering)Alle Varianten (typisch 8 KB Slow-SRAM verfügbar)Messwert-Pufferung, Boot-Zähler, Verbindungs-Caches
8. Chip-TemperatursensorChiptemperatur-Überwachung zum Überhitzungsschutz⭐ (Gering)S2, S3, C2, C3, C6 (beim klassischer ESP32 veraltet/entfernt)Diagnose in staubdichten Gehäusen, Lastüberwachung
9. AM-Radio-ExperimenteDirekte HF-Synthese zur drahtlosen Audioübertragung⭐⭐⭐⭐ (Hoch)klassischer ESP32 (via DAC) oder S3 (via Sigma-Delta GPIO)Lehrmittel für Funktheorie, Retro-Radio-Restaurierung
10. Dynamische Taktfrequenz (DFS)Takt automatisch oder manuell drosseln (10–240 MHz)⭐⭐ (Gering–Mittel)Alle Varianten (Achtung: WLAN verlangt mindestens 80 MHz)Tragbare Handhelds, Logger im Wachbetrieb, Eco-Modi

3.4. Leitfaden für das nächste Projekt

Der ESP32 muss nicht ständig im Kreis rennen. Wer ihn schlafen legt, wenn nichts zu tun ist, und nur weckt, wenn es sich lohnt, macht aus demselben Vier-Euro-Board ein anderes Gerät. Die Pausen sind übrigens die Stelle, an der der Chip am meisten leistet.

Schreib einen Kommentar

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