Пульт для самодельной колонки я нарисовал с одной кнопкой: клик — следующий трек, двойной — назад, удержание — режим громкости. Три жеста, один пин D2, по прикидкам — полчаса работы. Первая версия реагировала прямо на digitalRead — самую первую кнопку из базовых уроков: loop() на Uno прокручивается десятки тысяч раз в секунду, и нажатие длиной 150 мс натикало «нажато» несколько тысяч раз. Трек сменился один раз, а озвучка меню захлебнулась на минуту.
Второй заход был через delay: поймал нажатие — уснул на 50 мс — проверил снова. Дребезг ушёл, но с ним ушла и реакция устройства: пока скетч спал в ожидании отпускания, датчики и Serial стояли. Считалось и по-честному: нажатие длиной 150 мс — это три окна по 50 мс сна, а длинное удержание для режима громкости морило плату по полсекунды на каждое касание. А двойной клик в этой версии не получался в принципе: чтобы отличить одиночный от двойного, надо ждать после отпускания — а delay умеет паузу только после нажатия.
Выход из тупика — автомат состояний: переменная-состояние, switch и тайминги на millis(). Кнопка перестаёт быть «нажата или нет» и становится машиной, которая проходит стадии и в нужный момент выдаёт ровно одно событие: клик, двойной клик или удержание.
Уровень и фронт: почему digitalRead не событие
Ключевое различие, на котором я спотыкался дважды. digitalRead возвращает уровень — «прямо сейчас нажата». Событие — это фронт, переход из одного состояния в другое: стало нажатым, стало отпущенным. Пока их смешиваешь, скетч либо срабатывает на каждом круге loop (уровень), либо не срабатывает вовсе. Сами автоматы на состояниях я подробно расписывал на светофоре в уроке про конечный автомат — здесь та же машина, только ввод у неё не таймер, а палец.
Классическая грабля из той колонки: событие срабатывало на отпускание, а логика ждала нажатие. Схема выглядела разумно — «считаю длину нажатия, значит реагирую, когда палец убрал» — но подсветка выбранного пункта зажигалась в момент отпускания, а ожидание флага стояло на нажатии, которое уже прошло. Симптом один на все такие беды: интерфейс отзывается с задержкой в одно нажатие. Диагноз: перепутана семантика фронтов — где-то ветка стоит на спаде, где-то на фронте.
Автомат лечит это структурно: в каждом состоянии явно написано, что делать на нажатие и что на отпускание, а switch не даёт ветки перепутать. Состояние — обычная переменная, переходы — условия с millis() из урока про millis вместо delay, ни одного delay внутри.
| Состояние | Что видит автомат | Переход и событие |
|---|---|---|
| IDLE | кнопка нажалась | запомнить millis() и перейти в PRESSED; событий нет |
| PRESSED | держат дольше 600 мс | событие «удержание» один раз; ждать отпускания |
| PRESSED | отпустили раньше 600 мс | перейти в MEZH, запомнить время отпускания |
| MEZH | второе нажатие в окне 250 мс | событие «двойной клик», потом ждать отпускания |
| MEZH | окно 250 мс истекло | событие «клик» и обратно в IDLE |
Числа я выстрадал такие: граница удержания 600 мс, окно ожидания второго клика 250 мс. Шестьсот — примерно вдвое дольше обычного клика, который живёт в пределах 80-150 мс; короче — удержание ловится случайно, длиннее — сидеть в ожидании утомительно. Двести пятьдесят — пауза, в которую вписывается человек, жмущий второй раз подряд: между двумя своими кликами я попадал в 90-200 мс, а пауза «клик и всё» всегда выходила длиннее.
Дребезг в этот автомат я не вписывал сознательно: контактная дребежь живёт в первые 5-20 мс после замыкания, и её давит дебаунс из базового урока, стоящий перед автоматом. Разделение труда: debounce решает, что нажатие настоящее, автомат решает, что это за жест. Впихнуть оба в одну функцию — мой проверенный способ сломать окно двойного клика.
Второе правило после таймингов: событие выстреливает один раз. Внутри состояния не «пока удерживается — делай», а «перевалил за 600 — отметь и больше не отмечай». Флаг, который гасит повтор, — дешёвая страховка от тысяч срабатываний из первого абзаца урока.
Тройку жестов стоит изучить на готовых приборах — она везде. Карманный фонарик: клик — вкл/выкл, несколько кликов подряд — смена яркости, удержание — плавный диммер. Сигнализация: удержание при постановке снимает задержку выхода, двойной клик — режим «я дома». Контроллер светодиодной ленты в моей комнате устроен так же: клик — смена сцены, двойной — нейтральный белый, удержание — выключение с плавным гашением. Когда видишь такой интерфейс вживую, рука сама попадает в те самые 90-200 мс между кликами — интервалы подогнаны под человека, а не под даташит.
// Автомат кнопки: клик, двойной клик, удержание. Debounce стоит перед ним.const int PIN_BTN = 2;const int PIN_LED = 12;const unsigned long GRAN_DERZH = 600; // длиннее — удержаниеconst unsigned long OKNO_DVOJ = 250; // окно второго клика int sostoyanie = 0; // 0 IDLE, 1 PRESSED, 2 MEZH, 3 VTOROJunsigned long tNazhal = 0;unsigned long tOtpustil = 0;boolean derzhanieOtmecheno = false; void vybratPunkt() { Serial.println("DEYSTVIE: vybor"); }void nazadVMenu() { Serial.println("DEYSTVIE: nazad"); }void rejimNastroyki() { Serial.println("DEYSTVIE: nastroyka"); } void setup() { pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("Klik / dvojnoj klik / derzhanie");} void loop() { unsigned long now = millis(); boolean nazhata = digitalRead(PIN_BTN) == LOW; // INPUT_PULLUP: нажата — LOW if (sostoyanie == 0) { // IDLE: жду нажатие if (nazhata) { sostoyanie = 1; tNazhal = now; derzhanieOtmecheno = false; } } else if (sostoyanie == 1) { // PRESSED: палец на кнопке if (nazhata && !derzhanieOtmecheno && now - tNazhal >= GRAN_DERZH) { derzhanieOtmecheno = true; // событие один раз, не в цикле rejimNastroyki(); digitalWrite(PIN_LED, HIGH); // огонь горит, пока держат } if (!nazhata) { digitalWrite(PIN_LED, LOW); if (derzhanieOtmecheno) { sostoyanie = 0; // отпустили после удержания } else if (now - tNazhal < GRAN_DERZH) { sostoyanie = 2; // короткое нажатие — окно tOtpustil = now; } else { sostoyanie = 0; } } } else if (sostoyanie == 2) { // MEZH: ждём второй клик if (nazhata) { sostoyanie = 3; // второй клик состоялся tNazhal = now; } else if (now - tOtpustil >= OKNO_DVOJ) { vybratPunkt(); // второй не пришёл — одиночный sostoyanie = 0; } } else if (sostoyanie == 3) { // VTOROJ: ждём отпускания if (!nazhata) { nazadVMenu(); sostoyanie = 0; } }}Три жеста в симуляторе
В макетке кнопка D2 и светодиод D12. Каждое событие печатается в монитор строкой event=klick, event=dvojnoj или event=derzhanie с длительностью в миллисекундах — и вспыхивает светодиод: на клик 80 мс, на двойной 150, на удержание горит, пока держишь. Вспышки разной длины придуманы не для красоты: в собранной коробке монитор не виден, и разный ритм огня говорит, какой жест распознался.
Кнопка движка нажимается идеально — дребезга нет, поэтому автомат показан в чистом виде. На настоящей плате перед ним стоит дебаунс: он уже решил, что контакт настоящий. Если двойной клик не ловится на живой кнопке — проблема почти всегда в debounce, съевшем второе нажатие, а не в таймингах автомата.
Собирается это в меню прибора так: клик — выбрать пункт или перелистнуть, двойной — вернуться на уровень вверх, удержание — войти в режим настройки, где тот же клик меняет значение, а удержание сохраняет. Один пин — три команды, пульт колонки из начала урока уже не мечта. Раскладку попроще я применял в уроке про меню на двух кнопках: там два пина и два события, здесь один пин и три.
Честная цена двойного клика: одиночный теперь отрабатывает не сразу, а через 250 мс после отпускания — автомат обязан дождаться, не придёт ли второй. В меню и пульте это незаметно; в игре, где клик — выстрел, четверть секунды провала стоят проигрыша. Мнение с границей: если скорость реакции важнее экономии пинов — разноси функции по двум кнопкам, автомат с двойным кликом оставь приборным интерфейсам.
Куда расти. Удержание с автоповтором: после 600 мс каждые 150 мс выдавать событие ещё раз, пока держат, — так работают счётчики минут и часов на одном пине; таймер повтора — это тот же millis-гейт, только внутри состояния PRESSED, и флаг derzhanieOtmecheno в нём меняет роль: не гасит событие, а открывает дорогу повторам. Если жесты надо ловить во сне или в затылок с точностью цикла — переходи на прерывание кнопки: автомат остаётся тем же, просыпается по нажатию. А если хочется крутить вместо тыкать — энкодер EC11 ловит вращение теми же прерываниями.
- 1. Почему прямая реакция на digitalRead печатает «нажато» тысячи раз?
- 2. Какая граница отделяет удержание от обычного клика в уроке?
- 3. Окно ожидания второго клика закрывается через...
- 4. Почему одиночный клик стал «ленивым» на 250 мс?
- 5. Где в автомате место для печати события?
- digitalRead — уровень, событие — фронт: тысячи срабатываний за одно нажатие лечатся автоматом состояний на switch
- Тайминги урока: удержание — дольше 600 мс, окно второго клика — 250 мс после отпускания; константы наверх скетча, подгон под руку
- Каждое событие — один раз: флаг derzhanieOtmecheno гасит повторы, пока кнопка держится
- Debounce стоит перед автоматом и решает, настоящее ли нажатие; автомат решает, какой это жест — впихивать оба в одну функцию не надо
- Цена двойного клика — ленивый одиночный (+250 мс); для игр разноси жесты на две кнопки, автомат оставь меню и пультам
Частые вопросы по теме урока
- Почему прямая реакция на digitalRead печатает «нажато» тысячи раз?
- digitalRead — про «прямо сейчас», а не про «началось». Пока палец на кнопке, уровень на каждом круге один и тот же — и ветка срабатывает снова. Событие — это фронт, переход. digitalRead возвращает уровень, а loop крутится десятки тысяч раз в секунду — уровень держится на каждом круге
- Какая граница отделяет удержание от обычного клика в уроке?
- Удержание — дольше 600 мс: примерно вдвое дольше обычного клика на 80-150 мс. Константа наверху скетча — подгоняй под свою руку. 600 мс
- Окно ожидания второго клика закрывается через...
- Отсчёт окна идёт от отпускания: отпустил — 250 мс на второй клик. Пришёл второй — двойной; не пришёл — одиночный. 250 мс после отпускания первого клика
- Почему одиночный клик стал «ленивым» на 250 мс?
- Двойной и одиночный различаются только будущим: до истечения окна автомат не знает, что второй клика не будет. В меню четверть секунды незаметна, в игре — дорого. автомат обязан дождаться истечения окна: не придёт ли второй клик
- Где в автомате место для печати события?
- Печать и действие живут там же, где принимается решение о переходе, — поэтому событие одно, а не тысячи. delay внутри автомата ломает все тайминги. в ветке перехода: событие выстреливает один раз при смене состояния