Светильник в прихожей у меня включался от приближения руки. Не по сенсорной магии — провод от кнопки болтался в воздухе, а вход Arduino читал помехи из ниоткуда: плавающий пин ловил наводки и генерировал случайные HIGH. Классические полчаса отладки, которые можно было пропустить, поняв одну вещь: цифровой вход без подтяжки — это антенна.
В этом уроке подключаем кнопку правильно, без единого внешнего резистора — на встроенном INPUT_PULLUP. Разберём, почему у кнопки «инвертированная» логика, зачем нужен антидребезг, и соберём светильник, который реагирует на руку, но только настоящую.
Как читать кнопку: digitalRead
Если digitalWrite() говорит пину, какое напряжение держать, то digitalRead() слушает: функция возвращает HIGH (1), если на пине близко к 5 В, и LOW (0), если близко к 0 В. Но вход — не провод: если его ни к чему не подключить, он «висит в воздухе» и читает что попало. Вот почему у меня светильник срабатывал от руки — тело было антенной, притягивающей наводки.
Чтобы вход всегда знал своё состояние, его «подтягивают» резистором к 5 В (pull-up, вход всегда HIGH, кнопка прижимает к GND) или к земле (pull-down, наоборот). До недавнего времени в каждую схему с кнопкой ставили резистор 10 кОм. Но у ATmega328P внутри уже есть подтяжка — её включает pinMode(пин, INPUT_PULLUP), и внешний резистор не нужен.
void setup() { pinMode(2, INPUT_PULLUP); // внутренняя подтяжка к 5В Serial.begin(9600);} void loop() { int state = digitalRead(2); Serial.println(state); delay(100);}Инвертированная логика: LOW — значит нажата
Схема при INPUT_PULLUP выглядит контринтуитивно: одна нога кнопки идёт на пин 2, вторая — на GND. В покое подтяжка держит на входе HIGH. Нажатие соединяет пин с землёй — вход читает LOW. То есть «кнопка нажата» — это LOW, а не HIGH. Первое время каждый ардуинщик хотя бы раз пишет if (digitalRead(2) == HIGH) и удивляется, почему светодиод горит, когда кнопка НЕ нажата.
Запомнить легко: подтяжка поднимает, кнопка прижимает. Покой — HIGH, нажатие — LOW. Если ваша кнопка ведёт себя «наоборот» — проверьте, не pull-down ли у вас по факту, или просто поменяйте сравнение.
Дребезг: почему кнопка щёлкает не один раз
Контакты кнопки механические: при нажатии они стучат друг о друга 5–20 миллисекунд, замыкаясь и размыкаясь десятки раз. Для человека — один щелчок, для Arduino — десяток нажатий. Если счётчик нажатий растёт скачками по 3–7 за клик — это оно, дребезг (bounce).
Полноценный антидребезг на millis() — тема отдельного урока раздела «Продвинутая платформа», но простой вариант нужен уже сейчас: после обнаружения нажатия просто подождать 50 мс и игнорировать всё это время. Для светильника и меню — за глаза. Для энкодера или счётчика событий — уже нет, там debounce на millis обязателен.
int count = 0; void setup() { pinMode(2, INPUT_PULLUP); Serial.begin(9600);} void loop() { if (digitalRead(2) == LOW) { // нажата count++; Serial.print("Нажатий: "); Serial.println(count); delay(300); // ждём, пока палец уйдёт и дребезг затихнет }}Светильник: кнопка-переключатель
Объединим всё: кнопка не просто «пока держишь — горит», а переключает свет, как выключатель в комнате. Нажал — включился, нажал ещё раз — выключился. Для этого нужно помнить состояние света в переменной и переключать его по фронту нажатия — то есть ловить момент «стало нажато», а не «есть нажато».
Ловушка здесь та же, что и с дребезгом: если переключать свет на каждое LOW, за один щелчок свет мигнёт туда-сюда пять раз. Грубое лечение то же — пауза после переключения.
Три ошибки с кнопками, которые съедают вечер
- Нет подтяжки. Вход «висит в воздухе», значения скачут: 0, 1, 0... Симптом — «срабатывает сама». Лечение: INPUT_PULLUP или резистор 10 кОм.
- Перепутана логика. Ожидали HIGH при нажатии, а с подтяжкой нажатие — LOW. Лечение: инвертировать сравнение.
- Дребезг без обработки. Один клик = 3–7 срабатываний. Лечение: пауза 50–300 мс для простых задач, millis-дебаунс для серьёзных.
Эксперименты
- Поменяйте LOW на HIGH в светофоре-светильнике и объясните себе (вслух), что изменилось.
- Сделайте «инверсный светильник»: свет горит всегда, а кнопка его выключает. Одна строка кода.
- Добавьте второй счётчик — нажатий на «отпускание» (HIGH). Заметьте: ему тоже нужен антидребезг.
- Соберите «нормальный режим»: пока кнопка удерживается, светодиод мигает 5 раз в секунду (задача на комбинирование условий и таймера из урока про циклы).
- digitalRead(пин) читает уровень: HIGH ≈ 5 В, LOW ≈ 0 В
- INPUT_PULLUP включает встроенную подтяжку — внешний резистор не нужен
- С подтяжкой покой = HIGH, нажатие = LOW — логика инвертирована
- Вход без подтяжки — антенна: «срабатывает сама» это оно
- Дребезг 5–20 мс лечится паузой (просто) или millis-дебаунсом (правильно)
- 1. Кнопка подключена к пину 2 с INPUT_PULLUP, вторая нога — на GND. Что вернёт digitalRead(2) при нажатой кнопке?
- 2. Зачем нужен подтягивающий резистор (или INPUT_PULLUP)?
- 3. Один щелчок кнопки даёт пять срабатываний счётчика. Причина?
- 4. Что НЕПРАВИЛЬНО в этой строке: pinMode(2, OUTPUT); if (digitalRead(2) == LOW) ...?
Следующая ступень — сделать антидребезг и переключатель «по-взрослому», без блокирующих delay: это главный навык для всех дальнейших проектов, и им занимается урок про millis(). А пока пощёлкайте кнопкой в симуляторе: попробуйте собрать ночник, который включается одним коротким нажатием, а выключается — длинным (подсказка: держите кнопку и считайте миллисекунды).
Частые вопросы по теме урока
- Кнопка подключена к пину 2 с INPUT_PULLUP, вторая нога — на GND. Что вернёт digitalRead(2) при нажатой кнопке?
- Подтяжка держит вход на HIGH, нажатие прижимает его к земле — читается LOW. LOW
- Зачем нужен подтягивающий резистор (или INPUT_PULLUP)?
- Подтяжка задаёт входу определённый уровень в покое; без неё пин читает случайные значения от наводок. Чтобы вход не «висел в воздухе» и не ловил наводки
- Один щелчок кнопки даёт пять срабатываний счётчика. Причина?
- Контакты стучат 5–20 мс; лечится антидребезгом — паузой или millis-проверкой. Дребезг механических контактов
- Что НЕПРАВИЛЬНО в этой строке: pinMode(2, OUTPUT); if (digitalRead(2) == LOW) ...?
- Для чтения кнопки пин настраивают как INPUT или INPUT_PULLUP; OUTPUT для этого не подходит. Оба варианта: читать вход надо после настройки его как INPUT/INPUT_PULLUP