Passa al contenuto

ESP32 + dimmer AC: Guru Meditation Error e IRAM_ATTR — cause e soluzione

Il dimmer AC ESP32 va in crash con il WiFi? La causa è IRAM_ATTR — l'ISR di passaggio per lo zero viene eseguita dalla flash, che è disabilitata durante l'attività WiFi. Risolto con rbdimmerESP32 (IRAM_ATTR automatico + isolamento dei core) o DimmerLink (nessun ISR).

TL;DR: If your ESP32 with an AC dimmer crashes ("Guru Meditation Error" or "Cache disabled but cached memory region accessed"), the cause is that the zero-cross ISR handler is not marked with IRAM_ATTR and runs from flash memory. When WiFi accesses flash, ESP32 temporarily disables it — the ISR can't execute and the core panics. Fix for ESP32: use the rbdimmerESP32 library — it handles IRAM_ATTR automatically and runs dimmer and WiFi/BLE tasks on separate cores. Note: the library only works on dual-core ESP32 (not the C/S/H series). The old RBDdimmer on ESP32 requires a manual patch.



Descrizione del problema

Hai collegato un dimmer AC a un ESP32 e il codice di esempio funziona bene — stabile senza WiFi. Ma appena abiliti il WiFi (WiFi.begin()) o avvii un trasferimento dati attivo (MQTT, HTTP, OTA), l'ESP32 si blocca o si riavvia con un core panic.

Schema tipico: uno stack trace appare nel monitor seriale, poi un riavvio. Cause scatenanti più comuni:

  • abilitazione del WiFi dopo l'avvio del dimmer
  • MQTT attivo (publish/subscribe frequenti)
  • aggiornamento OTA del firmware
  • utilizzo della vecchia libreria RBDdimmer su ESP32

Messaggi di errore tipici:

text
Guru Meditation Error: Core 0 panic'ed (LoadProhibited)
PC: 0x400xxxxx
Cache disabled but cached memory region accessed
Guru Meditation Error: Core 1 panic'ed (IllegalInstruction)

Sintomi tipici:

  • Il dimmer è stabile senza WiFi, va in crash in pochi secondi con il WiFi attivo
  • I crash avvengono esattamente durante l'attività WiFi (MQTT publish, richiesta HTTP, OTA)
  • L'indirizzo nello stack trace è vicino a 0x400Dxxxx (zona flash) invece di 0x400Cxxxx (zona IRAM)
  • Riavvii casuali sotto carico normale
  • I crash cessano quando attachInterrupt() per il passaggio per lo zero viene rimosso



Causa profonda

L'ESP32 memorizza il codice del programma nella memoria flash SPI esterna. In condizioni normali la flash viene letta tramite la cache della CPU — velocemente e in modo trasparente per il programmatore.

Tuttavia, alcune operazioni richiedono accesso esclusivo al bus flash:

  • Scrittura/cancellazione flash (NVS, SPIFFS, LittleFS)
  • Aggiornamento OTA
  • WiFi: alcune operazioni dello stack (caricamento certificati, NVS)
  • Altre operazioni interne ESP-IDF

Durante queste operazioni l'ESP32 disabilita temporaneamente la cache flash e blocca l'esecuzione di qualsiasi codice che risiede nella flash. Se un interrupt scatta (ad es., passaggio per lo zero) e il suo gestore (ISR) è anch'esso nella flash — il processore non può eseguire l'ISR e genera l'eccezione «Cache disabled but cached memory region accessed».

Soluzione ESP-IDF: il codice ISR deve trovarsi in IRAM (RAM interna, sempre accessibile). Metti l'attributo IRAM_ATTR prima della dichiarazione della funzione:

cpp
void IRAM_ATTR zeroCrossISR() {
    // questo gestore risiede in IRAM, non nella flash
    // viene eseguito anche quando la flash è disabilitata
}

Inoltre, per un timing affidabile il task del dimmer dovrebbe essere eseguito sul Core 0 (xTaskCreatePinnedToCore(..., 0)). rbdimmerESP32 lo fa automaticamente.

Problema con la vecchia RBDdimmer: è stata scritta prima che i requisiti specifici dell'ESP32 fossero ampiamente conosciuti. I suoi gestori ISR mancano di IRAM_ATTR e vengono eseguiti dalla flash. Su ESP32 senza WiFi questo funziona — la flash non è bloccata. Con il WiFi abilitato, provoca crash periodici.



Soluzioni



🟢 Per principianti: DimmerLink — nessun ISR

Non vuoi occuparti di IRAM_ATTR, interrupt e particolarità dell'ESP32? DimmerLink gestisce il passaggio per lo zero e il controllo del TRIAC internamente — il tuo ESP32 invia semplicemente un comando di livello di luminosità. Funziona su qualsiasi modello di ESP32.

DimmerLink is a separate microcontroller that manages the AC dimmer via I2C or UART. In this setup the ESP32 doesn't handle zero-cross interrupts at all — it simply sends a level value. IRAM_ATTR crashes are impossible.

Quando scegliere questa soluzione:

  • ☐ Utilizzi ESP32-S2, ESP32-C3 o ESP32-H2 (single-core — nessuna libreria
  • ISR è supportata)

  • ☐ Hai bisogno di WiFi/MQTT/ESP-NOW e la stabilità conta più del controllo ISR
  • ☐ Vuoi usare Raspberry Pi o un altro SBC
  • ☐ Hai bisogno di controllo tramite LoRa-WAN, GSM/LTE, RS-232/485 via UART
  • ☐ Non vuoi occuparti degli interrupt
  • Cablaggio:

    • DimmerLink → ESP32: SDA → GPIO 21, SCL → GPIO 22, VCC → 3.3V, GND → GND
    • DimmerLink → dimmer AC: secondo lo schema di collegamento DimmerLink

    Codice (I2C, raccomandato):

    cpp
    // DimmerLink — controllo dimmer AC tramite I2C
    // L'ESP32 non usa interrupt — nessun conflitto con il WiFi
    // Documentazione: https://www.rbdimmer.com/docs/dimmerlink-I2CCommunication
    #include <Wire.h>
    #include <WiFi.h>
    #include <PubSubClient.h>  // esempio: MQTT + dimmer senza crash
    #define DIMMER_ADDR 0x50
    #define REG_LEVEL   0x10
    void setLevel(uint8_t level) {  // level: 0–100%
        Wire.beginTransmission(DIMMER_ADDR);
        Wire.write(REG_LEVEL);
        Wire.write(level);
        Wire.endTransmission();
    }
    void setup() {
        Wire.begin(21, 22);    // SDA, SCL
        WiFi.begin("ssid", "password");
        setLevel(50);           // 50% di luminosità immediatamente
    }
    void loop() {
        // MQTT, HTTP, OTA — tutto funziona senza conflitti con il dimmer
    }

    Risultato: Dimmer + WiFi + MQTT funzionano senza crash. Nessun IRAM_ATTR, nessun interrupt sull'ESP32.



    🔵 Per utenti avanzati: la libreria corretta per ESP32

    Vuoi usare gli ISR direttamente su ESP32 senza DimmerLink? Hai bisogno della libreria giusta.


    Opzione A: rbdimmerESP32 ✅ Raccomandato

    When to use: dual-core ESP32 (standard ESP32, not S2/C3/H2) Library: rbdimmerESP32

    rbdimmerESP32 è scritta specificamente per ESP32: posiziona l'ISR in IRAM automaticamente e assegna il task del dimmer al Core 0. Nessun IRAM_ATTR manuale necessario.

    cpp
    // Piattaforma: ESP32 dual-core
    // Libreria: rbdimmerESP32 — https://github.com/robotdyn-dimmer/rbdimmerESP32
    // IRAM_ATTR viene applicato automaticamente all'interno della libreria
    #include "rbdimmerESP32.h"
    #include <WiFi.h>
    #define ZC_PIN   18   // qualsiasi GPIO su ESP32
    #define DIM_PIN  19   // qualsiasi GPIO su ESP32
    rbdimmer dimmer;
    void setup() {
        Serial.begin(115200);
        dimmer.begin(ZC_PIN, DIM_PIN, 50);  // rete 50 Hz
        dimmer.setPower(50);                 // 50% di luminosità
        // Il WiFi può essere avviato dopo o prima dell'inizializzazione del dimmer —
        // l'ordine non è critico con rbdimmerESP32, ma è buona pratica
        WiFi.begin("ssid", "password");
        while (WiFi.status() != WL_CONNECTED) delay(500);
        Serial.println("WiFi connesso, dimmer stabile");
    }
    void loop() {
        // Sweep graduale della luminosità — nessun crash con WiFi attivo
        for (int p = 10; p <= 95; p++) {
            dimmer.setPower(p);
            delay(30);
        }
        for (int p = 95; p >= 10; p--) {
            dimmer.setPower(p);
            delay(30);
        }
    }

    Risultato atteso: Il dimmer funziona stabilmente con WiFi, MQTT, HTTP attivi — nessun crash. WiFi e dimmer funzionano in parallelo senza conflitti.

    Errori comuni:

    • Libreria non installata: rbdimmerESP32 è una libreria separata — non confonderla con la vecchia RBDdimmer. Installa da GitHub o dal gestore delle librerie Arduino.
    • Utilizzo su ESP32-S2/C3/H2: Non funzionerà. Le varianti single-core di ESP32 non supportano la dimmerizzazione ISR con nessuna libreria — usa DimmerLink.
    • Frequenza di rete errata: dimmer.begin(ZC_PIN, DIM_PIN, 50) — il terzo parametro è la frequenza di rete (50 o 60 Hz). Frequenza errata = temporizzazione di taglio di fase errata.


    Opzione B: patch manuale per la vecchia RBDdimmer

    Quando usare: stai già utilizzando la vecchia RBDdimmer e non vuoi migrare subito.

    Questa è una soluzione temporanea. Si raccomanda la migrazione a rbdimmerESP32.

    Trova il file sorgente RBDdimmer.cpp (o .h), individua la funzione gestore dell'interrupt di passaggio per lo zero e aggiungi IRAM_ATTR:

    cpp
    // In RBDdimmer.cpp — trova l'ISR e aggiungi IRAM_ATTR:
    // PRIMA (errato per ESP32):
    void zeroCrossISR() {
        // ...
    }
    // DOPO (corretto):
    void IRAM_ATTR zeroCrossISR() {
        // ...
    }

    Se l'ISR viene passato ad attachInterrupt() come lambda o callback, assicurati che anche il callback sia IRAM_ATTR. In RBDdimmer di solito c'è un ISR principale — trovalo cercando attachInterrupt o RISING.

    Importante: la patch viene persa all'aggiornamento della libreria. Fai un fork del repository o migra a rbdimmerESP32.



    Opzione C: implementazione personalizzata con IRAM_ATTR

    ⚠️ Solo a scopo educativo — non per la produzione. Due semplificazioni intenzionali nel codice seguente:

    1. delayMicroseconds() dentro l'ISR blocca gli altri interrupt.
    2. L'API Timer è scritta per ESP32 Arduino core 2.x — non compila su core 3.x (nuova API: timerBegin(1000000)).

    Per la produzione, usa rbdimmerESP32 (Opzione A).

    Quando usare: vuoi capire il principio prima di scrivere la tua implementazione.

    cpp
    // ⚠️ SOLO A SCOPO EDUCATIVO — leggi l'avviso sopra
    // Piattaforma: ESP32 dual-core, Arduino core 2.x
    // IRAM_ATTR è obbligatorio per TUTTE le funzioni chiamate da un ISR
    #define ZC_PIN  18
    #define DIM_PIN 19
    volatile int brightness = 50;
    volatile bool fired = false;
    hw_timer_t *timer = NULL;
    portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
    void IRAM_ATTR onTimer() {
        portENTER_CRITICAL_ISR(&timerMux);
        if (!fired) {
            // ⚠️ delayMicroseconds() nell'ISR — solo per dimostrazione
            // In produzione: usa un secondo timer per chiudere il gate del TRIAC
            digitalWrite(DIM_PIN, HIGH);
            delayMicroseconds(100);
            digitalWrite(DIM_PIN, LOW);
            fired = true;
        }
        portEXIT_CRITICAL_ISR(&timerMux);
    }
    void IRAM_ATTR zeroCrossISR() {
        portENTER_CRITICAL_ISR(&timerMux);
        fired = false;
        uint32_t delay_us = (100 - brightness) * 78;  // 0–7800 µs
        // ⚠️ API core 2.x — su core 3.x usa timerAlarm()
        timerAlarmWrite(timer, delay_us, false);
        timerAlarmEnable(timer);
        portEXIT_CRITICAL_ISR(&timerMux);
    }
    void setup() {
        pinMode(DIM_PIN, OUTPUT);
        // ⚠️ core 2.x: timerBegin(num, divider, countUp)
        // core 3.x: timer = timerBegin(1000000);
        timer = timerBegin(0, 80, true);  // clock 1 MHz
        timerAttachInterrupt(timer, &onTimer, true);
        attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, RISING);
    }
    void loop() {
        brightness = 50;
        delay(100);
    }


    ⚠️ Errori comuni

    • «Funziona senza WiFi, va in crash con WiFi»: Questo è esattamente il problema IRAM_ATTR. Non cercare la causa nel codice del dimmer — il problema è il posizionamento dell'ISR. Passa a rbdimmerESP32.

    • «Aggiunto IRAM_ATTR — ora un errore diverso»: Se l'ISR chiama funzioni senza IRAM_ATTR (ad es., Serial.print()), quelle chiamate causeranno il crash. Tutte le funzioni chiamate da un ISR devono anch'esse avere IRAM_ATTR o risiedere in IRAM.

    • «Funzionava, si è rotto dopo l'aggiornamento del SDK»: Dopo l'aggiornamento dell'Arduino core ESP32 o ESP-IDF, i requisiti di IRAM_ATTR sono diventati più stringenti. Passa a rbdimmerESP32 — segue la compatibilità con le nuove versioni del SDK.

    • «Provato ESP32-S2 — rbdimmerESP32 non funziona»: ESP32-S2, C3 e H2 sono single-core. La dimmerizzazione ISR è impossibile su di essi con qualsiasi libreria. L'unica soluzione è DimmerLink.

    • «Provato portDISABLE_INTERRUPTS() — ha aiutato temporaneamente»: Disabilitare gli interrupt compromette il WiFi e le altre funzioni di sistema. Non è una soluzione.




    Controllo rapido

    Prima di pubblicare sul forum, verifica:

  • ☐ Quale libreria stai usando — la vecchia `RBDdimmer` o la nuova
  • rbdimmerESP32? Per ESP32 serve la nuova.

  • ☐ Quale ESP32 — standard (dual-core) o S2/C3/H2?
  • Per S2/C3/H2 — solo DimmerLink.

  • ☐ Nello stack trace, l'indirizzo ISR è nella flash (`0x400Dxxxx`) o
  • in IRAM (0x400Cxxxx)?

  • ☐ Va in crash solo con WiFi attivo o sempre?
  • ☐ Ci sono chiamate a funzioni senza `IRAM_ATTR` nell'ISR (Serial.print,
  • ecc.)?



    Tabella di compatibilità

    Libreria / metodo Piattaforma IRAM_ATTR Funziona con WiFi?
    RBDdimmer (vecchia) ESP32 ❌ no ⚠️ crash
    RBDdimmer + patch manuale ESP32 ✅ manualmente ✅ sì
    rbdimmerESP32 ESP32 dual-core ✅ automatico ✅ sì
    rbdimmerESP32 ESP32-S2/C3/H2 ❌ non supportato
    DimmerLink Qualsiasi ESP32 — (nessun ISR) ✅ sì
    DimmerLink Raspberry Pi — (nessun ISR) ✅ sì
    RBDdimmer Arduino Uno/Mega — (non necessario) — (no WiFi)



    Problemi correlati

    • Passaggio per lo zero non rilevatotroubleshooting/zero-cross-detection-errors.md
    • Sfarfallio LED con dimmerload-types/led-flicker-triac-dimmer.md
    • Trailing vs Leading Edgeload-types/trailing-vs-leading-edge.md



    Hai ancora domande?

    Ask on forum.rbdimmer.com or open a GitHub Issue.

    Condividi articolo
    Accedi per lasciare un commento