Курс Arduino с нуля · Кнопки
Урок 13. Пульт из трёх кнопок
Три кнопки, три светодиода и программа, которая не превращается в кашу: массивы выводов, одна функция антидребезга на все кнопки и режимы работы устройства.
Что понадобится
- Плата Uno из набора
- Кабель USB
- Макетная плата
- 3 тактовые кнопки
- 3 светодиода
- 3 резистора 470 Ом
- 11 проводов «папа-папа»
Все детали есть в обучающем наборе, по которому идёт курс.
Одна кнопка умеет мало: включить и выключить. Настоящие устройства управляются несколькими кнопками — вспомните пульт от телевизора, панель микроволновки или кнопки на руле. Сегодня мы соберём такое устройство: три кнопки, три светодиода и программа, которая не превращается в кашу.
И это главная тема урока. Собрать три кнопки несложно — сложно написать программу так, чтобы при переходе к пяти кнопкам не пришлось переписывать всё заново. Приёмы, которые мы разберём, работают и для трёх кнопок, и для тридцати.
Что мы соберём
Кнопки не будут просто зажигать светодиоды — так делают выключатели, а не пульты. Каждая кнопка включит свой режим работы устройства:
Собираем схему
Отключите USB. Схема самая большая за курс: одиннадцать проводов, три кнопки, три светодиода и три резистора. Собирайте по одному каналу и проверяйте каждый — так найти ошибку в разы проще, чем в готовой куче проводов.
Обратите внимание на две вещи. Во-первых, светодиоды сидят на выводах 11, 10 и 9 — все три с волнистой чертой, то есть с ШИМ. Пока он не нужен, но пригодится в задании. Во-вторых, земля заведена дважды: на верхнюю шину и на нижнюю. На плате несколько гнёзд GND, и все они — одна и та же земля; просто светодиодам удобнее верхняя шина, а кнопкам — нижняя.
Проверьте перед включением: все три кнопки сидят верхом на канавке; у каждого светодиода длинная ножка смотрит в сторону своего провода; каждый резистор идёт из столбика короткой ножки в верхнюю шину «−»; обе шины «−» соединены с платой.
Шаг 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. То есть до полутора десятков кнопок напрямую. Если нужно больше, применяют матричное подключение или отдельные микросхемы-расширители.
