Дозатор корма: шесть порций по кругу, шторка ходит 0-30-60-90-120-150 и снова в ноль. Кнопка одна — вторая рука занята поводком. Скетч я написал за пять минут: нажата — плюс тридцать. На столе работало, в жизни — нет: одно нажатие перескакивало на 60°, следующее на 90°. Контакт кнопки в момент смыкания звенит 5-20 мс, а loop без пауз пробегает на Uno десятки тысяч раз в секунду — каждый звон засчитывался как отдельный шаг.
Второй слой та же сцена прятала глубже: команда write() возвращается мгновенно, а механика едет. Паспортная скорость SG90 — 0,1 с на 60°, значит, шаг в 30° занимает около 50 мс даже без нагрузки, а с рычагом и кормом — все 100-150. Принимать шаги во время движения — значит позволить счётчику позиций уехать от реальной шторки: «нажал три раза — стоит на третьей порции» перестаёт быть правдой.
План урока: фронт вместо уровня, повторный шаг только после отпускания, дребезг гасится окном на millis() без единого delay, кольцо из шести позиций одним сравнением. Двухкнопочный вариант со шагами ±15° разобран в уроке про шаги кнопками — там же таблица выбора величины шага; здесь замкнутый цикл на одной кнопке.
Почему кольцо, а не туда-обратно: дозатору неважно, откуда начинать — важно, что порции равные, а после шестой снова первая. Кнопка-кольцо экономит вторую кнопку и половину хода: из 150° в ноль шторка идёт назад напрямую, не через 180°. Для меню и переключателей режимов кольцо тоже честнее: после последнего пункта — первый, без бесконечного возврата.
Кольцо и фронт нажатия
Кольцо делается одним сравнением: после прибавления тридцати проверяем порог — больше 150, значит, начинаем с нуля. Шесть позиций замыкаются в круг честно: 150 плюс 30 — это 180, мы его отбрасываем к нулю. Остаток от деления тут соблазнителен, но с явным порогом скетч читается без математики и не разваливается, когда шаг перестаёт быть делителем шкалы. Шесть позиций по 30° — не прихоть: крупный шаг ощущается как щелчок, а мелкие 5-10° при дребезге сливаются в кашу даже с фронтом. Сравнение с порогом одинаково работает для шагов 15, 30 и 45 — меняется одна константа, кольцо пересчитывается само.
Фронт: держим прошлое состояние кнопки и шагаем только на переходе из отпущенной в нажатую. Одного фронта мало — дрожащий палец даёт микропаузы на удержании, и каждая ловится как новый фронт. Поэтому armed: шаг засчитан — флаг падает, и новый шаг разрешается только после отпускания кнопки. Пара фронт плюс armed даёт ровно один шаг за физическое нажатие, сколько бы loop ни пробежал. У тумблеров и микровыключателей звон есть на обоих краях — про отпускание забывают чаще, а там тоже фронт, только обратный.
Окно на millis вместо delay
Классика с delay(250) после шага душит звон — и весь остальной скетч вместе с ним: на паузе стоят опрос датчиков, Serial, таймеры. Окно на millis() работает иначе: скетч запоминает момент шага и первые 30 мс игнорирует любые переходы кнопки, не останавливаясь. Методика без задержек по шагам разобрана в уроке про millis вместо delay.
Числа под условие: звон контакта укладывается в 5-20 мс — окно 30 мс перекрывает его; ход на 30° занимает около 50 мс по паспорту — страховку я ставлю в 150 мс после шага, чтобы счётчик не обгонял механику даже на тугом рычаге. Оба окна считаются от одного millis() и живут в двух переменных — никакой магии, только два сравнения времени. Меняйте числа под свой механизм: окно длиннее — надёжнее для убитых кнопок, страховка больше — спокойнее для тяжёлого рычага; константы для этого и живут в двух строчках.
Если фронтов станет много, а опрос начнёт спотыкаться, тот же фронт ловится прерыванием на пинах 2 и 3 — про attachInterrupt был отдельный урок про прерывания. Я под кнопки отдаю именно эти два пина с самого начала: переезд на прерывания тогда не требует ни перепайки макетки, ни смены пинов.
Аппаратный вариант — RC-цепочка: 10 кОм подтяжки плюс 0,1 мкФ параллельно кнопке режет звон ещё до пина. На макетке я её не ставлю — конденсатор сглаживает и быстрые честные нажатия, а программное окно настраивается за минуту. Но в готовом устройстве, где кнопка уехала на панель метром провода, пара деталей у входа помогает лучше любых окон.
#include <Servo.h> Servo srv; int poziciya = 0; // 0, 30, 60, 90, 120, 150 — и снова нольint lastKnopka = 1; // прошлое состояние кнопки: 1 = отпущенаint armed = 1; // новый шаг разрешён после отпусканияlong tFronta = 0; // момент последнего шагаlong tShaga = 0; // для страховки на ход механики void setup() { srv.attach(11); pinMode(2, INPUT_PULLUP); srv.write(poziciya); Serial.begin(9600); Serial.println("Кнопка D2: шаг 30 градусов, кольцо 0-150");} void loop() { int knopka = digitalRead(2); long now = millis(); if (knopka == 0 && lastKnopka == 1 && armed == 1 && now - tFronta > 30) { poziciya = poziciya + 30; if (poziciya > 150) { poziciya = 0; // кольцо замкнулось } srv.write(poziciya); armed = 0; // ждём отпускания tFronta = now; // окно дребезга 30 мс tShaga = now; // страховка на ход 150 мс Serial.print("Позиция "); Serial.println(poziciya); } if (armed == 0 && knopka == 1 && now - tShaga > 150) { armed = 1; // палец убрали, механика доехала } lastKnopka = knopka; delay(10);}Разбор: armed стартует в единице, после шага падает и поднимается только когда кнопка вернулась в отпущенное состояние и прошло 150 мс от шага. Окно дребезга — 30 мс от момента шага; на убитых кнопках звон длиннее — окно растягивается до 50, счётчик позиций этого не заметит. Проверка на вшивость: десять нажатий — десять строк в мониторе, по одной позиции каждая, после 150 идёт ноль. Тот же скелет переносится на две кнопки: фронтов две, armed общий, кольцо то же.
И напоминание из первого урока кластера: каждый шаг — бросок тока до 650-700 мА. Одна кнопка и одно серво от USB живут спокойно, но частые щелчки подряд на тяжёлом рычаге уже просаживают шину — при паре моторов берите отдельный блок и общую землю. Про подтяжку: pinMode(2, INPUT_PULLUP) включает внутренний резистор платы, внешних деталей у кнопки нет; на длинных проводах ставят внешнюю подтяжку 10 кОм — об этом писал урок про кнопку и digitalRead.
Скорость нажатий — ещё один потолок: armed поднимается через 150 мс после шага, значит, максимум — пять-шесть шагов в секунду. Дозатору хватает с запасом, а игре на реакцию — нет: там серво меняют на светодиод, а кольцо — на счётчик миллисекунд.
| Реакция на кнопку | Что с дребезгом и циклом | Где применять |
|---|---|---|
| digitalRead + delay(250) после шага | Звон задавлен, но loop заморожен: датчики и Serial стоят | Разовая кнопка без соседних задач |
| Фронт + armed по отпусканию + окно 30 мс на millis | Один шаг за нажатие, loop крутится без пауз | Мой выбор для серво-проектов |
| attachInterrupt(FALLING) на D2 | Фронт ловится даже во время длинных delay | Когда loop перегружен и опрос спотыкается |
Куда расти: тумблер на два положения делает выбор без дребезга вообще — у него фиксация механическая, а не пружинкой (урок про переключатель); джойстик заменяет щелчки непрерывной ручкой (урок про джойстик и серво); ИК-пульт добавляет кнопкам дистанцию (урок про ИК-пульт). Кольцо из шести позиций при этом не меняется — меняется только источник фронтов.
Схема «счётчик позиций в переменной» масштабируется на что угодно: меню из четырёх положений, полив по дням, диск выбора режимов. Держите счётчик и реальный угол рядом — печатайте обе величины одной строкой в Serial и при шаге, и при старте: после перезапуска платы сразу видно, с чего скетч начал. Рассинхрон поймается в первую же неделю, а не когда дозатор недольёт корма.
Про симулятор — честно: кнопка там идеальна, звон контакта не смоделировать, и окно 30 мс кажется лишним. На реальной кнопке дребезг вернётся, и окно станет не украшением, а защитой; подержите кнопку зажатой — шаги не пойдут, пока не отпустите, и это ровно поведение armed. Бросков тока симулятор тоже не показывает: на живой макетке с USB частые щелчки подряд — верный способ увидеть перезагрузку из первого урока кластера.
- 1. Одно нажатие даёт два шага по 30°. Чего не хватило скетчу?
- 2. Паспортная скорость SG90 — 0,1 с на 60°. Сколько едет шаг на 30°?
- 3. Зачем armed по отпусканию, если фронт уже стоит?
- 4. Чем плох delay(250) после шага в проекте с датчиками?
- 5. После 150° кнопка нажата ещё раз. Какая позиция?
- Контакт кнопки звенит 5-20 мс, а loop пробегает десятки тысяч раз в секунду — без фронта каждый звон стоит шага
- Кольцо из шести позиций — одно сравнение: больше 150, значит, ноль
- Фронт плюс armed по отпусканию дают ровно один шаг за физическое нажатие
- Окно дребезга 30 мс на millis() работает без заморозки loop; страховка на ход механики — 150 мс
- Пины 2 и 3 под кнопки оставляйте с заделом на прерывания — переезд без перепайки
Частые вопросы по теме урока
- Одно нажатие даёт два шага по 30°. Чего не хватило скетчу?
- Без фронта каждый проход loop с нажатой кнопкой — новый шаг, питание тут ни при чём: серво честно исполняет каждую команду. Дребезг добавляет лишние переходы: нужна пара фронт плюс armed. Фронта: шаг ловился на уровне «нажата», плюс дребезг
- Паспортная скорость SG90 — 0,1 с на 60°. Сколько едет шаг на 30°?
- write() возвращается мгновенно, а механике нужно время: 30° — это примерно половина от 0,1 с на 60°. Под нагрузкой — в два-три раза дольше. Поэтому страховка 150 мс, а не 50. Около 50 мс без нагрузки
- Зачем armed по отпусканию, если фронт уже стоит?
- Фронт отсекает автоповтор, но не дрожание пальца. Armed плюс окно — два дешёвых сравнения времени. Дрожащий палец даёт микропаузы на удержании — ложные фронты; повторный шаг разрешается только после отпускания
- Чем плох delay(250) после шага в проекте с датчиками?
- delay() замораживает всё подряд. Окно на millis() гасит дребезг так же надёжно, но loop продолжает крутиться. На паузе стоит весь loop: опрос датчиков и Serial не работают
- После 150° кнопка нажата ещё раз. Какая позиция?
- 150 плюс 30 — это 180, порог больше 150 срабатывает, позиция сбрасывается в ноль. Шесть нажатий возвращают шторку в начало — полный круг. 0 — кольцо замкнулось