Курс Arduino с нуля · Кнопки

Урок 13. Пульт из трёх кнопок

Три кнопки, три светодиода и программа, которая не превращается в кашу: массивы выводов, одна функция антидребезга на все кнопки и режимы работы устройства.

Урок 13 из 31⏱ примерно 60 минутОбновлено: 26 июля 2026

Что понадобится

  • Плата Uno из набора
  • Кабель USB
  • Макетная плата
  • 3 тактовые кнопки
  • 3 светодиода
  • 3 резистора 470 Ом
  • 11 проводов «папа-папа»

Все детали есть в обучающем наборе, по которому идёт курс.

Одна кнопка умеет мало: включить и выключить. Настоящие устройства управляются несколькими кнопками — вспомните пульт от телевизора, панель микроволновки или кнопки на руле. Сегодня мы соберём такое устройство: три кнопки, три светодиода и программа, которая не превращается в кашу.

И это главная тема урока. Собрать три кнопки несложно — сложно написать программу так, чтобы при переходе к пяти кнопкам не пришлось переписывать всё заново. Приёмы, которые мы разберём, работают и для трёх кнопок, и для тридцати.

Что мы соберём

Кнопки не будут просто зажигать светодиоды — так делают выключатели, а не пульты. Каждая кнопка включит свой режим работы устройства:

Кнопка 1все три горятКнопка 2по кругу, каждые 0,18 сКнопка 3всё погашеноКнопки не зажигают светодиоды напрямую — они переключают режим работы устройства.
Три кнопки — три режима. Устройство помнит выбранный режим и живёт в нём, пока не прикажут иначе.

Собираем схему

Отключите USB. Схема самая большая за курс: одиннадцать проводов, три кнопки, три светодиода и три резистора. Собирайте по одному каналу и проверяйте каждый — так найти ошибку в разы проще, чем в готовой куче проводов.

++abcdefghij1510152025302479121418202325283012345671Три светодиода: длинные ножки — в столбики 2, 7 и 12 (ряд e), короткие — через два столбика.2Резисторы 470 Ом: из столбиков 4, 9 и 14 вверх, в верхнюю шину «−».3Зелёные провода: выводы 11, 10 и 9 → столбики 2, 7 и 12 (ряд a).4Три кнопки — верхом на канавке: столбики 18 и 20, 23 и 25, 28 и 30.5Жёлтые провода: выводы 4, 3 и 2 → столбики кнопок сверху от канавки.6Синие перемычки: столбики кнопок снизу от канавки → нижняя шина «−».7Земля: GND верхней гребёнки → верхняя шина «−», GND нижней → нижняя шина «−».
Слева три светодиода со своими резисторами, справа три кнопки. Земля приходит на обе шины «−».

Обратите внимание на две вещи. Во-первых, светодиоды сидят на выводах 11, 10 и 9 — все три с волнистой чертой, то есть с ШИМ. Пока он не нужен, но пригодится в задании. Во-вторых, земля заведена дважды: на верхнюю шину и на нижнюю. На плате несколько гнёзд GND, и все они — одна и та же земля; просто светодиодам удобнее верхняя шина, а кнопкам — нижняя.

UnoGNDD4SB1D3SB2D2SB3D11VD1R1 470 ОмD10VD2R2 470 ОмD9VD3R3 470 ОмСлева три одинаковых входа, справа три одинаковых выхода — потому программа и складывается в массивы.
На чертеже сразу видна главная особенность: три одинаковых входа и три одинаковых выхода.

Проверьте перед включением: все три кнопки сидят верхом на канавке; у каждого светодиода длинная ножка смотрит в сторону своего провода; каждый резистор идёт из столбика короткой ножки в верхнюю шину «−»; обе шины «−» соединены с платой.

Шаг 1. В лоб

Напишем программу самым простым способом: каждая кнопка зажигает свой светодиод, пока её держат. Три канала — три одинаковых куска кода.

const int LED1 = 11;
const int LED2 = 10;
const int LED3 = 9;

const int BUTTON1 = 4;
const int BUTTON2 = 3;
const int BUTTON3 = 2;

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);

  pinMode(BUTTON1, INPUT_PULLUP);
  pinMode(BUTTON2, INPUT_PULLUP);
  pinMode(BUTTON3, INPUT_PULLUP);
}

void loop() {
  // Каждая кнопка зажигает свой светодиод, пока её держат.
  digitalWrite(LED1, digitalRead(BUTTON1) == LOW);
  digitalWrite(LED2, digitalRead(BUTTON2) == LOW);
  digitalWrite(LED3, digitalRead(BUTTON3) == LOW);
}

Программа работает, и это уже приятно: три кнопки, три светодиода, всё слушается. Но посмотрите на неё внимательно. Шесть строк констант, шесть строк pinMode, три одинаковые строки в loop(). Добавим четвёртую кнопку — придётся дописывать в трёх местах. Ошибиться в одном из них — вопрос времени.

Кстати, о короткой записи. Строка digitalWrite(LED1, digitalRead(BUTTON1) == LOW) делает всё сразу: сравнение == LOW даёт да или нет, а digitalWrite понимает их как «включить» и «выключить». Мы уже видели такой приём в десятом уроке.

Шаг 2. Массивы: три канала одной строкой

В шестом уроке мы складывали номера выводов в массив, чтобы перебирать их циклом. Тот же приём спасёт и здесь, только массива теперь два — светодиоды и кнопки.

// Три светодиода и три кнопки. Порядок важен: LEDS[0] работает
// в паре с BUTTONS[0], LEDS[1] — с BUTTONS[1] и так далее.
const int LEDS[3] = {11, 10, 9};
const int BUTTONS[3] = {4, 3, 2};

void setup() {
  for (int i = 0; i < 3; i++) {
    pinMode(LEDS[i], OUTPUT);
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
}

void loop() {
  for (int i = 0; i < 3; i++) {
    digitalWrite(LEDS[i], digitalRead(BUTTONS[i]) == LOW);
  }
}

Ключевая мысль — согласованные массивы. Кнопка с номером i работает в паре со светодиодом с тем же номером i. Порядок внутри массивов не случайный: BUTTONS[0] управляет LEDS[0], BUTTONS[1]LEDS[1]. Стоит перепутать порядок в одном из списков — и кнопки начнут зажигать чужие светодиоды.

Программа стала втрое короче, а главное — чтобы добавить четвёртую пару, теперь достаточно дописать по одному числу в каждый массив и поменять 3 на 4. Ни одной новой строки логики.

Хорошая привычка. Число 3 встречается в программе трижды, и это уже немного опасно: поменяете в одном месте, забудете в другом. Взрослое решение — завести const int COUNT = 3; и писать i < COUNT. Попробуйте сделать это сами — программа станет ещё аккуратнее.

Шаг 3. Антидребезг для любой кнопки

Пока кнопки работают «пока держишь» — дребезг не мешает. Но стоит захотеть выключатель, и нам понадобится всё, что мы делали в одиннадцатом уроке: помнить прошлое состояние, помнить момент изменения, отбрасывать слишком быстрые скачки.

Только теперь это нужно для каждой кнопки отдельно. У каждой своя память — а значит, память тоже становится массивом:

const int LEDS[3] = {11, 10, 9};
const int BUTTONS[3] = {4, 3, 2};
const unsigned long BOUNCE_MS = 30;

// У каждой кнопки своя память: прошлое состояние и момент изменения.
int wasState[3] = {HIGH, HIGH, HIGH};
unsigned long lastChange[3] = {0, 0, 0};

// Горит ли светодиод номер i.
bool ledOn[3] = {false, false, false};

// Возвращает true один раз — в момент нажатия кнопки номер i.
bool pressed(int i) {
  int nowState = digitalRead(BUTTONS[i]);
  bool result = false;

  if (nowState != wasState[i]) {
    if (millis() - lastChange[i] > BOUNCE_MS && nowState == LOW) {
      result = true;
    }
    lastChange[i] = millis();
    wasState[i] = nowState;
  }

  return result;
}

void setup() {
  for (int i = 0; i < 3; i++) {
    pinMode(LEDS[i], OUTPUT);
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
}

void loop() {
  // Теперь каждая кнопка работает выключателем своего светодиода.
  for (int i = 0; i < 3; i++) {
    if (pressed(i)) {
      ledOn[i] = !ledOn[i];
      digitalWrite(LEDS[i], ledOn[i]);
    }
  }
}

Сравните с одиннадцатым уроком: там были переменные wasState и lastChange, здесь — массивы wasState[3] и lastChange[3]. Функция pressed получила параметр — номер кнопки, и работает с памятью именно этой кнопки.

Это очень мощный приём. Одна функция обслуживает любое число кнопок, и каждая из них избавлена от дребезга. Три строчки в loop() — и три независимых выключателя.

Частая ошибка. Если сделать wasState одной переменной на всех, кнопки начнут «мешать» друг другу: нажатие одной будет сбивать память другой. У каждой кнопки должна быть своя ячейка — именно поэтому это массив.

Шаг 4. Настоящий пульт: режимы

Теперь превратим три выключателя в пульт. Разница принципиальная: кнопка больше не управляет «своим» светодиодом — она выбирает, что делает всё устройство.

const int LEDS[3] = {11, 10, 9};
const int BUTTONS[3] = {4, 3, 2};
const unsigned long BOUNCE_MS = 30;
const unsigned long STEP_MS = 180;

int wasState[3] = {HIGH, HIGH, HIGH};
unsigned long lastChange[3] = {0, 0, 0};

// 0 — выключено, 1 — ровный свет, 2 — бегущий огонёк.
int mode = 0;

unsigned long lastStep = 0;
int step = 0;

bool pressed(int i) {
  int nowState = digitalRead(BUTTONS[i]);
  bool result = false;

  if (nowState != wasState[i]) {
    if (millis() - lastChange[i] > BOUNCE_MS && nowState == LOW) {
      result = true;
    }
    lastChange[i] = millis();
    wasState[i] = nowState;
  }

  return result;
}

void allOff() {
  for (int i = 0; i < 3; i++) {
    digitalWrite(LEDS[i], LOW);
  }
}

void setup() {
  for (int i = 0; i < 3; i++) {
    pinMode(LEDS[i], OUTPUT);
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
}

void loop() {
  // Дело первое: слушаем пульт.
  if (pressed(0)) {
    mode = 1;
  }
  if (pressed(1)) {
    mode = 2;
    step = 0;
  }
  if (pressed(2)) {
    mode = 0;
  }

  // Дело второе: показываем то, что выбрано.
  if (mode == 0) {
    allOff();
  }

  if (mode == 1) {
    for (int i = 0; i < 3; i++) {
      digitalWrite(LEDS[i], HIGH);
    }
  }

  if (mode == 2 && millis() - lastStep >= STEP_MS) {
    allOff();
    digitalWrite(LEDS[step], HIGH);

    step = step + 1;
    if (step > 2) {
      step = 0;
    }
    lastStep = millis();
  }
}

Посмотрите, как устроен loop(). Он честно поделён на две части, и это самая важная мысль урока:

  • Сначала слушаем. Опрашиваем кнопки и, если что-то нажали, меняем переменную mode. Больше ничего.
  • Потом показываем. Смотрим на mode и делаем то, что этому режиму положено.

Такое разделение называют «состояние и его отображение». Пока вы его держите, программу легко наращивать: захотели четвёртый режим — добавили кнопку, число и блок отображения. Если же смешать всё в кучу и зажигать светодиоды прямо в момент нажатия, программа превратится в клубок уже на пятом режиме.

Обратите внимание и на режим «бегущий огонёк»: он сделан на millis(), без единого delay. Поэтому огонёк бежит, а пульт при этом мгновенно отзывается на любую кнопку. С delay так не вышло бы: во время паузы устройство было бы глухим.

Шаг 5. Пульт докладывает

Добавим то, чему научились в прошлом уроке: пусть устройство рассказывает, что с ним происходит. Это не украшение — с таким выводом отлаживать пульт в разы проще.

const int LEDS[3] = {11, 10, 9};
const int BUTTONS[3] = {4, 3, 2};
const unsigned long BOUNCE_MS = 30;
const unsigned long STEP_MS = 180;

int wasState[3] = {HIGH, HIGH, HIGH};
unsigned long lastChange[3] = {0, 0, 0};

int mode = 0;
unsigned long lastStep = 0;
int step = 0;

bool pressed(int i) {
  int nowState = digitalRead(BUTTONS[i]);
  bool result = false;

  if (nowState != wasState[i]) {
    if (millis() - lastChange[i] > BOUNCE_MS && nowState == LOW) {
      result = true;
    }
    lastChange[i] = millis();
    wasState[i] = nowState;
  }

  return result;
}

void allOff() {
  for (int i = 0; i < 3; i++) {
    digitalWrite(LEDS[i], LOW);
  }
}

// Печатает, что именно включилось. Функция принимает номер режима.
void reportMode(int newMode) {
  Serial.print("режим ");
  Serial.print(newMode);
  Serial.print(" — ");

  if (newMode == 0) {
    Serial.println("выключено");
  }
  if (newMode == 1) {
    Serial.println("ровный свет");
  }
  if (newMode == 2) {
    Serial.println("бегущий огонёк");
  }
}

void setup() {
  Serial.begin(9600);

  for (int i = 0; i < 3; i++) {
    pinMode(LEDS[i], OUTPUT);
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }

  Serial.println("Пульт готов. Кнопки: свет, огонёк, выключить.");
}

void loop() {
  for (int i = 0; i < 3; i++) {
    if (pressed(i)) {
      Serial.print("нажата кнопка ");
      Serial.println(i + 1);

      if (i == 0) {
        mode = 1;
      }
      if (i == 1) {
        mode = 2;
        step = 0;
      }
      if (i == 2) {
        mode = 0;
      }

      reportMode(mode);
    }
  }

  if (mode == 0) {
    allOff();
  }

  if (mode == 1) {
    for (int i = 0; i < 3; i++) {
      digitalWrite(LEDS[i], HIGH);
    }
  }

  if (mode == 2 && millis() - lastStep >= STEP_MS) {
    allOff();
    digitalWrite(LEDS[step], HIGH);

    step = step + 1;
    if (step > 2) {
      step = 0;
    }
    lastStep = millis();
  }
}

Здесь появился ещё один приём: собственная функция reportMode с параметром. Она печатает человеческое название режима вместо голого числа. Печать вынесена из loop(), и он остался читаемым.

Откройте монитор порта и понажимайте кнопки. Вы увидите ровно одну строку на одно нажатие — значит, антидребезг работает; и увидите, какой режим включился, — значит, устройство поняло вас правильно.

Не работает? Разбираемся

  • Одна кнопка работает, остальные нет. Проверьте её жёлтый провод: он должен приходить в столбик кнопки сверху от канавки, а синяя перемычка уходить из того же столбика снизу. И проверьте номера выводов в массиве BUTTONS.
  • Кнопки зажигают чужие светодиоды. Перепутан порядок в массивах: номер в LEDS должен соответствовать номеру в BUTTONS.
  • Один светодиод не горит. Дело почти наверняка не в программе: переверните его (длинная ножка — к проводу) или проверьте, доходит ли его резистор до шины «−».
  • Светодиоды горят тускло и все сразу. Скорее всего, резисторы стоят не в своих столбиках и светодиоды соединились между собой. Проверьте по схеме каждый канал отдельно.
  • Кнопка срабатывает несколько раз подряд. Убедитесь, что wasState и lastChangeмассивы, а функция pressed работает с ячейкой [i], а не с общей переменной.
  • Огонёк не бежит. Проверьте, что вторая кнопка ставит mode = 2, и что в блоке огонька стоит millis(), а не delay.

Задания для тренировки

  • РазминкаДобавьте четвёртый режим: все светодиоды мигают одновременно. Подсказка: понадобится четвёртая кнопка — или пусть первая кнопка при повторном нажатии переключает между «ровным светом» и «миганием».
  • ПосложнееСделайте, чтобы огонёк бежал в обе стороны: туда и обратно. Подсказка: заведите переменную с направлением (+1 или −1) и меняйте её на краях.
  • Со звёздочкой ⭐Превратите пульт в диммер: первая кнопка — ярче, вторая — тусклее, третья — выключить. Пять ступеней яркости. Разбор ниже.
Показать разбор «пульт-диммер»

Здесь пригодятся сразу три вещи: ШИМ из восьмого урока (потому мы и посадили светодиоды на выводы 11, 10 и 9), антидребезг из одиннадцатого и массивы из этого.

const int LEDS[3] = {11, 10, 9};
const int BUTTONS[3] = {4, 3, 2};
const unsigned long BOUNCE_MS = 30;

int wasState[3] = {HIGH, HIGH, HIGH};
unsigned long lastChange[3] = {0, 0, 0};

// Уровень яркости: от 0 (темно) до 4 (на полную).
int level = 0;

bool pressed(int i) {
  int nowState = digitalRead(BUTTONS[i]);
  bool result = false;

  if (nowState != wasState[i]) {
    if (millis() - lastChange[i] > BOUNCE_MS && nowState == LOW) {
      result = true;
    }
    lastChange[i] = millis();
    wasState[i] = nowState;
  }

  return result;
}

void setup() {
  for (int i = 0; i < 3; i++) {
    pinMode(LEDS[i], OUTPUT);
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
}

void loop() {
  if (pressed(0)) {
    level = level + 1;
    if (level > 4) {
      level = 4;
    }
  }

  if (pressed(1)) {
    level = level - 1;
    if (level < 0) {
      level = 0;
    }
  }

  if (pressed(2)) {
    level = 0;
  }

  // Все три светодиода светят одинаково — как одна лампа.
  for (int i = 0; i < 3; i++) {
    analogWrite(LEDS[i], level * 63);
  }
}

Вся память устройства — одна переменная level. Кнопки только увеличивают и уменьшают её, а показывает результат отдельный цикл в конце: level * 63 превращает уровни 0…4 в яркости 0, 63, 126, 189 и 252.

Проверки if (level > 4) и if (level < 0) обязательны. Без них значение уедет за границы, и analogWrite поведёт себя странно — яркость скакнёт с максимума в темноту. Ограничивайте всё, что копится.

Что мы узнали

  • Несколько одинаковых деталей — это массивы. Согласованные массивы (LEDS и BUTTONS) связывают их попарно по номеру.
  • Функция с параметром обслуживает любое число кнопок; память каждой кнопки живёт в своей ячейке массива.
  • Устройство удобно описывать режимом — одной переменной, которая помнит, что оно сейчас делает.
  • Разделяйте loop() на «слушаем» и «показываем». Это то, что отличает программу, которую можно развивать, от той, которую придётся переписать.
  • Без delay устройство может делать несколько дел сразу: и бежать огоньком, и мгновенно слушать кнопки.

На этом блок «Кнопки» закончен. Итог у него солидный: плата умеет слушать человека, различать нажатие и удержание, не спотыкаться о дребезг, разговаривать текстом и управлять собой из нескольких точек.

Дальше — самое интересное. В следующем блоке плата научится чувствовать мир вокруг: свет, наклон, вибрацию и магнитное поле. И первым делом мы разберёмся с крутилкой-потенциометром, у которой, в отличие от кнопки, не два состояния, а целая тысяча.

Частые вопросы

Зачем складывать номера выводов в массив?

Чтобы одинаковые действия писать один раз. С массивом три канала обслуживает один цикл for, а чтобы добавить четвёртый, достаточно дописать по числу в каждый массив. Без массива каждая новая кнопка добавляет три-четыре строки в разных местах программы.

Почему память кнопок тоже должна быть массивом?

У каждой кнопки своё прошлое состояние и свой момент последнего изменения. Если сделать их общими переменными, кнопки начнут сбивать память друг другу: нажатие одной будет мешать другой. Массивы дают каждой кнопке свою ячейку.

Что такое режим работы устройства?

Это переменная, которая помнит, чем устройство занято сейчас: горит ровно, бежит огоньком или выключено. Кнопки только меняют её значение, а отдельная часть программы смотрит на неё и показывает результат. Так программу можно наращивать, не запутываясь.

Сколько кнопок можно подключить к Arduino Uno?

Свободных цифровых выводов у Uno одиннадцать (со 2 по 12), плюс при желании годятся аналоговые входы A0–A5. То есть до полутора десятков кнопок напрямую. Если нужно больше, применяют матричное подключение или отдельные микросхемы-расширители.

← Все уроки курса