Dimming 12V LED (halogen replacement)

I'm trying to use an Arduino to dim a "dimmable" LED buib - a GU5.3 3W halogen replacement While advertised as dimmable, it does not react well to the PWM signal. There is some reaction, but only on the low end of the PWM. it doesn't take much to get to full brightness. My circuit is a standard low side MOSFET with PWM input.

Putting the original halogen lights back into the armature gives perfect dimming behaviour, but for power and heat reasons I want to use the LEDs. A piece of 12V LED strip also works beautifully.

As experiment I've ran a 100 Hz PWM signal to the lamp (closer to what it would see using 50 Hz AC and phase cutting), but no difference. 50 Hz PWM made it flicker badly, so that's not an option either.

I found this older forum thread which makes me think those lamps have different internal circuitry...

Any suggestions on what I could try next, short of buying stacks of dimmable LEDs hoping one works?

#include <Encoder.h>
    
    const uint32_t DEBOUNCE_INTERVAL = 50;
    const uint32_t OTHER_TIMEOUT = 5000;
    //const uint32_t CLICK_TIME = 700;
    
    const uint8_t LAMP_LINKS      = 3;
    const uint8_t LAMP_RECHTS     = 6;
    const uint8_t LAMP_ONDER      = 9;
    
    const uint8_t LINKS_A         = 10;
    const uint8_t LINKS_B         = 11;
    const uint8_t LINKS_S         = 12;
    const uint8_t RECHTS_A        = A1;
    const uint8_t RECHTS_B        = A2;
    const uint8_t RECHTS_S        = A3;
    
    //const uint8_t BRIGHTNESS_VALUES = 32;
    //const uint8_t logBrightness[BRIGHTNESS_VALUES] = {0, 1, 2, 4, 6, 9, 12, 16,
    //                                                  20, 24, 30, 35, 42, 48, 56, 63,
    //                                                  71, 80, 89, 99, 109, 120, 131, 143,
    //                                                  155, 168, 181, 195, 209, 224, 239, 255
    //                                                 };
    
    const uint8_t BRIGHTNESS_VALUES = 72;         // Encoder makes four steps at a time... This is 3/4 turn for minimum to maximum brightness, 18 steps.
    const uint8_t logBrightness[BRIGHTNESS_VALUES] = {0, 0, 0, 1, 1, 2, 2, 3,
                                                      4, 5, 6, 7, 8, 9, 11, 12,
                                                      14, 16, 18, 20, 21, 24, 26, 28,
                                                      30, 33, 35, 38, 41, 44, 47, 50,
                                                      54, 56, 60, 63, 67, 70, 75, 79,
                                                      82, 87, 91, 95, 99, 104, 108, 113,
                                                      117, 123, 128, 133, 139, 143, 149, 154,
                                                      160, 165, 171, 178, 183, 190, 195, 202,
                                                      207, 215, 220, 228, 233, 241, 249, 255
                                                     };
    
    
    //const uint8_t BRIGHTNESS_VALUES = 96;         // Encoder makes four steps at a time... This is 1 turn for minimum to maximum brightness, 24 steps.
    //const uint8_t logBrightness[BRIGHTNESS_VALUES] = {0, 0, 0, 0, 1, 1, 1, 2,
    //                                                  2, 3, 3, 4, 5, 5, 6, 7,
    //                                                  8, 9, 10, 11, 12, 13, 15, 16,
    //                                                  17, 19, 20, 21, 23, 24, 26, 28,
    //                                                  30, 32, 34, 35, 38, 40, 42, 44,
    //                                                  47, 48, 51, 54, 56, 58, 61, 63,
    //                                                  66, 69, 71, 75, 77, 80, 84, 86,
    //                                                  89, 93, 95, 99, 103, 105, 109, 113,
    //                                                  116, 120, 124, 127, 131, 136, 139, 143,
    //                                                  148, 151, 155, 160, 163, 168, 173, 176,
    //                                                  181, 186, 190, 195, 200, 204, 209, 215,
    //                                                  218, 224, 230, 233, 239, 245, 249, 255
    //                                                 };
    
    //const uint8_t BRIGHTNESS_VALUES = 144;         // Encoder makes four steps at a time... This is 1 1/2 turn for minimum to maximum brightness, 36 steps.
    //const uint8_t logBrightness[BRIGHTNESS_VALUES] = {0, 0, 0, 0, 1, 1, 1, 1,
    //                                                  2, 2, 2, 3, 3, 3, 4, 4,
    //                                                  5, 5, 6, 6, 7, 7, 8, 8,
    //                                                  9, 10, 11, 11, 12, 13, 13, 14,
    //                                                  15, 16, 17, 18, 19, 20, 20, 21,
    //                                                  23, 24, 24, 26, 27, 28, 29, 30,
    //                                                  32, 32, 34, 35, 37, 38, 39, 41,
    //                                                  42, 43, 45, 47, 48, 49, 51, 53,
    //                                                  54, 56, 57, 59, 60, 62, 64, 66,
    //                                                  67, 69, 71, 74, 75, 77, 79, 81,
    //                                                  82, 85, 87, 88, 91, 93, 95, 97,
    //                                                  99, 102, 104, 105, 108, 111, 113, 115,
    //                                                  117, 120, 121, 124, 127, 130, 131, 134,
    //                                                  137, 140, 142, 145, 148, 151, 152, 155,
    //                                                  158, 160, 163, 166, 170, 171, 175, 178,
    //                                                  181, 183, 186, 190, 193, 195, 199, 202,
    //                                                  206, 207, 211, 215, 217, 220, 224, 228,
    //                                                  230, 233, 237, 241, 243, 247, 251, 255
    //                                                 };
    
    Encoder knobLeft(LINKS_A, LINKS_B);
    Encoder knobRight(RECHTS_A, RECHTS_B);
    
    enum KnobState {
      SELF,
      OTHER,
      UNDER_SELF,
      UNDER_OTHER
    };
    
    void setup() {
      pinMode(LINKS_A, INPUT_PULLUP);
      pinMode(LINKS_B, INPUT_PULLUP);
      pinMode(LINKS_S, INPUT_PULLUP);
      pinMode(RECHTS_A, INPUT_PULLUP);
      pinMode(RECHTS_B, INPUT_PULLUP);
      pinMode(RECHTS_S, INPUT_PULLUP);
    
      pinMode(LAMP_LINKS, OUTPUT);
      pinMode(LAMP_RECHTS, OUTPUT);
      pinMode(LAMP_ONDER, OUTPUT);
    }
    
    void loop() {
      static KnobState leftKnobState;
      static KnobState rightKnobState;
    
      static uint8_t leftBrightness;
      static uint8_t rightBrightness;
      static uint8_t underBrightness;
    
      static int32_t positionLeft;
      static int32_t positionRight;
      static uint32_t leftActivity;
      static uint32_t rightActivity;
      static bool leftPressed;
      static bool rightPressed;
      static uint32_t lastLeftPressed;
      static uint32_t lastRightPressed;
    
      // Read the encoders.
      uint8_t newLeft = knobLeft.read();
      handleKnob(newLeft, positionLeft, leftKnobState, &leftBrightness, &rightBrightness, &underBrightness, &leftActivity);
      positionLeft = newLeft;
    
      uint8_t newRight = knobRight.read();
      handleKnob(newRight, positionRight, rightKnobState, &rightBrightness, &leftBrightness, &underBrightness, &rightActivity);
      positionRight = newRight;
    
      analogWrite(LAMP_LINKS, logBrightness[leftBrightness]);
      analogWrite(LAMP_RECHTS, logBrightness[rightBrightness]);
      analogWrite(LAMP_ONDER, logBrightness[underBrightness]);
    
      // Read the encoder switch.
      // When switch is closed: set to UNDER.
      // When switch is released within 700 ms: switch between SELF and OTHER.
      // If OTHER and no activity for 5000 ms: switch to SELF.
      handleSwitch(digitalRead(LINKS_S), &leftPressed, &lastLeftPressed, &leftKnobState, leftActivity);
      handleSwitch(digitalRead(RECHTS_S), &rightPressed, &lastRightPressed, &rightKnobState, rightActivity);
    }
    
    void handleKnob(const int32_t newPosition, const int32_t oldPosition, const KnobState knobState,
                    uint8_t* selfBrightness, uint8_t* otherBrightness, uint8_t* underBrightness,
                    uint32_t* activity) {
      if (newPosition != oldPosition) {
        *activity = millis();
        switch (knobState) {
          case SELF:
            if (newPosition > oldPosition) {
              if (*selfBrightness < BRIGHTNESS_VALUES - 1) {
                (*selfBrightness)++;
              }
            }
            else {
              if (*selfBrightness > 0) {
                (*selfBrightness)--;
              }
            }
            break;
    
          case OTHER:
            if (newPosition > oldPosition) {
              if (*otherBrightness < BRIGHTNESS_VALUES - 1) {
                (*otherBrightness)++;
              }
            }
            else {
              if (*otherBrightness > 0) {
                (*otherBrightness)--;
              }
            }
            break;
    
          case UNDER_SELF:
          case UNDER_OTHER:
            if (newPosition > oldPosition) {
              if (*underBrightness < BRIGHTNESS_VALUES - 1) {
                (*underBrightness)++;
              }
            }
            else {
              if (*underBrightness > 0) {
                (*underBrightness)--;
              }
            }
            break;
        }
      }
    }
    
    void handleSwitch(const bool newState, bool* lastState, uint32_t* lastPressed, KnobState* knobState, const uint32_t activity) {
      if (newState == LOW) {                                    // Encoder switch is pressed.
        if (*lastState == HIGH) {                               // But it was not yet pressed.
          *lastPressed = millis();                              // Record when it got pressed.
          *knobState = (*knobState == SELF) ? UNDER_SELF : UNDER_OTHER;   // Pressed: now we're managing the UNDER lights.
          *lastState = LOW;                                     // Remember the state we're in.
        }
      }
      else {                                                    // Switch is now not pressed; newState is HIGH.
        if (*lastState == LOW) {                                // Switch was just released.
          if (millis() - *lastPressed > DEBOUNCE_INTERVAL) {    // It's been longer than we expect the switch to bounce, so we may act on it.
            *lastState = HIGH;                                  // Remember the state we're in.
    
            if (millis() - *lastPressed < millis() - activity) {    // No activity since pressed: this is a click to change sides.
              *knobState = (*knobState == UNDER_SELF) ? OTHER : SELF;
            }
            else {                                              // UNDER lights have changed; revert to previous state.
              *knobState = (*knobState == UNDER_SELF) ? SELF : OTHER;
            }
    
            //        if (millis() - *lastPressed < CLICK_TIME) {         // Short press: this was a click to switch sides.
            //          *knobState = (*knobState == UNDER_SELF) ? OTHER : SELF;
            //        }
            //        else {                                              // Pressed long - this one was to change the UNDER lights. Revert to previous state.
            //          *knobState = (*knobState == UNDER_SELF) ? SELF : OTHER;
            //        }
          }
        }
      }
      if (newState == HIGH && *knobState != SELF) {             // If set to anything but SELF and the switch is not pressed,
        if (millis() - activity > OTHER_TIMEOUT) {              // after some time of no activity
          *knobState = SELF;                                    // revert to SELF.
        }
      }
    }

I also found this in a similar ad " Instant On, Not Dimmable, Needs recycling ".

Well at least in my case the box clearly advertises "dimmable".

TBH I haven't hooked it up through a dimmer and the original transformer to see how well that works.

Another set of these lights are behind a modern 12V switching power supply and that set reacts terribly to dimming - I blame the power supply in that case. I know an old-fashioned transformer behaves very differently when presented with phase cutting, I just don't know exactly how different.

Great Schematic, Thanks. Looking at your schematic it appears you are using the Analog inputs as outputs to pwm the lamps. If you are in fact doing this and doing the PWM in software you are getting what I would expect. I beleive what is causing the flickering is the background interrupts running on the Nano. When these happen the processor no longer runs your code but does its own think such as keeping mills etc up to date, this stalls your software PWM at whatever point it is at until finished then continues.

Most of the Arduinos have a PWM output, the Uno, Nano, Mini use pins 3, 5, 6, 9, 10, 11 at 490 Hz while pins 5 and 6 run at 980 Hz.

The "A" pins are analog input, but can also be configured as digital outputs A6 and A7 are analog only. Theys do not generate an analog output and PWM unless you generate it with software. It is best to use one of the PWM pins.

@gilshultz You totally misread the schematic, mixing up the encoder inputs (one of which is indeed connected to the A1, A2 and A3 pins) and the PWM outputs (D3 D6, D9) that go to the MOSFETs. As also stated in my original post, dimming on regular LEDs worked beautifully.

@Paul_B in response to your PM: you're in for a surprise. I managed to outdo the dimmer.
Phase cutting is not that far off PWM. Regular phase cutting gives 100 pulses per second vs. the 500 of an Arduino PWM. The main difference is the shape of the pulses, which is square for the Arduino and part of a sine for the phase cutting. Note that I ignore the polarity, this as the LED bulbs have a bridge rectifier and the input is not polarised either. One of my LED dimmers produces 100 Hz pulses, where each pulse is a high frequency AC block wave, roughly filling the expected sine. Obviously some kind of switching power supply. An interesting image on the scope.

Now back to the project.

A little more research: it turns out that those dimmable LEDs actually react quite poorly (compared to halogens) to the output of a phase cutting dimmer, both cleanly phase cutting and the not so clean wave from my other dimmable halogen controller. A rather high minimum brightness, and very quick to each full brightness.

The first is understandable: only when the waveform has a sufficient minimum peak voltage (triggering after the peak for low brightness) the LEDs can actually light up. The second not so much, this must be an oddity of the LED driver chip.

In the end I managed to get the dimmable GU4.5 LED work better from the Arduino than from the original supply. I found out that at a PWM value of about 80 (so about 25% duty cycle!) the light already reached full brightness. So basically I changed my lookup table to go from 0-80 and now I have very good dimming, with the lowest brightness lower than I can get with a phase cutting dimmer.

So in the end no changes to the circuit, and no significant changes to the code - I just changed the brightness lookup table, and increased the number of steps simply as I liked that better. A bit more movement for full brightness giving a bit more control.

You are correct, sorry about that it was a long few days.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.