Таймер на кухне, отсчёт перед стартом, выдержка для фотопроцесса — схема одна: девять, восемь, семь… ноль, сигнал. Написать её на delay зудит руки сразу: delay(1000) на каждый тик — и готово. У меня такая версия прожила до первой же попытки повесить на неё кнопку «стоп» — и не повесилась: между тиками loop() стоял в паузе целую секунду, нажатия проскакивали мимо, и останавливать таймер приходилось выключением платы. Секунда блокировки на каждый тик — это десять секунд слепоты из десяти, как ни крути.
Соберём правильную версию на трёх деталях, которые уже есть в предыдущих уроках кластера: индикатор из урока про подключение рисует цифры через массив паттернов из счётчика, тик раз в секунду ведёт millis по принципам урока про millis вместо delay, а на нуле вступает зуммер с механикой tone из урока про пьезозуммер. Кнопка даст три действия в одной детали: старт, пауза и сброс длинным нажатием. Скетч получится на сотню строк, но в нём не будет ни одного незнакомого приёма — только знакомые, собранные в один автомат.
Три состояния и тик раз в секунду
Живой таймер — это автомат с тремя состояниями, ровно по схеме из урока про светофор. «Стоит» — цифра висит на индикаторе, ничего не тикает. «Идёт» — раз в 1000 мс цифра уменьшается на единицу. «Финал» — на нуле таймер гасит отсчёт и даёт сигнал: tone на 1000 Гц длиной 600 мс, потом noTone. Переключения делает кнопка: короткое нажатие стартует или ставит на паузу, длинное — сбрасывает на девятку.
Тик устроен гейтом: if (go == 1 && millis() - tTick >= 1000) — внутри цифра уменьшается, tTick перезаписывается, и loop() между тиками остаётся свободным для кнопки и зуммера. Защита if (cifra > 0) не даёт отсчёту уйти за ноль в мусор массива — та же дисциплина границ, что в счётчике. Флип флага go записан как go = 1 - go: ноль становится единицей и наоборот, без единого if.
Мелочь про честность тика: tTick = millis() внутри ветки означает, что следующий тик отстоит на секунду от предыдущего тика, а не от круглой даты, — за десяток отсчётов тайминг уползёт на десятки миллисекунд, которые loop() тратит между проходами. Для кухонного таймера это неважно; если когда-нибудь понадобится метроном с точностью, tTick заменяют на tTick += 1000 и ловят дрейф по-инженерному. Здесь я оставил простой вариант — его проще читать. В дежурном состоянии — go равен нулю и fin равен нулю — скетч вообще не трогает индикатор: цифра просто горит, и это правильное поведение, а не зависание.
| Состояние | Экран и звук | Как выходит |
|---|---|---|
| Стоит | цифра горит без изменений, тишина | короткое нажатие — старт отсчёта |
| Идёт | тик раз в 1000 мс, цифра падает на единицу | короткое нажатие — пауза; длинное — сброс на 9 |
| Финал | на нуле бип 1000 Гц на 600 мс, потом тишина | длинное нажатие — сброс и новый отсчёт |
const int SEG[7] = {2, 3, 4, 5, 6, 7, 8}; // a, b, c, d, e, f, gconst int BTN = 9;const int BUZZ = 8;const int N = 7; byte CIF[10] = { 0b0111111, 0b0000110, 0b1011011, 0b1001111, 0b1100110, 0b1101101, 0b1111101, 0b0000111, 0b1111111, 0b1101111}; int cifra = 9;int go = 0; // 0 - стоит, 1 - идётint fin = 0; // флаг финального бипаint longFired = 0; // сброс по длинному нажатию уже сработалint prevBtn = HIGH;unsigned long tTick = 0;unsigned long tPress = 0;unsigned long tFin = 0; void showDigit(int n) { for (int i = 0; i < N; i++) { if ((CIF[n] >> i) & 1) { digitalWrite(SEG[i], HIGH); } else { digitalWrite(SEG[i], LOW); } }} void setup() { for (int i = 0; i < N; i++) pinMode(SEG[i], OUTPUT); pinMode(BTN, INPUT_PULLUP); pinMode(BUZZ, OUTPUT); Serial.begin(9600); showDigit(cifra); Serial.println("Tajmer 9-0: start D9, sbros - derzhat");} void loop() { int btn = digitalRead(BTN); // фронт нажатия: запомнить момент if (btn == LOW && prevBtn == HIGH) { tPress = millis(); longFired = 0; } // удержание 900 мс: сброс на 9, срабатывает один раз if (btn == LOW && prevBtn == LOW && longFired == 0 && millis() - tPress >= 900) { longFired = 1; go = 0; fin = 0; noTone(BUZZ); cifra = 9; showDigit(cifra); Serial.println("sbras na 9"); } // отпускание без сброса: короткое нажатие - старт/пауза if (btn == HIGH && prevBtn == LOW && longFired == 0) { go = 1 - go; tTick = millis(); Serial.print("go="); Serial.println(go); } prevBtn = btn; // тик раз в секунду if (go == 1 && millis() - tTick >= 1000) { tTick = millis(); if (cifra > 0) { cifra = cifra - 1; showDigit(cifra); Serial.print("cifra="); Serial.println(cifra); if (cifra == 0) { go = 0; fin = 1; tFin = millis(); tone(BUZZ, 1000); Serial.println("finish"); } } } // финальный бип: 600 мс и тишина if (fin == 1 && millis() - tFin >= 600) { noTone(BUZZ); fin = 0; } delay(5);}Разберу пару мест, где таймер отличается от счётчика. Первое — кнопка: здесь одно нажатие таит два разных действия, и короткое ловится не на фронте, а на отпускании. Фронт лишь запоминает момент нажатия в tPress; если палец держит кнопку дольше 900 мс — срабатывает сброс и флаг longFired закрывает короткое действие; если отпустил раньше — это старт или пауза. Без этой развязки каждое длинное нажатие начиналось бы со случайного старта, и таймер дёргался бы на каждом сбросе.
Второе — финал. Когда тик опускает цифру до нуля, скетч сам останавливает отсчёт, ставит флаг fin и включает tone(BUZZ, 1000) — тон на 1000 герц. Через 600 мс отдельная ветка гасит его noTone. Разнесение «включить» и «выключить» по двум веткам — приём из планировщика: тон включается один раз, а loop() продолжает крутиться и слушать кнопку даже во время сигнала. Частоту можно поднять до 2000 герц — на пьезокерамике зона 2-4 кГц звучит громче за счёт резонанса корпуса, я разбирал это в уроке про тона и частоты; 1000 герц взяты как середина слышимости, на которой сигнал не режет ухо в маленькой кухне.
Третья мелочь — delay(5) в самом низу loop(). Это не блокирующая пауза в смысле delay(1000): пять миллисекунд — передышка движку симулятора и порогу срабатывания для глаза, а против окна нажатия в 100-150 мс она незаметна. Уберите её совсем — скетч тоже будет работать; оставите — Serial-журнал перестанет заливаться сплошным потоком.
Длинное нажатие и поведение после финиша
Границу 900 мс для длинного нажатия я подобрал не по даташиту, а по руке: короче — обычные нажатия иногда заезжают в сброс, длиннее секунды — сброс ощущается задумчивым. Полсекунды мало, 900 мс хватает, чтобы палец понял, что это отдельное действие, а не клик. Тот же приём используется в зажигалках с блокировкой и в кнопках стиральных машин: одна деталь, два действия, разделены временем удержания, а не второй кнопкой.
Флаг longFired нужен потому, что условие удержания истинно каждый проход loop(), пока кнопка зажата: без флага сброс срабатывал бы раз за разом каждые пять миллисекунд, и Serial-журнал заливало бы строками sbras na 9. Флаг поднимается при первом срабатывании и сбрасывается только на следующем фронте нажатия — стандартная развязка «одно действие на одно событие», та же, что у фронта кнопки в счётчике. Если держать кнопку с самого старта, сброс сработает раньше, чем вы отпустите палец, — таймер вернётся на девятку, так и задумано.
После финиша таймер ведёт себя по определению: цифра 0, счёт остановлен, и короткое нажатие стартует отсчёт… который тут же закончится, потому что тик защищён if (cifra > 0). Перезапуск живёт в длинном нажатии: сброс на 9 — и можно стартовать заново. Это осознанный контракт, а не поломка: после сигнала таймер обязан показать, что он выслужился, и не стартовать отсчёт от нуля по случайному клику.
В симуляторе собран полный таймер: индикатор на D12-D6, кнопка на D2, зуммер на D4 — макетка раскладывает компоненты по первым свободным пинам, поэтому номера в sim-скетче отличаются от платы. Держите кнопку зажатой секунду — увидите сброс; короткий клик — старт и пауза. В мониторе идёт последовательность cifra=8, cifra=7… finish — по ней удобно сверять ритм тика и ловить момент, когда пауза съела тик. Мой тест после сборки: досчитать до нуля дважды подряд и сравнить длину журнала — строк должно выйти поровну, иначе где-то тик слипся с паузой.
Куда расти дальше: один разряд умеет только от 9 до 0, а кухонному таймеру нужны десятки и сотни секунд. Прямое управление вторым разрядом съест ещё семь пинов и упрётся в бюджет Uno, поэтому индустрия ушла в мультиплексирование: разряды вспыхивают по очереди на пару миллисекунд, полный цикл короче 20 мс, и глаз видит все разряды горящими одновременно. Готовые модули с драйвером TM1637 закрывают тему двумя проводами: четырёхразрядный индикатор я подключал в уроке про TM1637, и сегментные паттерны из этого кластера переносятся туда один в один — шрифт тот же, меняется только транспорт.
Зажигайте сегменты по одному биту и собирайте цифры 0–9.
Подгружаем интерактив…
- Таймер — автомат из трёх состояний: стоит, идёт (тик 1000 мс на millis), финал; loop() без длинных delay остаётся живым
- Тик — гейт if (go == 1 && millis() - tTick >= 1000) с защитой if (cifra > 0) от ухода за ноль
- Одна кнопка таит два действия: короткое ловится по отпусканию (старт/пауза), удержание 900 мс — сброс на 9 с одноразовым флагом longFired
- Финал: tone(BUZZ, 1000) включается один раз, через 600 мс отдельная ветка глушит его noTone — сигнал не блокирует опрос кнопки
- После финиша отсчёт от нуля невозможен: перезапуск только сбросом; следующий уровень — мультиплексирование разрядов или TM1637
- 1. Почему короткое нажатие ловится по отпусканию, а не по нажатию?
- 2. Какой порог отделяет сброс от короткого нажатия в скетче урока?
- 3. Что делает скетч, когда tTick состарился на 1000 мс, но cifra уже равна 0?
- 4. Почему финальный сигнал не мешает кнопке работать?
- 5. Зачем второму разряду нужно мультиплексирование или TM1637?
Частые вопросы по теме урока
- Почему короткое нажатие ловится по отпусканию, а не по нажатию?
- На фронте нажатия ещё неизвестно, будет ли это клик или сброс. Фронт только запоминает момент; действие — на отпускании, если флаг longFired не поднялся. чтобы длинный сброс не начинался со случайного старта: решение принимается, когда ясно, сколько держали
- Какой порог отделяет сброс от короткого нажатия в скетче урока?
- Удержание от 900 мс считается сбросом: полсекунды мало — обычные клики заезжают в сброс, больше секунды — сброс ощущается задумчивым. Флаг longFired делает сброс одноразовым. 900 мс
- Что делает скетч, когда tTick состарился на 1000 мс, но cifra уже равна 0?
- Гейт if (cifra > 0) внутри тика отсекает уменьшение на нуле. Дальше нуля счёт не идёт, мусор из-за границы массива не читается. тик пропускается защитой if (cifra > 0) — счёт не уходит за ноль
- Почему финальный сигнал не мешает кнопке работать?
- Включение tone — одна команда без пауз. Отбой noTone происходит через 600 мс в отдельной ветке на millis, поэтому loop() крутится и кнопка жива всё время сигнала. tone включается один вызов, а гашение через 600 мс делает отдельная ветка — loop() продолжает опрашивать кнопку
- Зачем второму разряду нужно мультиплексирование или TM1637?
- Каждый разряд — ещё семь пинов. Два разряда — четырнадцать, и прямому управлению нечем дышать. Мультиплексирование делит одни сегменты между разрядами (цикл короче 20 мс), TM1637 выносит работу в драйвер на двух проводах. прямое управление ещё семью сегментами съест свободные пины Uno, поэтому разряды переключают по очереди или ставят драйвер