Указатель на шкафчике мастерской показывал, что внутри: рычаг влево — «деталей нет», вправо — «партия готова», промежуточные позиции — по числу коробок. Кнопки напрашивались сами: одна — шаг влево, другая — шаг вправо. Скетч я написал за пять минут и внутренне похвалил себя за простоту: digitalRead в loop, кнопка нажата — плюс пятнадцать градусов. Реальность оказалась другом: одно нажатие улетало в край шкалы за долю секунды. loop на Uno без пауз пробегает десятки тысяч раз в секунду, и всё время, пока палец лежит на кнопке, она честно докладывает «нажата». Серво исполнила не один приказ, а пару тысяч — и вжималась в упор.
Через этот скол мы и пройдём весь урок. Научимся отдавать команду на фронт — в момент перемены состояния кнопки, а не в каждом проходе loop; поставим ход в вилку constrain'ом; задушим дребезг короткой паузой после фронта; и подберём шаг так, чтобы весь ход шкалы занимал столько нажатий, сколько руке приятно. Никакой магии в финальном скетче нет: он короче этой абзаца — вся работа сидит в паре сравнений и двух переменных.
Почему кнопки, а не ручка из прошлого урока? Ручка даёт непрерывность, но требует взгляда: не глядя на макетку, вы не знаете, куда её накрутили. Кнопки работают вслепую и на ощупь: щёлк-щёлк — и стрелка стоит на делении, о котором вы знаете точно — на шаг от прежнего. Для панелей, шкатулок и всего, чем пользуются без монитора, это решающее преимущество; для всего остального — потенциометр из прошлого урока: он непрерывен там, где кнопки дискретны.
Фронт, а не уровень
У кнопки нет событий, есть только опрос: digitalRead() возвращает уровень прямо сейчас. Разница между «нажата» и «только что нажали» — это разница между уровнем и фронтом, и вся хитрость скетча ниже живёт в этой разнице. Для каждой кнопки заводим переменную прошлого состояния и на каждом проходе сравниваем: было 1, стало 0 — значит, в этот самый проход палец лёг на кнопку. Исполняем шаг и только шаг; на следующем проходе прошлое состояние уже 0, и совпадения с фронтом нет, сколько бы loop ни пробегал.
Числа для убедительности: простой loop с парой digitalRead() пробегает на Uno десятки тысяч раз в секунду; треть секунды удержания — это тысячи опросов и, без фронта, тысячи шагов. Фронт режет это до одного. Вторая напасть — дребезг: контакт в момент смыкания звенит 5-20 мс, и сырой вход успевает подмигнуть и 1, и 0. Пауза 150-250 мс сразу после зафиксированного фронта перекрывает и звон контакта, и время, пока палец уходит с кнопки. Канонический неблокирующий вариант на millis() мы разбирали в уроке про антидребезг — здесь я возьму паузу честно, а ниже объясню, почему для серво она не грех.
Про пины — привычка на вырост: под кнопки я всегда отдаю младшие цифровые 2 и 3, потому что они же умеют прерывания. Если однажды фронтов станет много и опрос перестанет успевать, переезд на attachInterrupt() случится без перепайки макетки и без смены пинов.
Проверка на вшивость делается монитором: десять нажатий подряд должны дать ровно десять строк «угол=» с приращением на один шаг каждая. Строк больше — вы ловите уровень или дребезг; строк меньше — часть фронтов теряется, обычно из-за паузы, съевшей следующий клик. Такой тест я прогоняю на каждой новой кнопке, прежде чем вшивать её в проект: пять секунд, а экономят вечер с осциллографом в руках.
#include <Servo.h> Servo srv;int ugol = 90; // стартуем с середины шкалыint shag = 15; // величина шага в градусахint lastPlus = 1; // прошлое состояние кнопки "плюс"int lastMinus = 1; // и "минус" void setup() { srv.attach(11); pinMode(2, INPUT_PULLUP); // минус: нажата = LOW pinMode(3, INPUT_PULLUP); // плюс: нажата = LOW srv.write(ugol); Serial.begin(9600); Serial.print("Старт, угол="); Serial.println(ugol);} void loop() { int plus = digitalRead(3); int minus = digitalRead(2); if (plus == 0 && lastPlus == 1) { // фронт: только что нажали ugol = constrain(ugol + shag, 0, 180); srv.write(ugol); Serial.print("угол="); Serial.println(ugol); delay(250); // глушим дребезг } if (minus == 0 && lastMinus == 1) { ugol = constrain(ugol - shag, 0, 180); srv.write(ugol); Serial.print("угол="); Serial.println(ugol); delay(250); } lastPlus = plus; lastMinus = minus;}Кнопки сидят на пинах 2 и 3 с INPUT_PULLUP: внутренняя подтяжка держит входы в единице, нажатие роняет их в ноль, и резисторов в схеме не нужно вовсе — вся обвязка из урока про digitalRead прячется внутрь микроконтроллера. Переменных прошлого состояния две, lastPlus и lastMinus; одна общая не справится: кнопки независимы, и фронт каждой надо ловить отдельно. Это первое место, где новички ужимают код до бага — не ужимайте.
constrain(ugol + shag, 0, 180) — обязательная деталь, а не украшение. Без него угол после последнего шага уехал бы в 195, а серво, получив приказ за вилкой хода, упёрлась бы в механический упор и завизжала — та самая болезнь, которую мы лечим отступами в соседних уроках и о питании при которой писал в уроке про провода и питание. Пауза 250 мс после фронта — сознательный компромисс: скетч на четверть секунды слепнет, но в этой схеме слепнуть нечему, кроме второй кнопки, а палец между нажатиями и так держит паузу. Появится рядом мигалка или дисплей — пауза переедет на millis() по канону из урока про millis вместо delay, каркас останется тем же.
Печать угла сидит внутри фронтовых веток не случайно: Serial показывает по строке на приказ, и монитор превращается в счётчик шагов. Пары имя=значение я пишу из привычки к Serial Plotter'у — они и глазами читаются быстрее, чем голые числа. Если однажды захотите видеть ход на графике, ничего переписывать не придётся: ugol=105 — это уже готовый именованный ряд. И не печатайте угол вне фронтовых веток: поток из тысяч одинаковых строк маскирует настоящие события — монитор должен щёлкать вместе с кнопкой.
| Шаг | Нажатий от 0 до 180 | Где уместен |
|---|---|---|
| 5° | 36 | тонкая подгонка: зеркало с транспортиром, макетные декорации |
| 10° | 18 | обычный ход: баланс точности и темпа |
| 15° | 12 | мой дефолт: весь ход за четыре секунды спокойной руки |
| 30° | 6 | крупная механика: заслонка, шлагбаум |
| 45° | 4 | режим «да-нет», где точность не нужна |
Шаг я выбираю делением, а не вкусом: шаг равен рабочему ходу в градусах, делённому на число позиций. Заслонке с положениями «приоткрыто» и «полностью» хватит 90 градусов за одно нажатие; указателю с двенадцатью делениями — 15 градусов; шкале тонкой подгонки — 5. Мой дефолт для «просто покрутить» — 15: двенадцать нажатий от края до края, рука не устаёт, и точность не страдает. Таблица выше — та же арифметика на весь диапазон 0-180, держите её рядом, пока не набьётся привычка.
Если захочется, чтобы при удержании серво шла сама, — это делается честно: фронт уже ловите, добавьте millis()-счётчик длительности удержания и каждые 300 мс прибавляйте ещё шаг, как автоповтор клавиатуры. Граница у приёма одна: механика не должна бояться рывков и не должна въезжать в упор без присмотра. А если точности шагов однажды не хватит — следующий уровень это шаговый двигатель, про старт с которым у меня отдельный урок: он делит оборот на сотни шагов и не путает «шаг» с «рычагом».
Не путайте шаг с темпом: шаг — это сколько градусов за нажатие, темп — как часто серво готова принять следующий приказ. Даже при удержании с автоповтором шаг оставляйте тем же, меняйте только интервал: иначе рука запомнит одни щелчки, а рычаг будет ездить по другим правилам.
Опыты для закрепления
Всё пробуется прямо в симуляторе выше. Первое: поставьте shag = 45, покрутите и верните 15 — разницу темпа рука запомнит лучше глаза. Второе: зажмите ход механики — замените границы constrain на 45 и 135, будто рычаг ходит в рамке; серво перестанет навещать края шкалы, где живут упоры. Третье: поменяйте пины кнопок местами в pinMode и в digitalRead — код не заметит подмены, а рука заметит: привычка к расположению кнопок сильнее любой переменной.
На живой плате добавится четвёртый опыт: после двадцати секунд бездействия пусть серво сама возвращается на 90 — нужен таймер на millis() и одно ветвление, а живой пример возврата к метке есть в уроке про sweep. И наблюдение от руки напоследок: чем больше шаг, тем заметнее, что серво доезжает за некоторое время, а не прыгает мгновенно — при лёгком рычаге 15-45 градусов занимают 100-300 мс. Не слать второй приказ, пока первый исполняется, — это уже половина хорошей механики, вторая половина ждёт вас в следующем уроке про переключатель. И привычка из жизни: подписывайте кнопки маркером — две одинаковые тактовые без подписей путаются в пальцах быстрее, чем в коде.
- 1. Почему без фронта одно нажатие давало десятки шагов?
- 2. Фронт нажатия при INPUT_PULLUP — это переход…
- 3. Что вернёт constrain(175 + 15, 0, 180)?
- 4. Шаг 15 градусов: сколько нажатий нужно от 0 до 180?
- 5. Зачем пауза 150-250 мс сразу после зафиксированного фронта?
- Команду отдаём на фронт — перемену состояния кнопки, а не на уровень
- У каждой кнопки своя пара переменных: текущее чтение и прошлое состояние
- constrain держит угол в вилке 0-180 — без него серво уходит в упоры
- Пауза 150-250 мс после фронта душит дребезг; в занятом скетче её делают на millis()
- Шаг равен ходу в градусах, делённому на число позиций; мой дефолт — 15 градусов
Частые вопросы по теме урока
- Почему без фронта одно нажатие давало десятки шагов?
- loop не ждёт: пока палец на кнопке, десятки тысяч опросов в секунду видят один и тот же уровень. Фронт — реакция на перемену состояния, и случается он один раз. loop пробегает тысячи раз в секунду, и каждый уровень «нажата» считался шагом
- Фронт нажатия при INPUT_PULLUP — это переход…
- Подтяжка держит вход в единице, нажатие роняет его в ноль. Значит, момент нажатия — переход из 1 в 0, и ловим мы пару «было 1, стало 0». из 1 в 0
- Что вернёт constrain(175 + 15, 0, 180)?
- constrain зажимает значение в вилку: 190 больше верхней границы, вернётся 180. Без него серво получила бы приказ за пределами хода и упёрлась бы в упор. 180
- Шаг 15 градусов: сколько нажатий нужно от 0 до 180?
- 180 делить на 15 — двенадцать нажатий. Отсюда и правило выбора шага: ход в градусах делить на число позиций, которые вам нужны. 12
- Зачем пауза 150-250 мс сразу после зафиксированного фронта?
- Контакт звенит 5-20 мс, и каждый звон выглядит как новый фронт. Пауза перекрывает звон и уход пальца; в занятых скетчах её делают на millis(). Чтобы заглушить дребезг контакта