Ir al contenido

AC TRIAC Dimmer Is Not PWM: How Phase-Cut Dimming Works

analogWrite or ledc on your AC dimmer DIM pin just gives full brightness — TRIAC doesn't respond to PWM. Phase-cut dimming requires a zero-cross signal and a proper library. Explained with ASCII diagrams and working code.

TL;DR: analogWrite() and PWM don't work with an AC TRIAC dimmer. A TRIAC is not a resistor or a DC transistor. It regulates power using phase-cut control: it opens at a precisely calculated moment each half-cycle, synchronized with the zero-cross signal. Without zero-cross and the correct library — the load will only be fully on or fully off, with no intermediate values.



Problem Description

You connected an AC dimmer, found the DIM pin — and tried to control it with analogWrite() like a regular PWM output. Or used ledc on ESP32. Result: the load is either at 100% or completely off. No intermediate brightness.

Sometimes the behavior is even more unexpected:

  • At low PWM values the load doesn't turn on at all
  • At high values — it jumps straight to full power
  • A value of 50% (128 out of 255) doesn't give 50% brightness

This is not a code bug or a faulty module. It's a fundamental mismatch between the control method and the load type.

Typical forum situations:

  • "I connected dimmer DIM pin to PWM, analogWrite(128) but lamp is fully on"
  • "Tried PWM on TRIAC dimmer but no dimming effect"
  • "ESP32 ledc on dimmer pin — light just turns on and off"
  • "Why doesn't analogWrite work with AC dimmer?"
  • ZC pin not connected at all — only DIM on a PWM output



Root Cause


PWM — for direct current (DC)

PWM (Pulse Width Modulation) controls power by rapid switching: a transistor opens and closes thousands of times per second, and the load receives an average voltage proportional to the duty cycle.

text
PWM 50% duty cycle (DC load — LED strip, motor, DC heater):
Voltage:
 ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐
─┘  └──┘  └──┘  └──┘  └──┘  └─
│←ON→│←OFF→│   ← frequency 1–20 kHz
Average power = 50% ✅

This works because a DC load responds to the average voltage. A heater warms at average power; an LED flickers fast enough that the eye doesn't notice.


TRIAC — for alternating current (AC): works differently

A TRIAC is a silicon-controlled rectifier for AC. It behaves fundamentally differently:

  1. Latches — once current starts flowing (and exceeds the holding current), the TRIAC stays open until the end of the half-cycle, even if the gate signal is removed.
  2. Self-turns off at the sine wave zero-crossing — when current drops below the holding current.
  3. Doesn't respond to rapid PWM: a single gate pulse of any length opens the TRIAC until end of half-cycle. 10 µs or 5 ms — the result is the same.
text
AC sine wave (50 Hz):
    ╭──────╮         ╭──────╮
    │      │         │      │
────╯      ╰────────╯      ╰────
    ↑      ↑         ↑      ↑
   ZC    ZC(-)      ZC    ZC(-)
   (100 pulses per second)
text
TRIAC opens at ZC — load gets the FULL half-cycle (100%):
    ╭──────╮         ╭──────╮
    │//////│         │//////│
────╯      ╰────────╯      ╰────
↑ opens                ↑ opens at ZC
text
TRIAC opens with 5 ms delay — load gets HALF the half-cycle (~50%):
    ──────╮         ──────╮
          │               │
    ──────╯────────  ─────╯──────
    ↑     ↑          ↑    ↑
   ZC   opens       ZC  opens
        (after 5 ms)     (after 5 ms)


Why PWM "can't see" the TRIAC

When you apply 50% PWM (e.g., 500 Hz) to the TRIAC's DIM pin:

  • PWM switches 500 times per second
  • The first HIGH pulse opens the TRIAC
  • Subsequent LOW pulses mean nothing — the TRIAC has already latched
  • It stays open until the end of the half-cycle (10 ms at 50 Hz)
  • The load ends up at full power

No zero-cross — no timing — no dimming.



Table: PWM vs Phase-Cut

Parameter PWM (DC) Phase-cut (AC TRIAC)
Signal type Continuous switching Single gate pulse
Needs ZC? No Yes — mandatory
Needs library? No (analogWrite) Yes
Load type DC (LED strip, motor, heater) AC (lamp, halogen, heater)
Control method Pulse duty cycle Gate delay relative to ZC
Switching frequency 1–20 kHz 100–120 Hz (once per half-cycle)
Working range 0–100% ~10–95% (TRIAC limitation)



Solutions



🟢 For beginners: DimmerLink — plug and play

Don't want to deal with zero-cross, phase angle, and ISR? DimmerLink handles all the logic internally — just set a level.

DimmerLink is a controller with its own zero-cross detector and TRIAC phase control. Your microcontroller only sends a brightness level (0–100%) via I2C or UART. No PWM, no zero-cross on the MCU side.

When to choose:

  • ☐ Just want to set brightness without learning phase-cut
  • ☐ Raspberry Pi (no real-time for ISR)
  • ☐ ESP32-S2/C3/H2 (single-core, ISR not supported)
  • ☐ Need simultaneous WiFi/MQTT control without crashes
  • Wiring:

    text
    DimmerLink → Arduino/ESP32
    VCC → 3.3V (ESP32) / 5V (Arduino)
    GND → GND
    SDA → SDA (GPIO 21 on ESP32, A4 on Uno)
    SCL → SCL (GPIO 22 on ESP32, A5 on Uno)

    Code:

    cpp
    // DimmerLink — set brightness as a simple 0–100 value
    // No PWM, zero-cross, or ISR — all handled inside DimmerLink
    // 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();
        setLevel(0);    // off
        delay(2000);
        setLevel(50);   // 50% brightness
        delay(2000);
        setLevel(100);  // full brightness
    }
    void loop() {}


    🔵 For advanced users: proper phase-cut control

    Want to work with the dimmer directly without DimmerLink? You need to connect zero-cross and use a library.

    What you need to connect:

    Dimmer pin Connect to
    VCC 5V (Arduino) / 3.3V (ESP32)
    GND GND
    ZC Interrupt-capable pin (2 or 3 on Arduino Uno; any GPIO on ESP32)
    DIM Any digital output

    Key difference from PWM: the DIM pin receives a single short pulse (~100 µs) at the right moment, calculated relative to ZC. No continuous PWM signal.


    Option A: ESP32 with rbdimmerESP32 ✅ Recommended

    cpp
    // Platform: dual-core ESP32
    // Library: rbdimmerESP32 — phase-cut, not PWM
    // https://github.com/robotdyn-dimmer/rbdimmerESP32
    #include "rbdimmerESP32.h"
    #define ZC_PIN  18  // zero-cross — REQUIRED
    #define DIM_PIN 19  // TRIAC control
    rbdimmer dimmer;
    void setup() {
        dimmer.begin(ZC_PIN, DIM_PIN, 50);  // 50 Hz mains
        dimmer.setPower(50);                 // 50% — works via phase-cut
    }
    void loop() {
        // Smooth brightness sweep
        for (int p = 10; p <= 95; p++) {
            dimmer.setPower(p);
            delay(30);
        }
        for (int p = 95; p >= 10; p--) {
            dimmer.setPower(p);
            delay(30);
        }
    }

    What happens inside the library:

    text
    1. ZC interrupt fires (t=0)
       ↓
    2. Calculate gate delay (leading edge, 50 Hz):
       delay_us = (100 - power%) × 78
       Example: 50% → delay = 50 × 78 = 3 900 µs
       ↓
    3. Hardware timer: fires after 3 900 µs
       ↓
    4. Send short pulse to DIM (~100 µs)
       ↓
    5. TRIAC opens and stays open until next ZC
    ⚠️ Working range — not 0–100%, but ~10–95%:
       100% → delay = 0 µs (opens at ZC, maximum power) ✅
         0% → delay = 7 800 µs (78% of half-cycle) → load gets 22%,
               not 0%. To turn off — don't open the TRIAC at all.
       < 10% — TRIAC opens so late current can't exceed holding current
               → unstable flickering.
       Libraries therefore limit the range: setPower(0) = off,
       setPower(1–9) ≈ 10%, setPower(95–100) ≈ 95%.


    Option B: Arduino Uno/Mega with RBDdimmer

    cpp
    // Platform: Arduino Uno / Mega / Nano (AVR)
    // Library: RBDdimmer — https://github.com/robotdyn/dimmer
    // ZC IS REQUIRED! Arduino Uno: only pins 2 or 3
    #include <RBDdimmer.h>
    #define ZC_PIN   2   // zero-cross — pin 2 (interrupt-capable on Uno)
    #define DIM_PIN  11  // TRIAC control
    dimmerLamp dimmer(DIM_PIN, ZC_PIN);
    void setup() {
        dimmer.begin(NORMAL_MODE, ON);
        dimmer.setPower(50);  // 50%
    }
    void loop() {}


    ⚠️ Common pitfalls

    • "Connected DIM to PWM, didn't connect ZC at all": Without ZC, phase-cut is physically impossible. The ZC pin is a required part of the circuit, not optional.

    • "analogWrite(DIM_PIN, 128) — lamp doesn't flicker, just stays on": That's the expected behavior. analogWrite opens the TRIAC with the very first HIGH pulse and it stays latched. Use a library with ZC.

    • "Set PWM frequency to 100 Hz — nothing changed": Matching the PWM frequency to the ZC rate doesn't help. The TRIAC still latches on the first pulse.

    • "PWM worked with DC load — why not with AC?": DC loads (12V LED strips, DC motors) use transistor control and respond to average voltage. AC TRIAC is different physics. See the table above.

    • "Can I leave DIM on PWM just for on/off switching?": Technically yes — you can turn the load on/off with a single HIGH/LOW. But that's not dimming. And without ZC the TRIAC may fire at a random point in the sine wave, causing interference and stress on the TRIAC.




    Quick Check

  • ☐ ZC pin connected to an interrupt-capable GPIO? Without it, nothing works.
  • ☐ Are you using a zero-cross library (rbdimmerESP32 / RBDdimmer / DimmerLink)?
  • ☐ Did you remove `analogWrite()` / `ledc` from the DIM pin?
  • ☐ Arduino Uno: ZC on pin 2 or 3 (not 4, 5, or others)?
  • ☐ Single-core ESP32 (S2/C3/H2)? → Use DimmerLink.


  • Related Issues

    • Zero-cross not detectedtroubleshooting/zero-cross-detection-errors.md
    • ESP32 + TRIAC: Guru Meditation Errortroubleshooting/esp32-iram-attr.md
    • Trailing vs Leading Edgeload-types/trailing-vs-leading-edge.md



    Still have questions?

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

    Compartir esta publicación
    Iniciar sesión para dejar un comentario