Разгон из прошлого урока я тащил через три проекта, а на четвёртом он сломался — не по-настоящему, а морально. Заслонке вентиляции добавили вторую команду: закрываться не до конца, а до половины, по показаниям датчика. В самодельной трапеции это означало пересчитать тормозной остаток на лету, не перепутать, какая половина профиля уже прожита, и не потерять счётчик разгона при развороте. Вечер возни с тремя переменными состояния — и осознание, что я пишу библиотеку. Плохую.
Библиотека AccelStepper уже была установлена и обкатана: HALF4WIRE для нашей связки с ULN2003, 4096 позиций на оборот против 2048 полных шагов из урока про передаточное число. Внешний слой мы разобрали в уроке про maxSpeed и acceleration: единицы — шаги в секунду, время и путь разгона считаются на бумажке, стартовая пара для 28BYJ-48 — 500 и 1000. Здесь спускаемся этажом ниже: что происходит внутри run() на каждом шаге, откуда берётся первый интервал и как библиотека узнаёт, что пора тормозить.
Заодно ответим на вопрос, который я задавал себе весь тот вечер: чем библиотечный разгон лучше моих сорока строк? Ответ не в цифрах скорости — он в том, кто хранит состояние движения. В самоделке это ваши переменные и ваша дисциплина, в библиотеке — автомат, которому достаточно цели. Проверим скетчем со сменой цели в полёте, таблицей честных сравнений и моделью автомата в симуляторе.
Автомат на одном счётчике
Главное слово библиотеки — run(): каждый вызов делает максимум один шаг и сразу возвращается, мы раскладывали это в уроке про неблокирующее движение. Но перед шагом run() пересчитывает интервал, и вот тут начинается интересное. В библиотеке живёт счётчик n — сколько шагов мотор уже сделал в текущем разгоне. Каждый шаг разгона этот счётчик растёт, а интервал, по которому живёт мотор, подрезается: скорость нарастает гладко, без ступеней самодельной дельты, потому что пересчёт идёт на каждом шаге, а не партиями по полсотни.
Первый шаг библиотека делает по интервалу, который выводится из ускорения. В исходнике формула выглядит как константа 0,676, умноженная на корень из двух, делённого на ускорение; при acceleration 1000 шаг/с² это около 30 миллисекунд на первый шаг — старт с тихой скорости порядка 33 шагов в секунду. Число 0,676 — эмпирическая поправка: авторы подобрали её, чтобы реальный путь разгона сходился с расчётным. Трогаться с тридцати шагов в секунду, а не с нуля и не с потолка — ровно то, что нужно пластиковому редуктору.
Торможение — самая красивая часть автомата. Библиотека в каждый момент знает остаток пути: distanceToGo() отдаёт его одной строкой. Как только остаток становится меньше разгонного счётчика n, счётчик начинает сдавать назад — интервал растёт шаг за шагом, и к цели мотор приезжает почти на нуле. Сообщать тормозной остаток руками не нужно: он вычисляется сам, и именно поэтому смена цели в полёте, которая сломала мне вечер, здесь стоит одну строку moveTo.
Теперь честное сравнение с самоделкой. Плавность: библиотека пересчитывает интервал на каждом шаге в дробной арифметике, мои ступени по пятьдесят шагов — компромисс ради читаемого кода. Торможение: у библиотеки автоматическое, у самоделки — константа nRamp, которую надо править при каждом изменении маршрута. Смена цели: moveTo перезаписывает её, и профиль пересобирается сам — обе половины, без вашей помощи.
Есть и то, чего у самоделки нет в принципе. Несколько моторов в одном скетче: второй объект AccelStepper живёт своими n и интервалом параллельно, в своём темпе. И режимы для драйверов: два пина STEP и DIR для A4988 и родни, где Stepper.h бессилен. Для станка или плоттера это не бонус, а входной билет.
И граница применимости, без которой я бы не поверил сам себе. Один мотор, маршрут фиксированный, ход занимает секунды — сорок строк самоделки из прошлого урока проживут в прошивке годами, и я их не переписываю. Библиотека берёт своё там, где движение становится живым: цели меняются на лету, моторов больше одного, профиль крутится параметрами. Цена — пара килобайт флеша и чужой словарь единиц: шаги в секунду, а не обороты в минуту.
#include <AccelStepper.h> // HALF4WIRE: полушаг через ULN2003, 4096 позиций на оборотAccelStepper stepper(AccelStepper::HALF4WIRE, 8, 10, 9, 11); const int BTN = 2; void setup() { stepper.setMaxSpeed(500); stepper.setAcceleration(1000); pinMode(BTN, INPUT_PULLUP); Serial.begin(9600); Serial.println("D2: eshe odna chetvert oborota v tu zhe storonu");} void loop() { static int lastBtn = HIGH; int nowBtn = digitalRead(BTN); if (lastBtn == HIGH && nowBtn == LOW) { // цель копится от текущей позиции: жать можно и во время хода stepper.moveTo(stepper.currentPosition() + 1024); Serial.print("dst="); Serial.println(stepper.distanceToGo()); } lastBtn = nowBtn; stepper.run(); static unsigned long lastPrint = 0; if (millis() - lastPrint >= 250) { lastPrint = millis(); Serial.print("spd="); Serial.print(stepper.speed(), 0); Serial.print(" dst="); Serial.println(stepper.distanceToGo()); }}Скетч начинается привычно: HALF4WIRE, потолок 500, наклон 1000 — стартовая пара, проверенная на шторке и столике. Кнопка D2 не запускает ход, а отодвигает цель: moveTo(currentPosition() + 1024) — четверть оборота в полушагах от того места, где вал сейчас. Нажали до старта — поедет сразу; нажали в пути — цель отодвинется, и автомат сам решит, продолжать разгон или уже тормозить по-новой.
Печать пар spd= и dst= раз в 250 миллисекунд я держу ради графика Serial Plotter: скорость рисует трапецию, остаток — лестницу вниз. Между печатями run() вызывается в каждом проходе — помните правило из неблокирующего урока: пропустили вызовы, для библиотеки остановилось время. Задержка печати на 9600 бод съедает двадцать миллисекунд, при 500 шагах в секунду это десятая часть плато — терпимо.
distanceToGo() в этой связке — счётчик остатка, на нём держится и диагностика, и условия остановки. Если spd= замерла на одном значении, а dst= не тает — значит, run() перестал вызываться, и искать надо delay или длинный if в loop(). Две величины из двух строк кода заменяют половину отладочных свистков.
| Задача | Самоделка из прошлого урока | AccelStepper |
|---|---|---|
| Плавный старт | линейка или дельта, ступени по 50 шагов | пересчёт интервала на каждом шаге, рост гладкий |
| Торможение к цели | константа nRamp и учёт остатка руками | остаток сверяется с разгонным счётчиком n автоматически |
| Смена цели в полёте | пересобрать обе половины профиля самому | moveTo перезаписал цель — профиль пересобрался сам |
| Цена | нуль библиотек, сорок строк кода | пара килобайт флеша и единицы в шагах в секунду |
Отладка профиля сводится к двум величинам из скетча. spd= должна расти от тридцати-сорока шагов в секунду к потолку и держаться на нём; dst= в середине пути тает почти линейно, а на краях — медленно. Если spd= стоит на одном значении с самого старта, профиль не живёт: ищите delay или длинный if, душащий run(). Если мотор трогается щелчком, как в прошлом уроке, — наклон великоват: режьте acceleration вдвое, с 1000 до 500, и путь разгона удвоится со 125 до 250 шагов.
Смена цели на лету меняет картину ожидаемым образом: если нажать кнопку на плато, spd= упадёт не с потолка, а с той скорости, где вал застал кнопку, — торможение началось раньше и с меньшей высоты. Это не странность, а честность автомата: он тормозит из текущего состояния, а не из идеального.
Отдельная осторожность — setCurrentPosition(0). На стоячем моторе это способ выставить ноль без концевика, и я пользуюсь им на каждой сборке. На ходу же вызов сбрасывает профиль на полпути: позиция прыгает, distanceToGo пересчитывается от новой точки, и мотор дёргается. Правило простое: сначала дождитесь dst= нуля, потом обнуляйтесь.
Автомат в симуляторе
AccelStepper в симуляторе не эмулируется — эта оговорка стоит ещё в уроке про установку, и здесь она всё так же честная. Зато автомат из этого урока собирается на симуляторном Stepper.h руками: тот же счётчик n, та же подрезка интервала, то же торможение по остатку — только каждый шаг этой машины напечатан в Serial, а не спрятан внутри run().
Кнопка D2 запускает ход на 512 шагов: n растёт до шестидесяти четырёх, iv= сползает с 40 мс к 8, затем остаток ost= догоняет n — и счётчик сдаёт назад, тормоз включился сам. Обратите внимание на симметрию: сколько шагов разгонялись, столько и тормозим — та же арифметика, что в прошлом уроке, только без ручного учёта.
Хвост перед целью едет медленно и может показаться заеданием — это не оно: при коротком остатке профиль честно складывается в треугольник, и последние шаги проходят на стартовом интервале. На плате то же самое покажет spd=, падающая к нулю у цели.
- run() пересчитывает интервал на каждом шаге: счётчик n растёт в разгоне и сдаёт назад в торможении
- Первый интервал выводится из ускорения: при 1000 шаг/с² это около 30 мс — старт с 33 шагов в секунду
- Торможение включается, когда остаток пути становится меньше n: сообщать его руками не нужно
- moveTo перезаписывает цель в полёте — профиль пересобирается сам, обе половины
- Граница: фиксированный маршрут на одном моторе — самоделка; живые цели и несколько моторов — библиотека
- 1. Кто в AccelStepper вычисляет интервал следующего шага?
- 2. Как библиотека узнаёт, что пора тормозить?
- 3. Первый интервал при acceleration 1000 шаг/с² — около 30 мс. Что это значит на валу?
- 4. Зачем вызывать run() в каждом проходе loop()?
- 5. Чем библиотечное торможение лучше ручного из прошлого урока?
Частые вопросы по теме урока
- Кто в AccelStepper вычисляет интервал следующего шага?
- run() перед каждым шагом пересчитывает интервал по счётчику n и ускорению. Ни таймер платы, ни драйвер про профиль скорости не знают. Сама библиотека перед каждым шагом, из текущего состояния разгона
- Как библиотека узнаёт, что пора тормозить?
- distanceToGo() сверяется с разгонным счётчиком: остаток догнал n — счётчик сдаёт назад, интервал растёт. Сообщать тормозной остаток руками не нужно. Остаток пути стал меньше числа шагов разгона n
- Первый интервал при acceleration 1000 шаг/с² — около 30 мс. Что это значит на валу?
- 30 мс на шаг — это 33 шага в секунду: старт с тихого темпа, который пластиковый редуктор стронет без щелчка. Формула в исходнике — 0,676 × корень из двух, делённого на ускорение. Мотор трогается с тихой скорости около 33 шагов в секунду
- Зачем вызывать run() в каждом проходе loop()?
- run() — насос профиля: каждый вызов делает максимум один шаг. Пропустили проходы loop — скорость не растёт и не падает, шагов нет. Без вызовов профиль замирает: для библиотеки останавливается время
- Чем библиотечное торможение лучше ручного из прошлого урока?
- Главное — состояние движения хранит автомат, а не ваши переменные: moveTo перезаписал цель, и обе половины профиля пересобрались без участия кода. Пересчёт идёт на каждом шаге, а цель можно сменить в полёте — тормозной остаток пересоберётся сам