Плоттер на моём столе жил по-старому: step(1024) из стандартной библиотеки, пауза, step(-1024). Однажды на разгоне он начал звенеть и рисовать ломаную вместо прямой: мотор трогался с места на полной скорости, две сотни шагов пролетали с рёвом, и на первом сантиметре каретка теряла шаги. Кривые съезжали вправо, а виновата была не механика — манера старта: сразу на максимальную скорость, как будто у ротора нет инерции и редуктора.
Лечится рампой: мотор должен набирать скорость и сбрасывать её плавно, по трапеции. Теорию этой рампы мы считали в уроке про трапецеидальное ускорение, здесь берём готовый инструмент — библиотеку AccelStepper. Установку я показывал в уроке про установку AccelStepper, она занимает две минуты; главное — три вызова в setup() и одна дисциплина в loop(), без которых библиотека превращается в ещё один источник рывков.
План такой: конструктор и выбор интерфейса под 28BYJ-48, setMaxSpeed и setAcceleration с их единицами, качели на moveTo и run, ловушка «run() не вызван — мотор стоит» и честное сравнение со встроенным Stepper. Блокирующий step() из базовых уроков никуда не денется — но после рампы возвращаться к нему не хочется, и я объясню почему.
Рампу можно писать и руками: порции step() по таймеру millis() с растущим темпом — мы делали ровно это в уроке про планировщик шагов. Для одного мотора и одного маршрута этого хватает. Но стоит добавить вторую ось, остановку по кнопке и торможение по датчику, как ручная рампа обрастает флагами, а арифметика периодов уезжает в дроби. AccelStepper закрывает весь этот слой: у каждого объекта своя рампа и своя очередь целей, и они не путаются. Свой рукописный вариант я выбросил через месяц — и не скучаю.
Конструктор: FULL4WIRE или HALF4DIR
Библиотека просит выбрать интерфейс — как физически раскладываются фазы по катушкам. Для 28BYJ-48 с ULN2003 рабочих варианта два: AccelStepper::FULL4WIRE — полный шаг, те же 2048 шагов на оборот, что в конструкторе Stepper из урока про первый поворот; и AccelStepper::HALF4DIR — полушаг, 4096 шагов на оборот: вдвое плотнее позиционирование и вдвое чаще импульсы. Для медленных узлов, которым нужна плавность, беру HALF4DIR; для всего, что гоняется быстро, — FULL4WIRE: у редуктора 28BYJ-48 потолок скорости и так низкий, полушагом его легко упереть.
Порядок пинов коварен: конструктор просит IN1, IN3, IN2, IN4 — вторую и третью ноги местами. На ULN2003 это значит AccelStepper motor(AccelStepper::FULL4WIRE, 8, 10, 9, 11) — те же восемь, десять, девять и одиннадцать, к которым мы подключались через Stepper. Если мотор после перехода дёргается на месте, первая проверка — порядок аргументов, вторая — пары обмоток.
Дальше два вызова, на которых держится всё. setMaxSpeed(320) — это триста двадцать шагов в секунду, не оборотов в минуту: привычный setSpeed из Stepper мерил RPM, и перенос привычки даёт мотору команду ползти. Забыли setMaxSpeed вообще — AccelStepper по умолчанию едет один шаг в секунду, два оборота за сорок минут. setAcceleration(200) — двести шагов в секунду за секунду: при таких цифрах 28BYJ-48 трогается без звона. Ноль ускорения — мгновенный выход на потолок, тот самый срыв, от которого мы уходим.
Полушаг требует и пересчёта скорости: 4096 шагов на оборот означают, что тот же физический темп просит вдвое больше шагов в секунду — 320 шагов/с при FULL4WIRE и 640 при HALF4DIR дают одинаковые 9,4 оборота в минуту. Момент на полушаге при этом ниже: катушки держат ротор слабее, и гружёная ось срывается раньше. Платите плотностью позиции и меньшим запасом по моменту — честный размен, о котором забывают в половине проектов.
| Вопрос | Stepper.step | AccelStepper.run |
|---|---|---|
| Блокировка | loop() спит до конца движения | loop() живёт, шаги выдаются порциями |
| Разгон | нет: старт сразу на полной скорости | трапеция по setAcceleration |
| Единицы скорости | setSpeed — обороты в минуту | setMaxSpeed — шаги в секунду |
| Мягкий стоп | нет, движение дорабатывает до конца | торможение начинается за разгонный путь до цели |
#include <AccelStepper.h> // 28BYJ-48 через ULN2003: IN1=8, IN3=10, IN2=9, IN4=11.// FULL4WIRE - полный шаг, 2048 шагов на оборот; HALF4DIR дал бы 4096.AccelStepper motor(AccelStepper::FULL4WIRE, 8, 10, 9, 11); void setup() { motor.setMaxSpeed(320); // шагов в СЕКУНДУ, не оборотов в минуту motor.setAcceleration(200); // шагов в секунду за секунду motor.moveTo(4096); // цель: два оборота Serial.begin(9600);} void loop() { motor.run(); // вызывается КАЖДЫЙ проход loop() if (motor.distanceToGo() == 0) { // доехали: торможение уже случилось само, по остатку пути motor.moveTo(-motor.currentPosition()); // разворот: теперь туда } // сюда можно воткнуть кнопки, датчики и Serial - loop не блокируется}В скетче нет ни одного delay — и это не поза, а условие работы библиотеки. Каждый вызов run() выдаёт мотору не больше одного шага: библиотека сама считает по millis(), пора ли, и сама ведёт рампу. При setAcceleration(200) выход на 320 шагов в секунду занимает 1,6 секунды и 256 шагов — столько пути съедает разгон, столько же оставляет торможению. Это и есть мягкий стоп: за 256 шагов до цели период шага начинает расти, и к финальной позиции мотор подъезжает медленно, без удара и без срыва.
Качели держатся одной строкой: когда distanceToGo() обнулился, moveTo(-motor.currentPosition()) разворачивает цель в противоположную точку. Куда двигаться дальше — зависит от задачи: две пары координат на двух валах раскладываются в уроке про два мотора и координаты, а здесь важно одно — ни одна ветка кода не имеет права задержать run().
Два вызова заслуживают отдельной строки. move(N) — цель относительно текущего места, moveTo(N) — от нуля отсчёта; перепутали — каретка уезжает на N шагов от себя, а не в точку N, и промах ищется по чертежу задним числом. runSpeed() — старший вариант без рампы: постоянная скорость без разгона, годится, когда мотор заведомо в стартовой зоне. disableOutputs() после остановки отпускает обмотки — ток удержания около 240 мА уходит, мотор остывает, но вал теряет фиксацию: горизонтальному столику это подарок, вертикальной оси — падение каретки.
Stepper.step против run(): цена блокировки
Встроенный Stepper.step() — это delay() в моторном исполнении: step(2048) при пяти оборотах в минуту — минута тишины, за которую кнопки не читаются, датчики спят, Serial молчит. Аргументы здесь те же, что в уроке про millis вместо delay: блокирующий вызов в loop() замораживает весь скетч, а не только движение. Аккуратный ответ AccelStepper — run() как планировщик: один шаг за вызов, ноль блокировок, полный разбор подхода ждёт в уроке про неблокирующий run.
Рампа меняет не только плавность, она меняет саму допустимую скорость. Мгновенный старт на 320 шагов в секунду рвёт синхронизацию: ротор не успевает за полем, слышен звон — и шаги пропали. Тот же потолок с рампой 200 шагов в секунду за секунду проходит чисто даже с ремнём и грузом: инерция набирается по пути. Моё правило после пары испорченных заготовок: любое движение с нагрузкой — только через рампу, а мгновенный старт оставьте пустому валу и скоростям ниже сотни шагов в секунду.
Где блокирующий step() всё ещё уместен? Там, где loop() всё равно спит: полив по расписанию, поворот столика раз в час, ночная лампа с заслонкой. А вот одновременное ведение двух моторов из одного loop() — два объекта AccelStepper и два run() подряд — доступно только неблокирующему подходу; блокирующий step() вынуждает моторы ехать по очереди, и в координатной механике это сразу видно по косым линиям.
Свой рукописный аналог рампы я честно пробовал: порции по millis(), дробные периоды, торможение по остатку — тридцать строк и две недели вечеров. Работало, пока я не добавил остановку по кнопке: она влезала между порциями, сбивала фазу рампы, и мотор на развороте срывался. AccelStepper держит ту же логику внутри, отлаженную сотнями проектов, и не просит за это ни ОЗУ сверх пары десятков байт, ни внимания после запуска. Ручная рампа остаётся отличной учебной задачей — и плохим рабочим инструментом.
Честно, как всегда: AccelStepper в движке симулятора не живёт — модель ниже показывает рампу на поддерживаемом API. Порции по восемь шагов выдаются по таймеру millis(), а интервал между порциями тает от тридцати миллисекунд до четырёх — это и есть модель разгона: период шага сжимается, скорость растёт. Кнопка D2 запускает ход на пол-оборота, ручка A0 задаёт жёсткость ускорения, Serial печатает текущий интервал.
Прогон: нажмите D2 и смотрите ленту Serial — цифры падают с тридцати до четырёх и замирают: мотор вышел на потолок модели. Поверните A0 в максимум — разгон сожмётся до пары десятков шагов, и это наглядная цена агрессивного ускорения: чем короче рампа, тем ближе срыв шагов.
Модель лукавит в масштабе: порции по восемь шагов грубее реального run(), где период меняется на каждом шаге, — поэтому лента Serial ступенчатая, а не плавная. Порядок величин тот же, и это главное: разгонный путь в сотню шагов, торможение по остатку, потолок, на котором интервал замирает. Понажимайте D2 во время движения: модель игнорирует кнопку, как и настоящий скетч выше, — перехват хода посреди рампы заслуживает отдельного разговора.
- 1. В каких единицах измеряет скорость setMaxSpeed в AccelStepper?
- 2. Мотор с moveTo() стоит, хотя скетч крутится. Что проверить первым?
- 3. Сколько шагов на оборот даст интерфейс FULL4WIRE для 28BYJ-48?
- 4. Потолок 320 шагов/с, setAcceleration(200). Сколько шагов займёт разгон?
- 5. Мотор ползёт примерно один шаг в секунду. Причина?
- AccelStepper(FULL4WIRE, 8, 10, 9, 11) — 2048 шагов/оборот; HALF4DIR — 4096; порядок пинов IN1, IN3, IN2, IN4
- setMaxSpeed — шаги в секунду (не RPM), без него — 1 шаг/с; setAcceleration — шагов/с², ноль = мгновенный старт
- run() — каждый проход loop(), максимум один шаг за вызов; delay и редкие вызовы останавливают мотор
- Мягкий стоп: торможение начинается за разгонный путь; 320 шагов/с при 200 шагов/с² — по 256 шагов на разгон и тормоз
- step() блокирует loop() на всё движение; run() держит кнопки, датчики и два мотора одновременно
Частые вопросы по теме урока
- В каких единицах измеряет скорость setMaxSpeed в AccelStepper?
- Привычка от Stepper.setSpeed(RPM) здесь подводит: 320 — это шагов в секунду, около 9,4 оборота в минуту при 2048 шагах на оборот. Шагов в секунду
- Мотор с moveTo() стоит, хотя скетч крутится. Что проверить первым?
- Библиотека двигает мотор только внутри run(). Спрятанный под if или задержанный delay() вызов — мотор стоит. Вызывается ли run() каждый проход loop()
- Сколько шагов на оборот даст интерфейс FULL4WIRE для 28BYJ-48?
- FULL4WIRE — полный шаг, те же 2048, что у Stepper. HALF4DIR — полушаг, вдвое больше: 4096. 2048
- Потолок 320 шагов/с, setAcceleration(200). Сколько шагов займёт разгон?
- Разгонный путь равен квадрату скорости, делённому на удвоенное ускорение: 320²/400 = 256 шагов, около 1,6 секунды. Столько же займёт торможение. 256
- Мотор ползёт примерно один шаг в секунду. Причина?
- Без setMaxSpeed AccelStepper едет один шаг в секунду — два оборота 28BYJ-48 за сорок минут. setMaxSpeed не задан: по умолчанию 1 шаг/с