← До всіх проєктів
Основи Середній

Класи C++ в Arduino: Led і Button без хаосу в loop()

Коли кнопок і світлодіодів стає кілька, глобальні змінні перетворюються на кашу. Пишемо два маленькі класи з конструкторами й методами, складаємо об'єкти в масиви — і loop() вміщується в п'ять рядків.

Складність
Середній
Час
45 min
Категорія
Основи
Оновлено

Про проєкт

У статті про кнопку одна кнопка потребувала трьох глобальних змінних: попередній стан, час зміни і стан світлодіода. Дві кнопки — вже шість змінних і дубльований код, п'ять — хаос. Правильна відповідь — класи: кожен об'єкт зберігає власний стан, а код пишеться один раз.

Що знадобиться

  • ESP32 DevKit
  • 2 кнопки, 2 світлодіоди, 2 резистори 220 Ом

Схема

Деталь GPIO
Кнопка 1 (червона) GPIO4 ↔ GND
Кнопка 2 (зелена) GPIO0 ↔ GND
Світлодіод 1 GPIO23 → 220 Ом → LED → GND
Світлодіод 2 GPIO22 → 220 Ом → LED → GND

GPIO0 — це кнопка BOOT на платі, тож другу кнопку можна навіть не ставити. Тільки не тримайте її під час увімкнення, інакше ESP32 перейде в режим прошивки.

Анатомія класу

class Led {
  public:            ← те, що доступно ззовні: інтерфейс
    Led(int pin);    ← конструктор: викликається при створенні об'єкта
    void toggle();   ← методи: що об'єкт уміє
  private:           ← внутрішній стан, недоступний ззовні
    int pin;
    bool state;
};
  • Список ініціалізації : pin(pin) заповнює поля ще до виконання тіла конструктора.
  • const після методу (isOn() const) обіцяє, що метод нічого не змінює в об'єкті.
  • Значення за замовчуванням (debounceMs = 30) дозволяють писати Button(4) або Button(4, 50).

Важлива деталь для Arduino: глобальні об'єкти створюються до setup(), коли периферія ще не готова. Тому pinMode() не викликають у конструкторі, а виносять в окремий метод begin(). Так само влаштовані Serial.begin() і Wire.begin().

Код

// ООП в Arduino: маленькі класи тримають loop() охайним
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 один раз на кожне натискання
    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("Кнопка %d → світлодіод %s\n", i + 1, leds[i].isOn() ? "УВІМК" : "ВИМК");
    }
  }
}

Чому & у циклі

for (Led &l : leds) проходить масив за посиланням. Без & кожна ітерація отримувала б копію об'єкта, і begin() налаштовував би копію, а не справжній світлодіод. Для класів, які зберігають стан, це класична помилка.

Що далі

  • Додайте в Led метод blink(periodMs), що працює через millis() без delay(), з викликом update() у loop().
  • Винесіть класи в окремі файли Led.h / Led.cpp: це перший крок до власної бібліотеки, як описано в документації Arduino за посиланням нижче.