Fun with millis(): Dimm LEDs Up and Done by Button Press (Fading LEDs) with an Arduino

In this series I want to show short snippets of code around millis(). Here we will use millis() to control the dimming of a LED (fading). When you press (and hold) a button a LED should dimm up. When you releae the button the LED will remain it's current brightness. If you press the button again, the LED will dimm down.

You could use delay to slow down the fading, but as the series is called "Fun with millis()" we will write a non blocking code based on millis(). Furthermore this method enables you to dimm several LEDs in near parallel.

The Dimm Circuit

For this sketch we will use 3 push buttons and 3 LEDs connected to the Arduino.

dimm a LED

A Finite State Machine

We want to follow the LED a specific sequence of button events. Therefore we define a finite state machine with following states:

IDLE,      // wait for button press
DIMMUP,    // push and hold button results in fade in (stop at max)
HOLD,      // stay in setted brightness
DIMMDOWN   // press button second time results in fade out (stop at light off)

Lets draw a diagram:

dimmer finite state diagram

These states are a good example for an enumeration. You can use these state names in your sketch - the compiler will assign numbers for each element in the enumeration. Adding new states becomes very easy - and you don't need to remember if a particular state is assigned with number 2 or 3. To be able to have several enumerations in the sketch with the same or similar enumerators, we can give a name.

    enum State {
      IDLE,                          // wait for button press
      DIMMUP,                        // push and hold button results in fade in (stop at max)
      HOLD,                          // stay in setted brightness
      DIMMDOWN                       // press button second time results in fade out (stop at light off)
    };

As naming convention I let start the enumeration it with a capital letter. Then we need a variable to store the current state. It can be defined as

    State state;

The enumeration and the variable can be combined as

    enum State {
      IDLE,                          // wait for button press
      DIMMUP,                        // push and hold button results in fade in (stop at max)
      HOLD,                          // stay in setted brightness
      DIMMDOWN                       // press button second time results in fade out (stop at light off)
    } state;                         // define one variable

A Dimm Object

As we will need several variables for each button/LED and some member functions, we write a Dimmer class.

/*
  fade PWM pins

  https://forum.arduino.cc/t/1-button-1-led-fade/1113139/4
  2023-04-10 by noiasca
  to be deleted: 2023-06
  code in forum
*/

// a class for one button, one LED, one Relay
class Dimmer {
    const uint8_t buttonPin;         // the input GPIO, active LOW
    const uint8_t ledPin;            // the output GPIO, active HIGH, must be a PWM pin
    uint8_t brightness = 0;          // current PWM for output pin
    uint32_t previousMillis = 0;     // time management
    enum State {
      IDLE,                          // wait for button press
      DIMMUP,                        // push and hold button results in fade in (stop at max)
      HOLD,                          // stay in setted brightness
      DIMMDOWN                       // press button second time results in fade out (stop at light off)
    } state;
    const uint8_t intervalUp = 50;   // up interval / debounce button
    const uint8_t intervalDown = 10;

  public:
    Dimmer(uint8_t buttonPin, uint8_t ledPin) : buttonPin{buttonPin}, ledPin{ledPin}
    {}

    void begin() {                         // to be called in setup()
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(ledPin, OUTPUT);
    }

    // just a function to print debug information to Serial
    void debug() {
      Serial.print(ledPin); Serial.print(F("\t")); Serial.print(state); Serial.print(F("\t")); Serial.println(brightness);
    }

    void update(uint32_t currentMillis = millis()) {   // to be called in loop()
      uint8_t buttonState = digitalRead(buttonPin);
      switch (state) {
        case State::IDLE :
          if (buttonState == LOW) {
            previousMillis = currentMillis;
            brightness = 1;
            analogWrite(ledPin, brightness);
            debug();
            state = State::DIMMUP;             // switch to next state
          }
          break;
        case State::DIMMUP :
          if (currentMillis - previousMillis > intervalUp) {
            if (buttonState == LOW) {
              previousMillis = currentMillis;
              if (brightness < 255) brightness++;  // increase if possible
              debug();
              analogWrite(ledPin, brightness);
            }
            else {
              state = State::HOLD;                // button isn't pressed any more - goto to next state
            }
          }
          break;
        case State::HOLD :
          if (currentMillis - previousMillis > intervalDown) {
            if (buttonState == LOW) {
              previousMillis = currentMillis;
              if (brightness > 0) brightness--;  // decrease if possible
              analogWrite(ledPin, brightness);
              debug();
              state = DIMMDOWN;                  // goto next state
            }
          }
          break;
        case State::DIMMDOWN :
          if (currentMillis - previousMillis > intervalDown) {
            previousMillis = currentMillis;
            if (brightness > 0) brightness--;
            analogWrite(ledPin, brightness);
            debug();
            //if (brightness == 0) state = State::IDLE;  // go back to IDLE
            if (brightness == 0 && buttonState != LOW) state = State::IDLE;  // go back to IDLE - alternative if you don't want to restart in case button is still pressed.
          }
          break;
      }
    }
};

//create  instances (each with one button and one led/LED)
Dimmer dimmer[] {
  {A0, 3},  // buttonPin, ledPin
  {A1, 5},
  {A2, 6},
};

void setup() {
  Serial.begin(115200);
  for (auto &i : dimmer) i.begin();  // call begin for all instances
}

void loop() {
  for (auto &i : dimmer) i.update(); // call update() for all instances
}

The member function void begin(byte pin)

The member function begin() takes care of the right pinMode settings. Call the begin function in your setup.

The member function void debug()

This is just a function to display internal variables. It can be used to follow what the finite state machine is doing during runtime.

The member function void update(uint32_t currentMillis = millis())

update() is the run method. It will handle the LED according to the current state and react on button actions.

You might ask, why the variable currentMillis is defined as parameter with the default value of millis(). Obvious you will need the current millis in the function to do checks against previousMillis. You could use millis() in the function also. But it might be calles several times in the function.

The reason why I prefer to handover the currentMillis as parameter is that this enables me to do one single call of millis() in loop and handover the same value for millis() for all instances per iteration:

void loop() {
  uint32_t currentMillis = millis();              // one call to millis() for all objects
  for (auto &i : dimmer) i.update(currentMillis); // call update() for all instances
}

Create several Dimmer Objects

Writing a class makes it very easy to reuse code for each of the button/LED combination. You just need to create an array of objects and hand over the needed pins:

//create  instances (each with one button and one led/LED)
Dimmer dimmer[] {
  {A0, 3},  // buttonPin, ledPin
  {A1, 5},
  {A2, 6},
};

The setup()

usually you would call the pinMode() for the used pins in your setup(). As we have written a .begin() function we just call .begin() for each object. We don't need a hardcoded count of the objects (in our example 3), nor we need to let the compiler calculate that during runtime .

Similar to other programming languages which offer a foreach loop, we will use an auto range based for loop in C++

for (auto &i : dimmer) i.begin();  // call begin for all instances

The loop()

In loop() we need to call the .update() function for each object. Therefore we use again a auto range based for loop

for (auto &i : dimmer) i.update(); // call update() for all instances

Summary

You see, writing a non blocking sketch and process a sequence of activities in a finite state machine is quite easy using millis(). There is no need to block the code with a delay().

Links

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

First upload: 2023-04-10 | Version: 2024-03-22