Se rendre au contenu

AC Dimmer Flickering at Low Brightness Levels

Irregular brightness fluctuation at 5–25% only — not an LED compatibility issue. This article covers zero-cross jitter, ISR delays on ESP32, and holding current misses, with platform-specific fixes.

TL;DR: Irregular flicker at 5–25% brightness (stable at 50%+) is caused by ZC signal jitter, ISR delays from WiFi on ESP32/ESP8266, or TRIAC holding-current misses. Fix: set a minimum stable level (10–15%), use rbdimmerESP32 on ESP32, filter the ZC line, or switch to DimmerLink for hardware-timed control.




This Article vs. the LED Flicker Article

Important: This article is about resistive loads — incandescent and halogen lamps, heating elements — flickering at low dimmer levels. The symptom is irregular, slow fluctuation (0.5–8 Hz) that disappears at medium and high brightness.

If you are using LED lamps and observe flicker at any brightness level, the problem is TRIAC–LED compatibility, not low-level timing. See: LED Flickering with AC Dimmer




What the Problem Looks Like

  • Lamp set to 10–20% brightness flickers visibly and irregularly.
  • The same lamp at 50% is rock-steady.
  • Replacing the controller with an Arduino stops the flicker, but an ESP32 or ESP8266 with WiFi does not.
  • Different lamp wattages give different minimum stable levels (40 W lamp may be stable at 18%, while 100 W is stable at 8%).



Root Causes


Cause 1 — Zero-Cross Jitter

Phase-cut dimming works by firing the TRIAC at a precise delay after each AC zero-crossing. At 10% brightness the delay is ~9 ms (for 50 Hz mains, half-period = 10 ms):

text
delay = (1 - level/100) × half_period
delay at 10% = (1 - 0.10) × 10 ms = 9.0 ms

A ±0.5 ms jitter on the ZC signal is only 5% error at 50% brightness, but 55% error in the firing angle at 10%. This produces large, visible brightness jumps.

Sources of ZC jitter:

  • mains voltage noise and EMI coupling onto the ZC line;
  • a long ZC cable running parallel to AC power wires;
  • inadequate decoupling on the zero-cross detector board.


Cause 2 — ISR Delays on ESP32 (WiFi)

The rbdimmerESP32 library pins its interrupt service routine to Core 0 with IRAM_ATTR. If you use the wrong library (RBDdimmer instead of rbdimmerESP32) or your sketch prevents proper ISR pinning, the WiFi stack can delay the DIM interrupt by 1–5 ms.

At 10% brightness (9 ms delay window), a 2 ms WiFi delay is a 22% relative timing error — enough to miss the firing window completely on some half-cycles.

On Arduino (AVR) the interrupt is hardware-handled in real time — no delay, hence no flicker from this cause.

See: ESP32 IRAM_ATTR Guru Meditation


Cause 3 — TRIAC Holding Current

At very short conduction angles (< 8–10% level) the current through the load may collapse below the TRIAC holding current before the device fully latches. Some half-cycles the TRIAC fires; others it does not. This produces slow, irregular flicker at 1–8 Hz — distinct from the smooth dimming at higher levels.

The minimum holding-current-limited level depends on load wattage:

Load Approx. min. stable level
25–40 W incandescent 20–25%
60 W incandescent 15%
100 W incandescent 8–10%
150 W incandescent 5–8%
500 W heating element 3–5%


Cause 4 — Minimum Power Limit Set Too Low

In ESPHome the ac_dimmer component has a min_power parameter. Setting it below the stable threshold for your load causes the component to try to fire at angles where the TRIAC cannot reliably latch.




Diagnosing the Problem

  1. Identify the load — test with a 100 W incandescent first. If stable at 10%, the original load has a holding-current issue at that wattage.
  2. Check the platform — swap the ESP32/ESP8266 for an Arduino Uno. If flicker disappears, ISR delays (Cause 2) or ZC jitter amplified by software processing (Cause 1) is the issue.
  3. Inspect the ZC wire routing — if the ZC cable runs next to AC power wires, reroute it away and retest.
  4. Measure the ZC pulse with an oscilloscope or logic analyser. Jitter > 200 µs at 50 Hz indicates a noisy ZC signal.



Solutions


🟢 For Beginners — Use DimmerLink

DimmerLink handles zero-cross detection and TRIAC firing on its own Cortex-M0+ microcontroller. Firing jitter is under 50 µs regardless of what the host MCU (ESP32, Raspberry Pi, Arduino) is doing. WiFi activity has zero impact on timing.

cpp
// ESP32 + DimmerLink — set 15% brightness via I2C
#include <Wire.h>
Wire.begin();
Wire.beginTransmission(0x50);
Wire.write(0x10);   // DIM0_LEVEL
Wire.write(15);     // 15% — stable at this level
Wire.endTransmission();

There is no minimum-level flicker with DimmerLink because the firmware enforces clean firing pulses with hardware timers.



🔵 For Advanced Users — Platform-Specific Fixes


Option A — Set a Minimum Stable Level

Find the lowest stable level experimentally for your specific load, then clamp the control range from below.

rbdimmerESP32 / RBDdimmer:

cpp
const uint8_t MIN_STABLE_LEVEL = 12;  // adjust for your load
void setDimmerLevel(uint8_t requestedLevel) {
    if (requestedLevel == 0) {
        dimmer.setState(OFF);
    } else {
        uint8_t level = max(requestedLevel, MIN_STABLE_LEVEL);
        dimmer.setState(ON);
        dimmer.setPower(level);
    }
}

ESPHome:

yaml
output:
  - platform: ac_dimmer
    id: triac_out
    gate_pin: GPIO4
    zero_cross_pin: GPIO5
    min_power: 0.12   # 12% — tune to your load
    zero_means_zero: true


Option B — Verify IRAM_ATTR on ESP32

Ensure you are using rbdimmerESP32, not RBDdimmer:

cpp
// Correct for ESP32 dual-core (ESP32, ESP32-S3):
#include <rbdimmerESP32.h>
rbdimmer::Dimmer dimmer(DIM_PIN, ZC_PIN, CHANNEL_1, 50);
// Wrong for ESP32 — no IRAM_ATTR, ISR can be preempted:
// #include <RBDdimmer.h>

Also confirm WiFi tasks are not pinning themselves to Core 0:

cpp
// Dedicated WiFi task should run on Core 1
xTaskCreatePinnedToCore(wifiTask, "WiFi", 4096,
                        NULL, 1, NULL, 1 /*core 1*/);

See: Wrong Library on ESP32


Option C — Filter the ZC Line

Add a simple RC filter to reduce high-frequency noise on the ZC signal:

text
ZC output pin ── 100 Ω ──┬── MCU ZC input pin
                         │
                        100 pF
                         │
                        GND

Additionally:

  • Route the ZC wire at least 2–3 cm away from AC power cables.
  • Cross AC and ZC wires at 90° if they must intersect.
  • Keep the ZC cable under 30 cm.


Option D — Switch to RMS Dimming Curve

The RMS curve concentrates control sensitivity in the 20–80% range, which avoids the unstable low-end region:

rbdimmerESP32:

cpp
// After begin():
dimmer.setCurve(RBDIMMER_CURVE_RMS);

DimmerLink (I2C register 0x11):

cpp
Wire.beginTransmission(0x50);
Wire.write(0x11);   // DIM0_CURVE
Wire.write(1);      // 1 = RMS
Wire.endTransmission();
Curve Code Best for
LINEAR 0 Universal (default)
RMS 1 Incandescent, halogen
LOG 2 Dimmable LED (eye perception)



Platform Matrix

Platform Likely cause Fix
Arduino Uno / Mega ZC jitter or holding current min_power + RC filter
ESP32 with WiFi ISR delay + ZC jitter rbdimmerESP32 + IRAM_ATTR
ESP8266 with WiFi ISR delay Min level 15%+, or DimmerLink
Any platform, load < 40 W Holding current Limit min level to load's threshold
Any platform, guaranteed Hardware solution DimmerLink



Quick Checklist

  • ☐ Load is resistive (incandescent, halogen, heater) — not an LED
  • ☐ On ESP32: using `rbdimmerESP32`, not `RBDdimmer`
  • ☐ Minimum stable level is set above the holding-current threshold
  • ☐ ZC cable does not run parallel to AC power wires
  • ☐ `min_power` in ESPHome is at least 0.10 (10%)
  • ☐ If DimmerLink: try setting the RMS curve (register 0x11 = 1)



  • Related Articles



    Still have questions?

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

    Partager cet article
    Se connecter pour laisser un commentaire.