Таймлапс-слайдер с поворотным столом я собрал быстро и сломал ещё быстрее: обе оси на Stepper.h, и каждая команда table.step(50) ждала slider.step(5), а slider ждал table. По очереди моторы работали, «одновременно» — никогда: на кадре видно, что стол дёргается, потом едет камера, потом снова стол. Stepper.step() блокирует до последнего шага, и двухосевая съёмка превращается в ступеньки. Это касалось бы и плоттера, и ЧПУ-гравёра, и любого узла, где две оси должны течь вместе.
Лечение — отказ от блокирующего step() в пользу STEP/DIR-драйверов и чередование шагов в одном loop(). Каждой оси — свой таймер и свой интервал: стол шагает каждые 3000 мкс, слайдер каждые 7000, и loop() успевает обслужить обе. Когда таймеры совпадают, шаги идут один за другим — микросекунды между ними, ни одна ось не «висит» и не ждёт. Ниже — скетч стека из двух осей и разбор, почему честность loop() здесь решает всё.
Стек осей: свой таймер на каждую
С драйверами A4988 мотору нужны два пина: фронты на STEP двигают на шаг, уровень на DIR выбирает сторону. Это снимает главную занозу Stepper.h — четыре пина и блокирующие вызовы. Дальше дисциплина millis-планировщика: lastStepA, lastStepB, intervalA, intervalB — и два if в loop(). Каждый if проверяет свой таймер и отрабатывает один шаг за микросекунды. Даже если интервалы совпали, два шага подряд занимают меньше десяти микросекунд — ось B не «ждёт» ось A ни на человеческий глаз, ни на датчик.
Финал двух задач — ловушка с обратной стороной: while(true){} после цели похоронит Serial, LED и всё остальное. Правильно — флаг done: задание выполнено, оси стоят, loop() продолжает жить и печатать статус. Куда развивать дальше: связать оси координатами и вести их парой — это уже урок про два мотора и координаты, здесь — сама механика одновременности.
const int PIN_STEP_A = 4; // ось A: поворотный столconst int PIN_DIR_A = 5;const int PIN_STEP_B = 6; // ось B: слайдер камерыconst int PIN_DIR_B = 7; const long TARGET_A = 1600;const long TARGET_B = 800;const unsigned long INTERVAL_A = 3000; // мкс: 333 шага/сconst unsigned long INTERVAL_B = 7000; // мкс: 143 шага/с long posA = 0, posB = 0;unsigned long lastA = 0, lastB = 0;bool done = false; void stepAxis(int pinStep, int pinDir, int dirSign, long* pos) { digitalWrite(pinDir, dirSign > 0 ? HIGH : LOW); digitalWrite(pinStep, HIGH); digitalWrite(pinStep, LOW); *pos += dirSign;} void setup() { pinMode(PIN_STEP_A, OUTPUT); pinMode(PIN_DIR_A, OUTPUT); pinMode(PIN_STEP_B, OUTPUT); pinMode(PIN_DIR_B, OUTPUT); Serial.begin(9600); Serial.println("две оси поехали: A=1600, B=800");} void loop() { unsigned long now = micros(); if (posA < TARGET_A && now - lastA >= INTERVAL_A) { lastA = now; stepAxis(PIN_STEP_A, PIN_DIR_A, 1, &posA); } if (posB < TARGET_B && now - lastB >= INTERVAL_B) { lastB = now; stepAxis(PIN_STEP_B, PIN_DIR_B, 1, &posB); } if (!done && posA >= TARGET_A && posB >= TARGET_B) { done = true; Serial.print("готово: A="); Serial.print(posA); Serial.print(" B="); Serial.println(posB); }}stepAxis принимает указатель на счётчик — одна функция обслуживает обе оси, и добавление третьей — это ещё один блок if с новыми константами, не новая библиотека. Условие pos < TARGET вместе с done-флагом даёт честную остановку: оси доходят до целей каждая в своё время, при этом вторая продолжает шагать, пока не докрутится. Если оси должны закончить строго вместе — это уже пропорциональное распределение скоростей по остатку пути, то есть интерполяция из урока про координаты.
Проверьте себя на честность loop(): ни одного delay(), ни одного блокирующего вызова, Serial только по событиям. Стоит вклиниться delay(100) «для стабильности» — интервалы поплывут, шаги станут рваными, и на слайдере это видно кадрами. Момент по току удержания, когда оси стоят, снимается обесточиванием — приём из урока про отключение катушек, для STEP/DIR это enable-пин драйвера.
- Stepper.step() блокирует: два мотора на библиотеке работают строго по очереди, «одновременно» не выйдет
- A4988 и родня дают интерфейс STEP/DIR: два пина на ось и шаг за несколько микросекунд
- Каждой оси — свои lastStep и interval; два if в loop() обслуживают обе без приоритетов
- Совпавшие таймеры — не проблема: шаги идут подряд за микросекунды, никто не ждёт
- Финал — флаг done, а не while(true): после цели Serial, кнопки и вторая задача живут
- 1. Таймеры двух осей сработали в один и тот же момент loop(). Что произойдёт?
- 2. Почему после достижения целей нельзя писать while(true){}?
- Два мотора на Stepper.step() — очередь, а не одновременность: библиотека блокирует до конца хода
- STEP/DIR-драйверы делают ось двухпиновой: фронт на STEP — шаг, уровень DIR — сторона
- Каждой оси — свой lastStep и interval, два if в loop(); совпадение таймеров стоит микросекунды
- После цели — флаг done: движение останавливается, плата продолжает жить и обслуживать остальное
Частые вопросы по теме урока
- Таймеры двух осей сработали в один и тот же момент loop(). Что произойдёт?
- Каждый if обслуживает свою ось независимо: два шага подряд занимают меньше десяти микросекунд. Ожидание появилось бы только с блокирующим step(). Шаги пойдут подряд: два if выполнятся за микросекунды, обе оси шагнут
- Почему после достижения целей нельзя писать while(true){}?
- Мёртвый loop() перестаёт читать Serial, кнопки и датчики. Флаг done останавливает оси и оставляет программу живой — стоп только у движения, не у платы. Он похоронит Serial и всё остальное: loop() должен жить до снятия питания