Skip to Content

ESP32 + AC Dimmer: Guru Meditation Error and IRAM_ATTR — Causes and Fix

ESP32 AC dimmer crashes with WiFi? The root cause is IRAM_ATTR — the zero-cross ISR runs from flash, which is disabled during WiFi activity. Fixed by rbdimmerESP32 (automatic IRAM_ATTR + core isolation) or DimmerLink (no ISR at all).

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.



Problem Description

You connected an AC dimmer to an ESP32 and the sample code runs fine — stable without WiFi. But as soon as you enable WiFi (WiFi.begin()) or start active data transfer (MQTT, HTTP, OTA), the ESP32 freezes or reboots with a core panic.

Typical pattern: a stack trace appears in the Serial monitor, then a reboot. Most common triggers:

  • enabling WiFi after starting the dimmer
  • active MQTT (frequent publish/subscribe)
  • OTA firmware update
  • using the old RBDdimmer library on ESP32

Typical error messages:

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)

Typical symptoms:

  • Dimmer is stable without WiFi, crashes within seconds with WiFi active
  • Crashes exactly during WiFi activity (MQTT publish, HTTP request, OTA)
  • Stack trace address near 0x400Dxxxx (flash region) instead of 0x400Cxxxx (IRAM region)
  • Random reboots under normal load
  • Crashes stop when attachInterrupt() for zero-cross is removed



Root Cause

ESP32 stores program code in external SPI flash memory. During normal operation flash is read through the CPU cache — fast and transparent to the programmer.

However, some operations require exclusive access to the flash bus:

  • Flash write/erase (NVS, SPIFFS, LittleFS)
  • OTA update
  • WiFi: certain stack operations (loading certificates, NVS)
  • Other internal ESP-IDF operations

During these operations ESP32 temporarily disables the flash cache and blocks execution of any code that resides in flash. If an interrupt fires (e.g., zero-cross) and its handler (ISR) is also in flash — the processor cannot execute the ISR and throws a "Cache disabled but cached memory region accessed" exception.

ESP-IDF solution: ISR code must be in IRAM (internal RAM, always accessible). Place the IRAM_ATTR attribute before the function declaration:

cpp
void IRAM_ATTR zeroCrossISR() {
    // this handler lives in IRAM, not in flash
    // executes even when flash is disabled
}

Additionally, for reliable timing the dimmer task should run on Core 0 (xTaskCreatePinnedToCore(..., 0)). rbdimmerESP32 does this automatically.

Problem with the old RBDdimmer: it was written before ESP32-specific requirements became widely known. Its ISR handlers lack IRAM_ATTR and run from flash. On ESP32 without WiFi this works — flash isn't blocked. With WiFi enabled, it leads to periodic crashes.



Solutions



🟢 For beginners: DimmerLink — no ISR at all

Don't want to deal with IRAM_ATTR, interrupts, and ESP32 quirks? DimmerLink handles zero-cross and TRIAC control internally — your ESP32 just sends a brightness level command. Works on any ESP32 model.

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.

When to choose this solution:

  • ☐ Using ESP32-S2, ESP32-C3, or ESP32-H2 (single-core — no ISR library
  • is supported)

  • ☐ Need WiFi/MQTT/ESP-NOW and stability matters more than ISR control
  • ☐ Want to use Raspberry Pi or another SBC
  • ☐ Need control over LoRa-WAN, GSM/LTE, RS-232/485 via UART
  • ☐ Don't want to deal with interrupts
  • Wiring:

    • DimmerLink → ESP32: SDA → GPIO 21, SCL → GPIO 22, VCC → 3.3V, GND → GND
    • DimmerLink → AC dimmer: per the DimmerLink connection diagram

    Code (I2C, recommended):

    cpp
    // DimmerLink — AC dimmer control via I2C
    // ESP32 uses no interrupts — no conflict with WiFi
    // Docs: https://www.rbdimmer.com/docs/dimmerlink-I2CCommunication
    #include <Wire.h>
    #include <WiFi.h>
    #include <PubSubClient.h>  // example: MQTT + dimmer without crashes
    #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% brightness immediately
    }
    void loop() {
        // MQTT, HTTP, OTA — all work without conflicts with the dimmer
    }

    Result: Dimmer + WiFi + MQTT work without crashes. No IRAM_ATTR, no interrupts on the ESP32.



    🔵 For advanced users: correct library for ESP32

    Want to use ISR directly on ESP32 without DimmerLink? You need the right library.


    Option A: rbdimmerESP32 ✅ Recommended

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

    rbdimmerESP32 is written specifically for ESP32: it automatically places the ISR in IRAM and pins the dimmer task to Core 0. No manual IRAM_ATTR needed.

    cpp
    // Platform: dual-core ESP32
    // Library: rbdimmerESP32 — https://github.com/robotdyn-dimmer/rbdimmerESP32
    // IRAM_ATTR is applied automatically inside the library
    #include "rbdimmerESP32.h"
    #include <WiFi.h>
    #define ZC_PIN   18   // any GPIO on ESP32
    #define DIM_PIN  19   // any GPIO on ESP32
    rbdimmer dimmer;
    void setup() {
        Serial.begin(115200);
        dimmer.begin(ZC_PIN, DIM_PIN, 50);  // 50 Hz mains
        dimmer.setPower(50);                 // 50% brightness
        // WiFi can be started after or before dimmer init — order is not critical
        // with rbdimmerESP32, but this is good practice
        WiFi.begin("ssid", "password");
        while (WiFi.status() != WL_CONNECTED) delay(500);
        Serial.println("WiFi connected, dimmer stable");
    }
    void loop() {
        // Smooth brightness sweep — no crashes with active WiFi
        for (int p = 10; p <= 95; p++) {
            dimmer.setPower(p);
            delay(30);
        }
        for (int p = 95; p >= 10; p--) {
            dimmer.setPower(p);
            delay(30);
        }
    }

    Expected result: Dimmer runs stably with active WiFi, MQTT, HTTP — no crashes. WiFi and dimmer work in parallel without conflicts.

    Common mistakes:

    • Library not installed: rbdimmerESP32 is a separate library — don't confuse it with the old RBDdimmer. Install from GitHub or Arduino Library Manager.
    • Using on ESP32-S2/C3/H2: Won't work. Single-core ESP32 variants do not support ISR dimming with any library — use DimmerLink.
    • Wrong mains frequency: dimmer.begin(ZC_PIN, DIM_PIN, 50) — the third parameter is the mains frequency (50 or 60 Hz). Wrong frequency = wrong phase-cut timing.


    Option B: manual patch for the old RBDdimmer

    When to use: you're already using the old RBDdimmer and don't want to migrate right now.

    This is a temporary fix. Migrating to rbdimmerESP32 is recommended.

    Find the RBDdimmer.cpp (or .h) source file, locate the zero-cross interrupt handler function, and add IRAM_ATTR:

    cpp
    // In RBDdimmer.cpp — find the ISR and add IRAM_ATTR:
    // BEFORE (wrong for ESP32):
    void zeroCrossISR() {
        // ...
    }
    // AFTER (correct):
    void IRAM_ATTR zeroCrossISR() {
        // ...
    }

    If the ISR is passed to attachInterrupt() as a lambda or callback, make sure the callback is IRAM_ATTR too. In RBDdimmer there is usually one main ISR — find it by grepping for attachInterrupt or RISING.

    Important: the patch is lost when the library is updated. Fork the repository or migrate to rbdimmerESP32.



    Option C: custom implementation with IRAM_ATTR

    ⚠️ For educational purposes only — not for production. Two intentional simplifications in the code below:

    1. delayMicroseconds() inside the ISR blocks other interrupts.
    2. The Timer API is written for ESP32 Arduino core 2.x — does not compile on core 3.x (new API: timerBegin(1000000)).

    For production, use rbdimmerESP32 (Option A).

    When to use: you want to understand the principle before writing your own implementation.

    cpp
    // ⚠️ FOR EDUCATIONAL PURPOSES ONLY — read the warning above
    // Platform: dual-core ESP32, Arduino core 2.x
    // IRAM_ATTR is required for ALL functions called from an 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() in ISR — for demo only
            // In production: use a second timer to close the TRIAC gate
            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
        // ⚠️ core 2.x API — on core 3.x use 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);  // 1 MHz tick
        timerAttachInterrupt(timer, &onTimer, true);
        attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, RISING);
    }
    void loop() {
        brightness = 50;
        delay(100);
    }


    ⚠️ Common pitfalls

    • "Works without WiFi, crashes with WiFi": This is exactly the IRAM_ATTR issue. Don't look for the cause in your dimmer code — the problem is ISR placement. Switch to rbdimmerESP32.

    • "Added IRAM_ATTR — now a different error": If the ISR calls functions without IRAM_ATTR (e.g., Serial.print()), those calls will crash it. All functions called from an ISR must also be IRAM_ATTR or located in IRAM.

    • "Was working, broke after SDK update": After updating the ESP32 Arduino core or ESP-IDF, IRAM_ATTR requirements became stricter. Switch to rbdimmerESP32 — it tracks compatibility with new SDK versions.

    • "Tried ESP32-S2 — rbdimmerESP32 doesn't work": ESP32-S2, C3, and H2 are single-core. ISR dimming is impossible on them with any library. The only solution is DimmerLink.

    • "Tried portDISABLE_INTERRUPTS() — helped temporarily": Disabling interrupts breaks WiFi and other system functions. This is not a solution.




    Quick Check

    Before posting on the forum, verify:

  • ☐ Which library are you using — old `RBDdimmer` or new `rbdimmerESP32`?
  • For ESP32 you need the new one.

  • ☐ Which ESP32 — standard (dual-core) or S2/C3/H2?
  • For S2/C3/H2 — DimmerLink only.

  • ☐ In the stack trace, is the ISR address in flash (`0x400Dxxxx`) or
  • IRAM (0x400Cxxxx)?

  • ☐ Does it crash only with active WiFi or always?
  • ☐ Are there calls to non-`IRAM_ATTR` functions in the ISR (Serial.print,
  • etc.)?



    Compatibility Table

    Library / method Platform IRAM_ATTR Works with WiFi?
    RBDdimmer (old) ESP32 ❌ no ⚠️ crashes
    RBDdimmer + manual patch ESP32 ✅ manually ✅ yes
    rbdimmerESP32 ESP32 dual-core ✅ automatic ✅ yes
    rbdimmerESP32 ESP32-S2/C3/H2 ❌ not supported
    DimmerLink Any ESP32 — (no ISR) ✅ yes
    DimmerLink Raspberry Pi — (no ISR) ✅ yes
    RBDdimmer Arduino Uno/Mega — (not needed) — (no WiFi)



    Related Issues

    • Zero-cross not detectedtroubleshooting/zero-cross-detection-errors.md
    • LED flicker with dimmerload-types/led-flicker-triac-dimmer.md
    • Trailing vs Leading Edgeload-types/trailing-vs-leading-edge.md



    Still have questions?

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

    Share this post
    Sign in to leave a comment