Поворотную платформу для телефона я собирал под созвоны: коллеги устали смотреть в потолок, когда я уезжал с рабочего места вместе с ноутбуком. Задумалось две кнопки на корпусе — плюс десять градусов, минус десять — и никаких Serial-команд в моменте. Схема привычная: 28BYJ-48 за ULN2003, кнопки на D2 и D3.
Первый скетч был наивный: digitalRead увидел LOW — прибавь 57 шагов и поезжай. Короткое нажатие работало идеально. Но я удержал кнопку на секунду — и платформа уехала на полсотни градусов: за эту секунду loop() успел обернуться несколько раз, и каждый оборот был посчитан за новое нажатие. Вторая попытка с delay() после хода вылечила повторы, но кнопка оглохла: быстрое двойное нажатие ловилось через раз, а весь скетч спал вместе с delay.
Разберём, как считать нажатие строго один раз — по фронту с антидребезгом на millis() — и сколько шагов прячется в десяти градусах: там лежит дробь, которая то копится, то исчезает, в зависимости от того, куда едет механизм.
Фронт кнопки: одно нажатие — один ход
Уровень на пине — не нажатие. Пока палец держит кнопку, уровень LOW висит сколько угодно: сто миллисекунд, секунда, минута. Если в loop() сравнивать только текущий уровень, любое удержание — это серия ходов. Ход должен запускать переход: прошлый цикл видел HIGH, текущий видит LOW. Одна переменная прошлого состояния на кнопку — и удержание перестаёт плодить события.
Фронт сам по себе не спасает от дребезга: в момент замыкания контакта за первую миллисекунду HIGH и LOW сменяются несколько раз, и каждый провал выглядит новым фронтом. После сработавшего фронта я держу блокировку на millis() — сто двадцать миллисекунд хватает и механике успокоиться, и двойное нажатие темпом быстрее восьми в секунду остаётся живым. Механику дребезга и другие способы лечения мы разбирали в уроке про антидребезг, а если вы ещё не дружите с digitalRead — начните с первой кнопки.
Обе кнопки живут по одному рецепту, но у каждой своя переменная прошлого состояния, а блокировка общая: если кнопка плюса запустила ход, минус подождёт — два хода в разные стороны не должны столкнуться внутри библиотеки. И держите опрос честным: без тяжёлой работы в loop() фронт ловит даже быстрое касание на пятьдесят миллисекунд; цикл длиннее ста миллисекунд уже способен проглотить короткое нажатие, поэтому всё медленное из loop() выносят.
Теперь арифметика хода. Формулу шагов из угла мы вывели в уроке про 2048 шагов: шаги = угол × 2048 / 360. На один градус приходится 5,689 шага, и десяти градусов она даёт 56,89. step() принимает целое, значит, число надо округлить: отсечение даст 56, округление к ближайшему — 57. Я ставлю 57: промах одной команды всего плюс 0,11 шага, у отсечения — минус 0,89, почти целый шаг в минус на каждом ходе.
Почему это не мелочь: ошибки складываются, пока команды идут в одну сторону. Сто односторонних команд плюс десять: при 56 шагах платформа недоедет 89 шагов — минус 15,6 градуса; при 57 — переедет 11 шагов, плюс 1,9 градуса. Когда команды ходят туда-обратно, погрешности гасят друг друга, и это ещё одна причина проектировать интерфейс симметричным. Правило у меня такое: округляй к ближайшему и помни, в какую сторону едет статистика твоих команд.
И вторая половина аккуратности: счётчик позиции держите в шагах, а не в градусах. Градусы — производная для печати; если хранить угол и пересчитывать его в шаги на каждой команде, дробь будет округляться заново каждый раз и уползать. Шаги — язык мотора, градусы — язык человека, и конвертация идёт только в момент отчёта.
С минусом всё симметрично, если считать правильно: беру модуль угла, округляю, знак прикладываю к готовому числу — минус десять даёт те же 57 шагов в обратную сторону с тем же промахом плюс 0,11. Если же округлять само отрицательное число отсечением, минус 56,89 превратится в минус 56 — на восьмь шагов за сотню команд хуже в сравнении с симметричным вариантом. Один раз заведите функцию пересчёта с честной обработкой знака и не вспоминайте о ней.
| Угол | Точно шагов | Округляем | Промах команды |
|---|---|---|---|
| 1° | 5,69 | 6 | плюс 0,06° |
| 10° | 56,89 | 57 | плюс 0,02° |
| 45° | 256 | 256 ровно | нет |
| 90° | 512 | 512 ровно | нет |
#include <Stepper.h> const int STEPS_PER_REV = 2048;const int DEG10_STEPS = 57; // 10 x 2048 / 360 = 56,89 -> 57 Stepper stepper(STEPS_PER_REV, 8, 10, 9, 11); const int BTN_PLUS = 2; // плюс десятьconst int BTN_MINUS = 3; // минус десять long pos = 0; // позиция в шагах: longint lastPlus = HIGH;int lastMinus = HIGH;unsigned long lockUntil = 0; void setup() { stepper.setSpeed(10); pinMode(BTN_PLUS, INPUT_PULLUP); pinMode(BTN_MINUS, INPUT_PULLUP); Serial.begin(9600);} void loop() { if (millis() < lockUntil) { return; } int nowPlus = digitalRead(BTN_PLUS); int nowMinus = digitalRead(BTN_MINUS); if (lastPlus == HIGH && nowPlus == LOW) { moveBy(DEG10_STEPS); } if (lastMinus == HIGH && nowMinus == LOW) { moveBy(-DEG10_STEPS); } lastPlus = nowPlus; lastMinus = nowMinus;} void moveBy(long delta) { stepper.step(delta); pos += delta; Serial.print("pos="); Serial.print(pos); Serial.print(" deg="); Serial.println(pos * 360 / 2048.0, 1); lockUntil = millis() + 120; // антидребезг после хода}Скетч читается слоями. Фронт — пара переменных last и сравнение прошло-сейчас. Блокировка на сто двадцать миллисекунд после хода — антидребезг: глушит и звон контакта, и повтор, если палец подрагивает. Функция moveBy — единственное место, где живёт ход и счётчик: одно нажатие — одна строка отчёта, и искать расхождение между кнопкой и позицией больше негде.
Печать угла — отдельная граблина. pos × 360 в int лопается уже при 91 шаге: произведение 32 760 стоит у предела 32 767, на следующем шаге оно тихо уходит в минус. Поэтому счётчик у меня long — до двух миллиардов, — а делитель записан с точкой, 2048.0: точка заставляет компилятор считать в дробях, и угол печатается с одним знаком после запятой. Подробно про типы у нас был разбор переменных.
Во время хода кнопки молчат: step(57) при 10 RPM занимает 167 миллисекунд, и loop() всё это время внутри библиотеки, как мы замеряли. Для кнопочного интерфейса это терпимо — ход короче реакции пальца; движение длиннее секунды я бы уже дробил порциями, как в уроке про ручку и вал.
Отчёт после хода — не украшение, а инструмент отладки. Позиция печатается в шагах, угол — в градусах, и когда через полгода вы поменяете 57 на 58, Serial сразу покажет, что счётчик едет по другой шкале. Строка отчёта — символов десять, на 9600 бод это около десяти миллисекунд эфира после каждого хода: ничего по сравнению со 167 миллисекундами самого хода. Я вешаю такую печать на каждый ход во всех моторных проектах — она окупается первой же поимкой расхождения.
И про ту самую строку с ранним выходом: if (millis() < lockUntil) — единственное место, где loop() коротко возвращается. Без неё во время блокировки кнопки продолжали бы читаться, а ход — запускаться повторно; с ней во время паузы скетч просто пропускает оборот. Альтернатива — гасить фронт условием внутри каждой кнопки, но это две копии одной мысли: общий замок чище, и его всегда видно в начале функции.
Проверяем в симуляторе
В симуляторе две кнопки сидят на D2 и D3, как в настоящей схеме, мотор — на пинах 8, 10, 9, 11 с циферблатом. Нажмите кнопку плюса: вал проедет 57 шагов — это 10,02 градуса — и Serial напечатает pos= и deg=. Теперь держите кнопку зажатой: ход будет один, фронт срабатывает на переход, а не на уровень.
Дальше — проверка арифметики на статистике. Пять плюсов и пять минусов: счётчик вернётся в ноль, промахи погасятся. Десять плюсов подряд: 570 шагов, то есть 100,2 градуса против обещанных ста — одиннадцать лишних шагов на сотню команд, ровно как обещала арифметика округления.
И последняя проверка — на дребезг: нажмите и подержите, потом щёлкните коротко, затем два раза быстро. Один ход за удержание, один за щелчок, два за пару — если где-то вылезло лишнее, увеличивайте блокировку со ста двадцати до двухсот миллисекунд, но не дальше: упрётесь в ощущение отстающей кнопки.
- 1. Сколько шагов вала 28BYJ-48 приходится на один градус?
- 2. Почему удержание кнопки в первом скетче давало многократные ходы?
- 3. 10 × 2048 / 360 = 56,89. Что выберет округление к ближайшему?
- 4. Что напечатает Serial при pos × 360 в int, когда pos равен 200?
- 5. Где держать счётчик позиции кнопочного механизма?
- Одно нажатие — один ход: фронт HIGH-LOW плюс блокировка 120 мс на millis()
- 10° = 56,89 шага; округляйте к ближайшему — 57 шагов, промах команды плюс 0,11 шага
- Односторонние команды копят округление: сотня ходов по 56 шагов — минус 15,6°, по 57 — плюс 1,9°
- Счётчик позиции — в шагах и в long: pos × 360 в int ломается уже при 91 шаге
- step(57) при 10 RPM — 167 мс молчания loop(): для кнопок терпимо, длинные ходы дробите порциями
Кнопки дали точечные ходы, но по мере роста скетча я таскал одну и ту же тройку строк — пересчёт, шаг, счётчик — по всему коду, пока одна копия не поехала не туда. Лечится это своими функциями: rotateDeg по градусам, moveTo по абсолютной отметке и возврат в ноль. А ещё в третьем уроке разберём, почему счётчик позиции обязан быть long — с числами из собственной съёмки.
Частые вопросы по теме урока
- Сколько шагов вала 28BYJ-48 приходится на один градус?
- 2048 шагов на оборот делим на 360 градусов — 5,689 шага на градус. Дробь заставляет округлять каждую команду в целое число шагов. Около 5,69
- Почему удержание кнопки в первом скетче давало многократные ходы?
- Уровень LOW держится всё удержание, и каждая итерация loop() считалась за новое нажатие. Ход должен запускать переход HIGH-LOW, а не уровень; за уровнем ещё и дребезг прячется. loop() видел LOW на каждой итерации — нужен фронт
- 10 × 2048 / 360 = 56,89. Что выберет округление к ближайшему?
- 57 шагов: промах команды плюс 0,11 шага против минус 0,89 у отсечения. На односторонней серии из сотни команд разница — одиннадцать шагов против восьмидесяти девяти. 57
- Что напечатает Serial при pos × 360 в int, когда pos равен 200?
- 200 × 360 = 72 000 — за пределом int в 32 767. Компилятор не ругается, значение тихо переворачивается. Лечение — long у счётчика. Мусор: произведение уходит за 32 767
- Где держать счётчик позиции кнопочного механизма?
- Хранение в градусах заставляет пересчитывать дробь в шаги на каждой команде, и ошибка округления уползает в одну сторону. Шаги хранятся точно, градусы — производная для отчёта. В шагах: это язык мотора, градусы — только для печати