← All projects
Basics Intermediate

C++ Classes in Arduino: Led and Button Without a Messy loop()

Once you have several buttons and LEDs, global variables turn into a mess. We write two small classes with constructors and methods, put the objects into arrays — and loop() fits into five lines.

Difficulty
Intermediate
Time
45 min
Category
Basics
Updated

About the project

In the button article, one button needed three global variables: the previous state, the time of change and the LED state. Two buttons already need six variables and duplicated code, and five are chaos. The right answer is classes: every object keeps its own state, and the code is written once.

What you need

  • ESP32 DevKit
  • 2 buttons, 2 LEDs, 2 × 220 Ω resistors

Wiring

Part GPIO
Button 1 (red) GPIO4 ↔ GND
Button 2 (green) GPIO0 ↔ GND
LED 1 GPIO23 → 220 Ω → LED → GND
LED 2 GPIO22 → 220 Ω → LED → GND

GPIO0 is the BOOT button on the board, so you can even skip the second button. Just don't hold it at power-up, or the ESP32 enters flashing mode.

Anatomy of a class

class Led {
  public:            ← what the outside can use: the interface
    Led(int pin);    ← constructor: runs when the object is created
    void toggle();   ← methods: what the object can do
  private:           ← internal state, hidden from the outside
    int pin;
    bool state;
};
  • The initializer list : pin(pin) fills the fields before the constructor body runs.
  • const after a method (isOn() const) promises that the method changes nothing in the object.
  • Default arguments (debounceMs = 30) let you write Button(4) or Button(4, 50).

An important detail on Arduino: global objects are created before setup(), when the peripherals are not ready yet. So pinMode() is not called in the constructor but in a separate begin() method. Serial.begin() and Wire.begin() work the same way.

Code

// Object-oriented Arduino: small classes keep loop() tidy
class Led {
  public:
    Led(int pin) : pin(pin) {}
    void begin() { pinMode(pin, OUTPUT); off(); }
    void on()  { set(true); }
    void off() { set(false); }
    void toggle() { set(!state); }
    bool isOn() const { return state; }
  private:
    int pin;
    bool state = false;
    void set(bool s) { state = s; digitalWrite(pin, s ? HIGH : LOW); }
};

class Button {
  public:
    Button(int pin, unsigned long debounceMs = 30) : pin(pin), debounce(debounceMs) {}
    void begin() { pinMode(pin, INPUT_PULLUP); }
    // true once per press
    bool pressed() {
      int now = digitalRead(pin);
      if (now != last && millis() - changedAt > debounce) {
        changedAt = millis();
        last = now;
        return now == LOW;
      }
      return false;
    }
  private:
    int pin;
    unsigned long debounce;
    int last = HIGH;
    unsigned long changedAt = 0;
};

Led leds[] = { Led(23), Led(22) };
Button buttons[] = { Button(4), Button(0) };

void setup() {
  Serial.begin(115200);
  for (Led &l : leds) l.begin();
  for (Button &b : buttons) b.begin();
}

void loop() {
  for (int i = 0; i < 2; i++) {
    if (buttons[i].pressed()) {
      leds[i].toggle();
      Serial.printf("Button %d → LED %s\n", i + 1, leds[i].isOn() ? "ON" : "OFF");
    }
  }
}

Why the & in the loop

for (Led &l : leds) walks the array by reference. Without the &, every iteration would get a copy of the object, and begin() would set up the copy instead of the real LED. It is a classic mistake with classes that keep state.

What next

  • Add a blink(periodMs) method to Led that runs on millis() without delay(), with an update() call in loop().
  • Move the classes into their own Led.h / Led.cpp files: the first step towards your own library, as the Arduino guide linked below explains.