После первого скетча мне понадобилась скорость: штора должна была открываться за четыре секунды, а не за десять. Я рассуждал по привычке от Stepper.h — там скорость в оборотах в минуту, и десятка хватало, — поэтому вписал setMaxSpeed(60), потом 600, потом 6000, двигаясь наугад: единицы были незнакомы. На 6000 мотор перестал двигаться вовсе — стоял и звенел, как струна, а я ещё и поднимал ускорение вслед за скоростью, усугубляя. Отдельная деталь той истории: четвёрку 6000 я взял не с потолка, а из чужого скетча для NEMA 17 с драйвером — для нашей связки с ULN2003 она бессмысленна вдвойне, но я этого ещё не знал.
Разобраться помогла бумажка с трапецией. Скорость шаговика — не ручка громкости, а профиль: подъём, плато, спуск. Библиотека не может бросить вал на полную скорость, если наклон задан злой, а нагрузка тяжёлая: ротор сорвётся и будет гудеть на месте. Сорванные шаги ничем не лечатся — мы это вымеряли в уроке про скорость и позиционирование: счёт продолжит идти, а вал не поедет, и никакой обратной связи мотор не пришлёт.
Здесь разложим трапецию на числа: что значат maxSpeed и acceleration, в каких они единицах, сколько времени и шагов съедает разгон и какие стартовые значения брать для 28BYJ-48. Единицы, время и путь — три числа, которые надо уметь считать на бумажке до заливки, а не после. Отдельная строка плана — run(): профиль живёт только пока run() вызывается в каждом проходе loop, и это правило свяжет урок со следующим, где мы выгоним из скетча последние блокировки.
Трапеция: подъём, плато, спуск
Сначала единицы. setMaxSpeed принимает шаги в секунду, setAcceleration — шаги в секунду за секунду. Ни RPM, ни градусов: библиотека считает в своих единицах, а режим конструктора определяет, что считать шагом. В HALF4WIRE шаг — полушаг, на оборот вала 28BYJ-48 приходится 4096 единиц, значит 500 шагов в секунду — это 500 × 60 / 4096, около 7,3 оборота в минуту. Тот же темп в полных шагах был бы 250. Из-за этого пересчёта половина жалоб «мотор едет медленно» решается не заменой мотора, а поправкой единиц. Если когда-нибудь перейдёте на режим DRIVER с A4988, единицы не изменятся: там шаг задаёт перемычка микрошага на драйвере, а библиотека по-прежнему считает в шагах.
Теперь цены разгона. Время разгона — потолок, делённый на наклон: 500 / 1000 = полсекунды. Путь разгона — квадрат скорости, делённый на удвоенный наклон: 500 × 500 / (2 × 1000) = 125 шагов, около одиннадцати градусов вала. Торможение зеркально. Суммарно маршрут в один оборот (4096 позиций) несёт 250 шагов разгона и торможения — две с половиной сотни позиций, которые нельзя забывать на коротких ходах: moveTo(200) при такой паре даже не успеет выйти на потолок. Обратная проверка: разгон до 1024 шагов в секунду при 2000 шаг/с² занял бы те же полсекунды, но 262 шага пути — трапеция честно масштабируется с обеими величинами.
run() в этой картине — насос. Каждый вызов сверяет часы, вычисляет, где вал должен находиться по трапеции, и делает максимум один шаг. Не вызвали — профиль замер: для библиотеки остановилось время, скорость не растёт и не падает. Поэтому рецепт «run() в каждом проходе loop» — не стилистика, а условие существования трапеции. Отсюда и способ отладки: если профиль выглядит странно, первым делом проверьте, что run() вызывается безусловно, а не под if, который иногда ложный. Долгий delay внутри loop разрывает профиль, и этим граблям посвящён третий урок.
| setMaxSpeed, шаг/с | setAcceleration, шаг/с² | Что происходит |
|---|---|---|
| 500 | 1000 | Рабочий старт для 28BYJ-48: разгон полсекунды, 125 шагов |
| 250 | 250 | Бережный режим для грузной шторки: разгон секунда, те же 125 шагов |
| 1000 | 4000 | Резко: вал трогается рывком, редуктор трещит, на старте возможен срыв |
| 1500 | 5000 | За пределом: выше 1000 шаг/с UNO не успевает, документация библиотеки предупреждает |
Стартовая пара 500 и 1000 — не догма, а рамка. Логика подбора двухходовая: мотор гудит на старте и не трогается — вал срывается от резкого наклона, режьте acceleration вдвое; едет, но в пути замирает с гулом — потолок высок, режьте maxSpeed. Мои 28BYJ-48 на 5 вольтах через ULN2003 живут до 600-800 шагов в секунду — дальше пластиковый редуктор начинает звенеть, а шаги пропадают; 500 оставляю как рабочий потолок с запасом. Верхняя граница для acceleration тоже есть: выше 3000-4000 шаг/с² старт всё равно упирается в трение редуктора, разница между 4000 и 8000 на глаз уже не читается. Грузной шторке, наоборот, беру 250 и 250: старт растягивается на секунду, и карниз не пылил.
Красивое совпадение: потолок мотора по оборотам — 15 об/мин, вымеренный в уроке про скорость, — это 1024 полушага в секунду, ровно рядом с порогом 1000 шагов в секунду, выше которого документация AccelStepper обещает пропуски на обычной плате. Два ограничения сходятся в одну точку с разных сторон: физика редуктора и темп пересчёта. Совпадение тут не мистика, а следствие того, что оба числа выросли из одного процессорного бюджета. Выше этой точки идут только с драйвером и мотором другого класса.
Пересчёт шагов в обороты удобно вести через передаточное число 2048: шаги в секунду делите на 4096 (полушаги) или на 2048 (полные шаги) и умножаете на шестьдесят. А анатомия мотора объясняет, почему разгон нужен в принципе: редуктор 1:64 с пластиковыми шестернями стронуть труднее, чем крутить, — статическое трение выше рабочего, и именно его трапеция преодолевает мягко, вместо рывка через срыв.
#include <AccelStepper.h> AccelStepper stepper(AccelStepper::HALF4WIRE, 8, 10, 9, 11); void setup() { stepper.setMaxSpeed(500); stepper.setAcceleration(1000); stepper.moveTo(4096); // один оборот: подъём, плато, спуск Serial.begin(9600);} void loop() { stepper.run(); static unsigned long lastPrint = 0; if (millis() - lastPrint >= 100) { lastPrint = millis(); Serial.print("spd="); Serial.print(stepper.speed(), 0); // текущая скорость трапеции, шаг/с Serial.print(" dst="); Serial.println(stepper.distanceToGo()); }}Залейте скетч на плату, откройте Serial-монитор и скиньте пары spd= в Serial Plotter: трапеция будет видна вживую — рост до 500 за полсекунды, плато, спуск к нулю у цели. Потом поменяйте acceleration со 1000 на 250 и сравните наклон: числа с бумажки превратятся в движущуюся линию, и спорить с формулой больше не захочется. Второй аргумент в Serial.print(stepper.speed(), 0) округляет скорость до целого — для графика этого хватает, дробная часть здесь шум.
В dst= читается форма маршрута. Пока до цели дальше, чем разгон с торможением вместе взятые, профиль — трапеция с плато. На коротких ходах вершина срезается: потолок не достигается вовсе, и это не сбой, а честная математика — moveTo(200) при паре 500/1000 превращается в треугольник с вершиной около 450 шагов в секунду и длится почти 0,9 секунды: ход выходит медленнее, чем подсказывает интуиция. Планируя механизм, держите это в голове: короткие ходы не выигрывают от высокого потолка, только от наклона.
И ещё раз про run() — теперь с числом. Каждому шагу предшествует пересчёт позиции по трапеции с плавающей точкой; на 16 МГц это десятки микросекунд, но при тысячах шагов в секунду проценты съедаются. Отсюда порог из документации: выше 1000 шагов в секунду пересчёт перестаёт успевать, шаги пропускаются, вал звенит. Потолок 500 оставляет запас вдвое — на печать, кнопки и остальную жизнь loop, о которой поговорим в третьем уроке. То же правило перенесите на подбор: нашли реальный потолок мотора — ставьте вдвое меньше, и loop останется время на жизнь.
Лестница скорости в симуляторе
- setMaxSpeed — шаги в секунду, setAcceleration — шаги в секунду за секунду; в HALF4WIRE шаг полушаговый, на оборот 4096 единиц
- Время разгона = maxSpeed / acceleration, путь = maxSpeed² / (2 × acceleration): при 500 и 1000 это 0,5 с и 125 шагов
- run() в каждом проходе loop — условие существования профиля: без вызовов трапеция замирает
- Стартовая пара для 28BYJ-48 — 500 и 1000; гудит на старте — режьте acceleration, замирает в пути — режьте maxSpeed
- Выше 1000 шаг/с UNO не успевает, а редуктор упирается в 15 об/мин: два потолка сходятся в одной точке
- 1. В каких единицах setMaxSpeed?
- 2. Сколько шагов займёт разгон до 500 шаг/с при acceleration 1000?
- 3. Где должен жить вызов run()?
- 4. Мотор гудит на старте и не трогается. Какое число крутить первым?
- 5. Что печатает spd= из скетча с профилем?
Профиль настроен, числа подобраны, график построен. Если хочется сравнить единицы со старыми скетчами — переводите шаги в секунду в обороты в минуту через 4096: привычная система координат помогает не путаться при чтении форумов. В словаре осталась одна запирающая функция — runToPosition: удобная, но наглухо закрывающая loop на весь переезд. Следующий урок раскладывает её рядом с run() по полочкам и собирает финальный скетч шторы: движение, кнопка и светодиод в одном цикле, без единого delay и без мёртвых секунд.
Частые вопросы по теме урока
- В каких единицах setMaxSpeed?
- Шаги в секунду, и не RPM: в HALF4WIRE шаг полушаговый, поэтому 500 шаг/с — это около 7,3 оборота в минуту, а не сотни. В шагах в секунду
- Сколько шагов займёт разгон до 500 шаг/с при acceleration 1000?
- Путь разгона: 500 × 500 / (2 × 1000) = 125 шагов, около 11° вала. Время — 500 / 1000, полсекунды. 125
- Где должен жить вызов run()?
- run() — насос профиля: каждый вызов делает максимум один шаг. Пропустили проходы loop — трапеция замерла, для библиотеки остановилось время. В каждом проходе loop()
- Мотор гудит на старте и не трогается. Какое число крутить первым?
- Гул на старте — срыв от резкого наклона: режьте acceleration вдвое. Замирание в пути лечится снижением maxSpeed, пины здесь ни при чём. Понизить acceleration
- Что печатает spd= из скетча с профилем?
- stepper.speed() возвращает текущую скорость профиля в шагах в секунду: на графике пары spd= рисуют трапецию — рост, плато, спуск. Текущую скорость трапеции от stepper.speed()