Перейти к содержимому

Zero-Cross Not Detected: AC Dimmer Doesn't Respond to Commands

AC dimmer stuck at full brightness or not responding? Zero-cross detection failure is the most common cause — wrong interrupt pin, no power on ZC circuit, or noise. 5-step diagnostic with test code and fix for Arduino and ESP32.

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:

  1. Wrong pin (not interrupt-capable) — most common error on Arduino
  2. Incorrect ZC wiring — signal doesn't reach the pin
  3. Wrong interrupt mode (FALLING instead of RISING or vice versa — depends on the circuit)
  4. Noise — multiple false triggers on one half-cycle
  5. 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:

  • ☐ Can't figure out interrupt pins on Arduino
  • ☐ Using Raspberry Pi (no real-time for interrupts)
  • ☐ Using ESP32-S2/C3/H2 (single-core, ISR doesn't work)
  • ☐ Want a reliable solution without debugging the ZC circuit
  • 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:

    cpp
    // 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.

    cpp
    // 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 exist

    On ESP32 — any GPIO supports interrupts:

    cpp
    // 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:

    cpp
    // 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:

    text
    RBDimmer  →  Arduino/ESP32
    -------      -------------
    VCC       →  5V (Arduino) / 3.3V (ESP32)
    GND       →  GND
    ZC        →  Interrupt-capable pin (2 or 3 on Arduino Uno)
    DIM       →  Any digital output

    Checklist:

  • ☐ ZC pin connected to the correct interrupt-capable GPIO
  • ☐ VCC supplied to the module (without power the ZC circuit won't work)
  • ☐ Common GND — both logic and power sections
  • ☐ ZC cable does not run parallel to 220V power wires (induces noise)
  • ☐ ZC connector not swapped with DIM (they're adjacent on some modules)

  • Step 4: Check the interrupt mode (RISING/FALLING/CHANGE)

    The mode depends on the ZC detector circuit of your specific module:

    cpp
    // 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:

    cpp
    // 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:

    cpp
    // 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
    cpp
    // 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 use digitalPinToInterrupt(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:

  • ☐ Arduino: ZC pin — only 2 or 3 (Uno/Nano). Others don't work.
  • ☐ Is VCC supplied to the dimmer's ZC circuit?
  • ☐ Is the ZC pin physically connected to the board (not just DIM)?
  • ☐ Zero-cross counter over 1 second: ~100 (50 Hz) or ~120 (60 Hz)?
  • ☐ Counter `>200`? Noise — add debounce or RC filter.
  • ☐ Interrupt mode: `RISING` or `FALLING` for your circuit?
  • ☐ ESP32: are you using `rbdimmerESP32`, not the old `RBDdimmer`?


  • 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 Errortroubleshooting/esp32-iram-attr.md
    • LED flicker with dimmerload-types/led-flicker-triac-dimmer.md
    • Dimmer doesn't regulate LEDload-types/led-lamp-compatibility-triac.md



    Still have questions?

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

    Поделиться этой записью
    Войти оставить комментарий