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_ATTRand 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 handlesIRAM_ATTRautomatically 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 oldRBDdimmeron 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
RBDdimmerlibrary on ESP32
Typical error messages:
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 of0x400Cxxxx(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:
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:
is supported)
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):
// 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.
// 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:
rbdimmerESP32is a separate library — don't confuse it with the oldRBDdimmer. 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:
// 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:
delayMicroseconds()inside the ISR blocks other interrupts.- 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.
// ⚠️ 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 beIRAM_ATTRor 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:
For ESP32 you need the new one.
For S2/C3/H2 — DimmerLink only.
IRAM (0x400Cxxxx)?
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 detected →
troubleshooting/zero-cross-detection-errors.md - LED flicker with dimmer →
load-types/led-flicker-triac-dimmer.md - Trailing vs Leading Edge →
load-types/trailing-vs-leading-edge.md
Still have questions?
Ask on forum.rbdimmer.com or open a GitHub Issue.