TL;DR: An AC dimmer can't regulate the load if the microcontroller isn't receiving the zero-cross signal (the sine wave crossing zero). Without this signal it's impossible to calculate the TRIAC firing moment. Causes: wrong pin (not interrupt-capable), incorrect ZC wiring, noise on the signal line, or a software error (wrong interrupt mode). Work through the checklist below step by step.
Problem Description
You connected an AC dimmer, uploaded sample code — but the load doesn't respond or is always at full brightness. The wiring looks correct, no compile errors.
The most common cause in these cases is that zero-cross is not being detected by the microcontroller. Phase-cut dimming works on this principle: detect the sine wave zero-crossing → wait the calculated delay → fire the TRIAC. Without zero-cross there is no timing, and the dimmer either doesn't turn on the load at all or keeps it at 100% with no regulation.
Typical symptoms:
- Load is always at full brightness (TRIAC always open)
- Load doesn't turn on at all
setPower(50)has no effect — load is either fully on or off- Zero-cross counter doesn't increment in Serial monitor (if you add debug)
- Code compiles without errors but the dimmer "doesn't work"
Common forum messages:
- "Dimmer doesn't dim — lamp always full brightness"
- "setPower has no effect"
- "Zero cross interrupt not triggering"
- "Dimmer works sometimes, randomly"
Root Cause
The zero-cross signal is generated by a zero-cross detector circuit built into the AC dimmer or connected externally. Typically it's an optocoupler + resistor divider that produces a short pulse at each sine-wave zero crossing.
Pulse frequency: 100 Hz (50 Hz mains) / 120 Hz (60 Hz mains). One pulse per half-cycle.
To receive these pulses the microcontroller uses an external interrupt
(attachInterrupt()). If the interrupt is not configured correctly or the
signal doesn't reach the right pin — the ISR handler is never called, timing
can't be computed, and the dimmer doesn't work.
Root causes of failure:
- Wrong pin (not interrupt-capable) — most common error on Arduino
- Incorrect ZC wiring — signal doesn't reach the pin
- Wrong interrupt mode (
FALLINGinstead ofRISINGor vice versa — depends on the circuit) - Noise — multiple false triggers on one half-cycle
- ZC circuit power issue — no VCC on the zero-cross detector
Solutions
🟢 For beginners: DimmerLink — zero-cross built in
Don't want to deal with interrupt pins, RISING/FALLING, and electrical noise? DimmerLink has its own zero-cross detector and controls the TRIAC autonomously.
DimmerLink detects zero-cross in hardware and handles TRIAC firing timing internally. Your microcontroller only sets the brightness level — no ISR, no interrupts, no pin headaches.
When to choose this solution:
DimmerLink → Arduino/ESP32 wiring:
- VCC → 3.3V (ESP32) or 5V (Arduino)
- GND → GND
- SDA → SDA (GPIO 21 on ESP32, A4 on Arduino Uno)
- SCL → SCL (GPIO 22 on ESP32, A5 on Arduino Uno)
Code:
// DimmerLink — zero-cross detector and TRIAC control inside the module
// Microcontroller only sets the brightness level
// Docs: https://www.rbdimmer.com/docs/dimmerlink-I2CCommunication
#include <Wire.h>
#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(); // default SDA/SCL for your board
setLevel(50); // 50% brightness
}
void loop() {
// No ISR, no interrupts, no zero-cross on the MCU
}Result: Dimmer works without depending on interrupt pins or zero-cross signal on the microcontroller.
🔵 For advanced users: diagnose and fix the ZC
Want to keep the direct ISR connection? Follow the diagnostic steps.
Diagnosis: steps 1–5
Step 1: Verify the pin is interrupt-capable
This is cause #1 on Arduino.
// Arduino Uno / Nano / Mini:
// ✅ Interrupt-capable: ONLY pins 2 and 3
// ❌ All other pins (4, 5, 6...) — interrupts not supported
// Correct:
attachInterrupt(digitalPinToInterrupt(2), zeroCrossISR, RISING); // pin 2 ✅
attachInterrupt(digitalPinToInterrupt(3), zeroCrossISR, RISING); // pin 3 ✅
// Wrong:
attachInterrupt(4, zeroCrossISR, RISING); // ❌ pin 4 — not interrupt-capable
// On Uno, attachInterrupt(4) refers to INT4, which doesn't existOn ESP32 — any GPIO supports interrupts:
// ESP32: any GPIO — all work
attachInterrupt(digitalPinToInterrupt(18), zeroCrossISR, RISING); // ✅
attachInterrupt(digitalPinToInterrupt(34), zeroCrossISR, RISING); // ✅On ESP8266 — all GPIOs except GPIO 16 support interrupts; recommend 4, 5, 12, 13, 14 (no boot-mode dependencies).
Step 2: Add a counter to verify
The simplest test — count pulses over 1 second:
// Zero-cross diagnostic: expect ~100 pulses/sec (50 Hz mains)
// or ~120 pulses/sec (60 Hz mains)
#define ZC_PIN 2 // ← make sure this is an interrupt-capable pin
volatile uint32_t zcCount = 0;
#ifdef ESP32
void IRAM_ATTR zeroCrossISR() { // IRAM_ATTR required on ESP32
#else
void zeroCrossISR() { // Arduino/ESP8266: IRAM_ATTR not needed
#endif
zcCount++;
}
void setup() {
Serial.begin(115200);
pinMode(ZC_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, RISING);
}
void loop() {
delay(1000);
Serial.print("ZC impulses per second: ");
Serial.println(zcCount);
zcCount = 0;
// Expected results:
// ~100 — 50 Hz mains, all OK
// ~120 — 60 Hz mains, all OK
// 0 — ZC not detected (wiring or pin problem)
// >200 — noise, false triggers (RC filter problem)
}Step 3: Check the ZC wiring
Typical RBDimmer → Arduino/ESP32 wiring:
RBDimmer → Arduino/ESP32
------- -------------
VCC → 5V (Arduino) / 3.3V (ESP32)
GND → GND
ZC → Interrupt-capable pin (2 or 3 on Arduino Uno)
DIM → Any digital outputChecklist:
Step 4: Check the interrupt mode (RISING/FALLING/CHANGE)
The mode depends on the ZC detector circuit of your specific module:
// Most RBDimmer modules: RISING (pulse on rising edge)
attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, RISING);
// Some circuits with inverting optocoupler: FALLING
attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, FALLING);
// If unsure — try CHANGE (catches both edges):
// Note: with CHANGE the counter will read ~200 Hz instead of 100 Hz
attachInterrupt(digitalPinToInterrupt(ZC_PIN), zeroCrossISR, CHANGE);Use an oscilloscope or logic analyzer to check the ZC signal shape — identify whether you need to catch the rising or falling edge.
Step 5: Filter noise
If the counter shows >200 (on a 50 Hz grid) — false triggers due to noise.
This is especially common with long wires or proximity to the load:
// Software debounce for ZC: ignore pulses too close to the previous one
// Half-cycle period at 50 Hz = 10 000 µs → filter anything shorter than 8 000 µs
volatile uint32_t lastZC = 0;
void IRAM_ATTR zeroCrossISR() {
uint32_t now = micros();
if (now - lastZC > 8000) { // 8 ms minimum interval between ZC pulses
lastZC = now;
// your TRIAC timing logic here
}
}Hardware filter: an RC filter on the ZC line (1 kΩ resistor + 100 nF capacitor between ZC and GND) removes high-frequency noise.
Fix: choose your option
Option A: rbdimmerESP32 on ESP32 ✅
When: dual-core ESP32 with direct dimmer connection. The library handles ZC and timing automatically:
// Platform: dual-core ESP32
// Library: rbdimmerESP32
#include "rbdimmerESP32.h"
#define ZC_PIN 18 // any ESP32 GPIO
#define DIM_PIN 19 // any ESP32 GPIO
rbdimmer dimmer;
void setup() {
Serial.begin(115200);
dimmer.begin(ZC_PIN, DIM_PIN, 50); // 50 Hz mains
dimmer.setPower(50);
Serial.println("Dimmer initialized");
}
void loop() {}Common mistakes:
- ZC_PIN and DIM_PIN swapped in
dimmer.begin()— check which pin goes to the load and which to zero-cross. - Wrong mains frequency (third parameter) — 50 or 60 Hz.
Option B: Arduino Uno/Mega with RBDdimmer
// Platform: Arduino Uno / Mega / Nano (AVR)
// Library: RBDdimmer — https://github.com/robotdyn/dimmer
// WARNING: for ESP32 use rbdimmerESP32, not this library!
#include <RBDdimmer.h>
// ⚠️ Arduino Uno: ZC ONLY on pins 2 or 3
#define ZC_PIN 2 // interrupt-capable ✅
#define DIM_PIN 11 // any digital output
dimmerLamp dimmer(DIM_PIN, ZC_PIN);
void setup() {
Serial.begin(9600);
dimmer.begin(NORMAL_MODE, ON);
dimmer.setPower(50); // 50%
Serial.println("Dimmer ready");
}
void loop() {}⚠️ Common pitfalls
-
"Using pin 4 on Arduino Uno — not working": Pins 4, 5, 6, ... on the Uno don't support external interrupts. Only pins 2 and 3 do. This is the most common beginner mistake.
-
"
attachInterrupt(4, ...)compiles — should work": It compiles, but it won't work.attachInterrupt(4, ...)on the Uno refers to INT4 (hardware interrupt number), not GPIO 4. Always usedigitalPinToInterrupt(pin). -
"Added counter — always 0": Three possible causes: 1. Wrong pin (not interrupt-capable) — check above 2. No power on the ZC module 3. ZC pin not physically connected to the board
-
"Counter shows 300–400 instead of 100": False triggers due to noise. Add software debounce (see above) or a hardware RC filter.
-
"Pin 34 on ESP32 doesn't work for ZC": GPIO 34–39 on ESP32 are input-only — they support interrupts. But they have no internal pull-up/down. Add an external 10 kΩ resistor to 3.3V.
-
"Works without load, breaks with load connected": Electrical noise from the load (especially motors, transformers) can couple into the ZC line. Physically separate power and signal wires.
Quick Check
Before posting on the forum, verify:
Interrupt Pin Compatibility Table
| Board | Interrupt-capable pins | Note |
|---|---|---|
| Arduino Uno | 2, 3 | Only these two |
| Arduino Nano | 2, 3 | Only these two |
| Arduino Mega | 2, 3, 18, 19, 20, 21 | Six pins |
| ESP32 | All GPIO (0–39) | Except reserved |
| ESP8266 | All GPIO except GPIO 16 | Recommend 4,5,12,13,14 (no boot) |
| Raspberry Pi | All GPIO (via pigpio) | No real-time — prefer DimmerLink |
Related Issues
- ESP32 + AC dimmer: Guru Meditation Error →
troubleshooting/esp32-iram-attr.md - LED flicker with dimmer →
load-types/led-flicker-triac-dimmer.md - Dimmer doesn't regulate LED →
load-types/led-lamp-compatibility-triac.md
Still have questions?
Ask on forum.rbdimmer.com or open a GitHub Issue.