Ir al contenido

ESP32 + dimmer AC: Guru Meditation Error e IRAM_ATTR — causas y solución

¿El dimmer AC ESP32 se cuelga con WiFi? La causa es IRAM_ATTR — la ISR de cruce por cero se ejecuta desde flash, que se desactiva durante la actividad WiFi. Se soluciona con rbdimmerESP32 (IRAM_ATTR automático + aislamiento de núcleos) o DimmerLink (sin 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 ESP32 doble núcleo 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.



Descripción del problema

Conectaste un dimmer AC a un ESP32 y el código de ejemplo funciona bien — estable sin WiFi. Pero en cuanto activas WiFi (WiFi.begin()) o inicias transferencia activa de datos (MQTT, HTTP, OTA), el ESP32 se congela o reinicia con un core panic.

Patrón típico: aparece un stack trace en el monitor serie, luego un reinicio. Desencadenantes más comunes:

  • activar WiFi después de iniciar el dimmer
  • MQTT activo (publish/subscribe frecuentes)
  • actualización OTA de firmware
  • uso de la biblioteca antigua RBDdimmer en ESP32

Mensajes de error típicos:

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)

Síntomas típicos:

  • El dimmer es estable sin WiFi, se cuelga en segundos con WiFi activo
  • Los fallos ocurren exactamente durante actividad WiFi (MQTT publish, solicitud HTTP, OTA)
  • La dirección del stack trace está cerca de 0x400Dxxxx (zona flash) en vez de 0x400Cxxxx (zona IRAM)
  • Reinicios aleatorios bajo carga normal
  • Los fallos cesan al eliminar attachInterrupt() para el cruce por cero



Causa raíz

El ESP32 almacena el código del programa en memoria flash SPI externa. En operación normal, la flash se lee a través del caché de la CPU — rápido y transparente para el programador.

Sin embargo, algunas operaciones requieren acceso exclusivo al bus flash:

  • Escritura/borrado de flash (NVS, SPIFFS, LittleFS)
  • Actualización OTA
  • WiFi: ciertas operaciones del stack (carga de certificados, NVS)
  • Otras operaciones internas de ESP-IDF

Durante estas operaciones, el ESP32 desactiva temporalmente el caché flash y bloquea la ejecución de cualquier código que resida en flash. Si una interrupción se dispara (por ej., cruce por cero) y su manejador (ISR) también está en flash — el procesador no puede ejecutar la ISR y lanza una excepción «Cache disabled but cached memory region accessed».

Solución ESP-IDF: el código ISR debe estar en IRAM (RAM interna, siempre accesible). Coloque el atributo IRAM_ATTR antes de la declaración de la función:

cpp
void IRAM_ATTR zeroCrossISR() {
    // este manejador reside en IRAM, no en flash
    // se ejecuta incluso cuando flash está desactivada
}

Adicionalmente, para un timing confiable, la tarea del dimmer debe ejecutarse en el Core 0 (xTaskCreatePinnedToCore(..., 0)). rbdimmerESP32 lo hace automáticamente.

Problema con la antigua RBDdimmer: fue escrita antes de que los requisitos específicos del ESP32 fueran ampliamente conocidos. Sus manejadores ISR carecen de IRAM_ATTR y se ejecutan desde flash. En ESP32 sin WiFi esto funciona — la flash no se bloquea. Con WiFi activado, provoca fallos periódicos.



Soluciones



🟢 Para principiantes: DimmerLink — sin ISR

¿No quieres lidiar con IRAM_ATTR, interrupciones y particularidades del ESP32? DimmerLink maneja el cruce por cero y el control del TRIAC internamente — tu ESP32 solo envía un comando de nivel de brillo. Funciona en cualquier modelo de ESP32.

Cualquier ESP32 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.

Cuándo elegir esta solución:

  • ☐ Usas ESP32-S2, ESP32-C3 o ESP32-H2 (núcleo único — ninguna biblioteca
  • ISR es compatible)

  • ☐ Necesitas WiFi/MQTT/ESP-NOW y la estabilidad importa más que el control ISR
  • ☐ Quieres usar Raspberry Pi u otro SBC
  • ☐ Necesitas control por LoRa-WAN, GSM/LTE, RS-232/485 vía UART
  • ☐ No quieres lidiar con interrupciones
  • Cableado:

    • DimmerLink → ESP32: SDA → GPIO 21, SCL → GPIO 22, VCC → 3.3V, GND → GND
    • DimmerLink → dimmer AC: según el diagrama de conexión de DimmerLink

    Código (I2C, recomendado):

    cpp
    // DimmerLink — control de dimmer AC vía I2C
    // ESP32 no usa interrupciones — sin conflicto con WiFi
    // Documentación: https://www.rbdimmer.com/docs/dimmerlink-I2CCommunication
    #include <Wire.h>
    #include <WiFi.h>
    #include <PubSubClient.h>  // ejemplo: MQTT + dimmer sin fallos
    #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% de brillo inmediatamente
    }
    void loop() {
        // MQTT, HTTP, OTA — todo funciona sin conflictos con el dimmer
    }

    Resultado: Dimmer + WiFi + MQTT funcionan sin fallos. Sin IRAM_ATTR, sin interrupciones en el ESP32.



    🔵 Para usuarios avanzados: la biblioteca correcta para ESP32

    ¿Quieres usar ISR directamente en ESP32 sin DimmerLink? Necesitas la biblioteca correcta.


    Opción A: rbdimmerESP32 ✅ Recomendado

    When to use: dual-core ESP32 (standard ESP32, not S2/C3/H2) Library: ESP32 doble núcleo

    rbdimmerESP32 está escrita específicamente para ESP32: ubica la ISR en IRAM automáticamente y fija la tarea del dimmer al Core 0. No se necesita IRAM_ATTR manual.

    cpp
    // Plataforma: ESP32 de doble núcleo
    // Biblioteca: rbdimmerESP32 — https://github.com/robotdyn-dimmer/rbdimmerESP32
    // IRAM_ATTR se aplica automáticamente dentro de la biblioteca
    #include "rbdimmerESP32.h"
    #include <WiFi.h>
    #define ZC_PIN   18   // cualquier GPIO en ESP32
    #define DIM_PIN  19   // cualquier GPIO en ESP32
    rbdimmer dimmer;
    void setup() {
        Serial.begin(115200);
        dimmer.begin(ZC_PIN, DIM_PIN, 50);  // red eléctrica 50 Hz
        dimmer.setPower(50);                 // 50% de brillo
        // WiFi puede iniciarse después o antes de la inicialización del dimmer —
        // el orden no es crítico con rbdimmerESP32, pero es buena práctica
        WiFi.begin("ssid", "password");
        while (WiFi.status() != WL_CONNECTED) delay(500);
        Serial.println("WiFi conectado, dimmer estable");
    }
    void loop() {
        // Barrido suave de brillo — sin fallos con WiFi activo
        for (int p = 10; p <= 95; p++) {
            dimmer.setPower(p);
            delay(30);
        }
        for (int p = 95; p >= 10; p--) {
            dimmer.setPower(p);
            delay(30);
        }
    }

    Resultado esperado: El dimmer funciona de forma estable con WiFi, MQTT, HTTP activos — sin fallos. WiFi y dimmer funcionan en paralelo sin conflictos.

    Errores comunes:

    • Biblioteca no instalada: rbdimmerESP32 es una biblioteca separada — no la confundas con la antigua RBDdimmer. Instala desde GitHub o el gestor de bibliotecas de Arduino.
    • Uso en ESP32-S2/C3/H2: No funcionará. Las variantes de núcleo único de ESP32 no soportan atenuación por ISR con ninguna biblioteca — usa DimmerLink.
    • Frecuencia de red incorrecta: dimmer.begin(ZC_PIN, DIM_PIN, 50) — el tercer parámetro es la frecuencia de red (50 o 60 Hz). Frecuencia incorrecta = temporización de corte de fase incorrecta.


    Opción B: parche manual para la antigua RBDdimmer

    Cuándo usar: ya estás usando la antigua RBDdimmer y no quieres migrar ahora mismo.

    Esta es una solución temporal. Se recomienda migrar a rbdimmerESP32.

    Busca el archivo fuente RBDdimmer.cpp (o .h), localiza la función manejadora de interrupción de cruce por cero y agrega IRAM_ATTR:

    cpp
    // En RBDdimmer.cpp — busca la ISR y agrega IRAM_ATTR:
    // ANTES (incorrecto para ESP32):
    void zeroCrossISR() {
        // ...
    }
    // DESPUÉS (correcto):
    void IRAM_ATTR zeroCrossISR() {
        // ...
    }

    Si la ISR se pasa a attachInterrupt() como lambda o callback, asegúrate de que el callback también tenga IRAM_ATTR. En RBDdimmer generalmente hay una ISR principal — encuéntrala buscando attachInterrupt o RISING.

    Importante: el parche se pierde al actualizar la biblioteca. Haz un fork del repositorio o migra a rbdimmerESP32.



    Opción C: implementación personalizada con IRAM_ATTR

    ⚠️ Solo con fines educativos — no para producción. Dos simplificaciones intencionales en el código siguiente:

    1. delayMicroseconds() dentro de la ISR bloquea otras interrupciones.
    2. La API de Timer está escrita para ESP32 Arduino core 2.x — no compila en core 3.x (nueva API: timerBegin(1000000)).

    Para producción, usa rbdimmerESP32 (Opción A).

    Cuándo usar: quieres entender el principio antes de escribir tu propia implementación.

    cpp
    // ⚠️ SOLO CON FINES EDUCATIVOS — lee la advertencia anterior
    // Plataforma: ESP32 de doble núcleo, Arduino core 2.x
    // IRAM_ATTR es obligatorio para TODAS las funciones llamadas desde una 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() en ISR — solo para demostración
            // En producción: usa un segundo timer para cerrar la compuerta 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 — en 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);  // reloj 1 MHz
        timerAttachInterrupt(timer, &onTimer, true);
        attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, RISING);
    }
    void loop() {
        brightness = 50;
        delay(100);
    }


    ⚠️ Errores comunes

    • «Funciona sin WiFi, se cuelga con WiFi»: Esto es exactamente el problema de IRAM_ATTR. No busques la causa en tu código del dimmer — el problema es la ubicación de la ISR. Cambia a rbdimmerESP32.

    • «Agregué IRAM_ATTR — ahora un error diferente»: Si la ISR llama funciones sin IRAM_ATTR (por ej., Serial.print()), esas llamadas provocarán el fallo. Todas las funciones llamadas desde una ISR también deben ser IRAM_ATTR o estar ubicadas en IRAM.

    • «Funcionaba, se rompió después de actualizar el SDK»: Después de actualizar el core Arduino ESP32 o ESP-IDF, los requisitos de IRAM_ATTR se volvieron más estrictos. Cambia a rbdimmerESP32 — sigue la compatibilidad con nuevas versiones del SDK.

    • «Probé ESP32-S2 — rbdimmerESP32 no funciona»: ESP32-S2, C3 y H2 son de núcleo único. La atenuación por ISR es imposible con cualquier biblioteca. La única solución es DimmerLink.

    • «Probé portDISABLE_INTERRUPTS() — ayudó temporalmente»: Desactivar interrupciones rompe WiFi y otras funciones del sistema. No es una solución.




    Verificación rápida

    Antes de publicar en el foro, verifica:

  • ☐ ¿Qué biblioteca usas — la antigua `RBDdimmer` o la nueva `rbdimmerESP32`?
  • Para ESP32 necesitas la nueva.

  • ☐ ¿Qué ESP32 — estándar (doble núcleo) o S2/C3/H2?
  • Para S2/C3/H2 — solo DimmerLink.

  • ☐ En el stack trace, ¿la dirección ISR está en flash (`0x400Dxxxx`) o
  • IRAM (0x400Cxxxx)?

  • ☐ ¿Se cuelga solo con WiFi activo o siempre?
  • ☐ ¿Hay llamadas a funciones sin `IRAM_ATTR` en la ISR (Serial.print, etc.)?
  • etc.)?



    Tabla de compatibilidad

    Plataforma Plataforma ¿Funciona con WiFi? RBDdimmer (antigua)
    ESP32 ❌ no ⚠️ fallos RBDdimmer + parche manual
    ESP32 ❌ no ✅ sí rbdimmerESP32
    ESP32 doble núcleo ✅ automático ✅ sí rbdimmerESP32
    ESP32 doble núcleo DimmerLink
    Cualquier ESP32 — (sin ISR) ✅ sí rbdimmerESP32
    Cualquier ESP32 — (sin ISR) ✅ sí rbdimmerESP32
    Arduino Uno/Mega — (no necesario) — (sin WiFi) — (no WiFi)



    Problemas relacionados

    • Cruce por cero no detectadotroubleshooting/zero-cross-detection-errors.md
    • Trailing vs Leading Edgeload-types/trailing-vs-leading-edge.md
    • Trailing vs Leading Edgeload-types/trailing-vs-leading-edge.md



    ¿Todavía tienes preguntas?

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

    Compartir esta publicación
    Iniciar sesión para dejar un comentario