Первый XY-столик я собрал из двух 28BYJ-48: одна тянет каретку вдоль рельса, вторая — поперёк, направляющие из мебельного профиля, две платы ULN2003, восемь сигнальных пинов. Скетч казался очевидным: едим в точку — сначала один мотор, потом второй. step(600) по X, step(200) по Y. На бумаге — диагональ, на столе — лесенка: платформа проезжала длинную сторону, останавливалась, заворачивала и только потом шла короткую. Маркер рисовал букву L там, где я заказывал линию.
Причина в том, что step() блокирует loop(): пока первый мотор досчитывает свои шестьсот шагов, второй стоит и ждёт очереди. Это свойство библиотеки Stepper мы разобрали в уроке про первый поворот вала: один вызов — одно движение до конца, без вариантов. Для заслонки этого хватает, для координатного движения — нет: обе оси обязаны работать в одном времени, иначе траектория рассыпается на куски.
В этом уроке соберём честную прямую через пропорцию шагов, нарисуем круг из мелких приращений и посмотрим, как меняются правила, когда моторов два: от выбора библиотеки до порядка вызовов в loop(). Движок симулятора умеет крутить только один настоящий шаговик, поэтому вторая ось поедет координатами в Serial — логика от этого не перестаёт быть честной.
Пропорция шагов: прямая без магии
Прямая в координатах — это два числа: на сколько шагов съезжает X и на сколько Y. Пусть из нуля в точку (600, 200). Отношение 3:1: на каждый шаг поперечной оси продольная делает три. Техника простая: делим оба числа на их общий делитель и получаем такт — пару шагов, которая повторяется весь путь. Шестьсот и двести делятся на двести: выходит двести тактов, в каждом три шага по X и один по Y.
Если числа делятся плохо — например, 701 и 300, — общего такта нет, и работает накопление дроби: ведущая ось, та, что дальше, шагает каждый такт, а ведомая копит отставание и шагает, когда наберётся целый шаг. Это алгоритм Брезенхэма из компьютерной графики, только вместо пикселей — шаги мотора. В самодельных столиках я чаще упрощаю: подгоняю цели под кратные числа, потому что дозировать рисунок дробями на 28BYJ-48 — роскошь, которую никто не видит.
Зачем возиться, если лесенка приводит в ту же точку? Из-за характера движения. Лесенка — это две остановки и рывок на повороте; для маркера это ступенька на линии, для дозатора — двойная подача в углу, для гравёра — зарубка. И из-за времени: пока X едет шестьсот шагов, Y молчит, потом наоборот. Любая задача, где важно само движение вдоль пути, а не только конечная точка, видит эту разницу глазами.
| Подход | Что даёт | Цена |
|---|---|---|
| Два step() по очереди | Две строки кода, работает сразу | Лесенка вместо прямой, loop() заперт дважды: времена складываются |
| Пропорция в одном такте | Честная прямая на голой Stepper без библиотек | Арифметика тактов и накопление дроби — на вашей совести |
| Два AccelStepper с run() в loop() | Обе оси едут в одном времени, loop() живой | Нужна установка библиотеки и пересчёт позиций в полушаги |
Теперь грабли, на которых я простоял вечер. Двум моторам — две платы и восемь пинов, это понятно. Но скорость у каждой оси своя: я задал X десять оборотов в минуту, Y — пять, из осторожности. При 2048 шагах на оборот шаг при 10 RPM занимает 2,9 мс, при 5 RPM — 5,9 мс. Скетч звал step(600) у каждой оси по очереди: X закончил за 1,76 секунды, Y полз три с половиной — вышел прямоугольник вместо квадрата, и ни один угол не совпал с заказанным.
Второй слой тех же граблей — блокировка. Пока первый мотор шагает, второй не получает ни такта, и всё, что вы имеете в виду под словом одновременно, превращается в по очереди. Арифметика безжалостна: два блокирующих вызова подряд — это сумма их времён, а не худшее из двух. Диагональ, которая на живой оси занимает полторы секунды, тем же стилем с двумя Stepper-объектами растянется вдвое.
Выход — неблокирующее движение: библиотека AccelStepper держит по объекту на мотор, а в loop() вы на каждом проходе зовёте run() для каждой оси. Внутри она сама решает, чей такт настал. Ставится она через Library Manager — как устанавливают библиотеки, мы проходили в начале курса. Для осей с разными задачами и скоростями это стандартный путь; про потолок скорости 28BYJ-48 помните и тут: редуктор прощает медленную езду, быструю — нет.
#include <AccelStepper.h> // две оси XY-столика: 28BYJ-48 через две платы ULN2003AccelStepper axisX(AccelStepper::HALF4WIRE, 8, 9, 10, 11, true);AccelStepper axisY(AccelStepper::HALF4WIRE, 4, 6, 5, 7, true); void setup() { Serial.begin(9600); // прямая из (0,0) в (1200, 400) полушагов: отношение 3:1 axisX.setMaxSpeed(300); // полушагов в секунду axisY.setMaxSpeed(100); // треть от X: приедем одновременно axisX.moveTo(1200); axisY.moveTo(400);} void loop() { axisX.run(); axisY.run(); if (axisX.distanceToGo() == 0 && axisY.distanceToGo() == 0) { Serial.print("Arrived X="); Serial.print(axisX.currentPosition()); Serial.print(" Y="); Serial.println(axisY.currentPosition()); delay(1000); axisX.moveTo(0); axisY.moveTo(0); }}Разбор по строкам. HALF4WIRE включает полушаг на четырёх проводах — для 28BYJ-48 это 4096 позиций на оборот вместо 2048: классическая Stepper полушага не умеет, AccelStepper умеет. Порядок пинов здесь другой, и на этом спотыкаются все: библиотека ждёт их как pin1, pin3, pin2, pin4, поэтому при распайке IN1 к 8, IN2 к 10, IN3 к 9, IN4 к 11 запись выглядит как (8, 9, 10, 11). Аргумент true в конце инвертирует сигналы под ULN2003 — без него мотор только дёргается.
setMaxSpeed измеряет не обороты в минуту, а шаги в секунду: триста полушагов в секунду на нашем редукторе — это около 4,4 оборота в минуту, разумный потолок для плавной езды. Главная строка — пропорция: цели 1200 и 400, скорости 300 и 100. Обе оси приедут через четыре секунды: 1200 на 300 и 400 на 100. Нарушите пропорцию — быстрая ось приедет раньше, и траектория загнётся в сторону лидера.
Тот же пакет ставит библиотеку MultiStepper: она синхронизирует прибытие сама, но без разгона и торможения — для постоянных скоростей годится, для рывка на старте нет. run() не блокирует, поэтому между тактами loop() живёт обычной жизнью: кнопки, Serial, датчики — приём неблокирующего цикла здесь нужен весь. Я поначалу пихал в loop() ещё и delay(20) для спокойствия — и получал дрожание диагонали: пауза сдвигает такты осей неравномерно.
Круг из мелких приращений
Круг — это много коротких прямых. Параметрически: X равен R на cos(t), Y равен R на sin(t); двигаете угол t мелкими приращениями и от каждой точки едете к следующей той самой пропорцией шагов. На 36 секторов каждый сектор — десять градусов, и хорда короче дуги на доли процента: глаз разницы не находит, а код остаётся тем же, что для прямой.
Числа для планирования. Радиус 400 полушагов: соседние точки отстают по хорде на 2 умножить на 400 и на синус пяти градусов — около 70 полушагов на сектор. Весь круг — 2 пи на 400, примерно 2500 полушагов, при средней скорости 300 в секунду — около восьми секунд на оборот. Для поворотного столика предметной съёмки это рабочий темп: кадр каждые десять градусов каждые восемь секунд.
Грабли две. Первая — дробь: приращения в целых шагах с накоплением остатка, иначе последний сектор не замкнётся; я ловил щель в шесть-восемь полушагов между началом и концом круга, пока не завёл переменную под остаток. Вторая — люфт пластикового редуктора: круг по часовой и круг против часовой не лягут друг на друга, расхождение в несколько градусов на 28BYJ-48 — норма, её отрабатывают подбором направления для критичных задач.
В симуляторе ниже живой только один мотор: ось X крутит настоящий шаговик с циферблатом, ось Y нарисована координатой pos2 в Serial. Пропорция та же, что в скетче выше: три шага X на один шаг Y, цель (600, 200). Читается это удобно в Serial-мониторе: пары значений идут с меткой такта.
Задача на прогон: убедиться, что пары pos1 и pos2 держат отношение три к одному на всём пути, и что в финальной точке они приходят одновременно. Потом поменяйте цели на 700 и 300, такт на семь и три, а число тактов на сто — пропорция пар обязана сохраниться. Если после правки пары поехали вразнобой, значит, сломана пропорция такта, а не мотор.
- Две оси едут по прямой, когда шаги идут в пропорции целей: 600 к 200 — три шага X на каждый шаг Y
- step() при двух моторах даёт лесенку: пока едет один, второй стоит; два вызова подряд — сумма времён
- Прямая на AccelStepper: пропорция maxSpeed равна пропорции целей — 1200 к 400 как 300 к 100 полушагов в секунду
- HALF4WIRE — 4096 позиций на оборот; пины пишутся как pin1, pin3, pin2, pin4; true — инверсия под ULN2003
- Круг — 36 секторов по 10 градусов: приращения в целых шагах с накоплением остатка, иначе круг не замкнётся
- 1. Почему первый XY-столик рисовал букву L вместо линии?
- 2. Какая пропорция шагов нужна для прямой из (0,0) в (600,200)?
- 3. Почему в AccelStepper::HALF4WIRE пины записаны как (8, 9, 10, 11) при распайке IN1 к 8, IN2 к 10, IN3 к 9?
- 4. Цели осей — 1200 и 400 полушагов. Какие maxSpeed приведут обе оси в цель одновременно?
- 5. Сколько позиций на оборот вала даёт режим HALF4WIRE у 28BYJ-48?
Координаты освоены, но в них осталась дыра: ноль. После перезапуска питания счётчик шагов стартует с нуля, а механика стоит где стояла, и любая прямая уедет от ниоткуда. Следующий урок ставит механический концевик на пин D2 и учит мотор находить свою нулевую позицию при каждом включении — homing, без которого столик из этого урока рисует красивые линии мимо реального мира.
Прогоните симулятор ещё раз с целью (400, 400): такт один к одному — самый наглядный случай, когда обе оси шагают синхронно, и пары в Serial совпадают до единицы. После этого чужие прошивки с двумя осями читаются с листа: сначала ищите пропорцию, потом проверяйте, чем живёт loop() между тактами.
Частые вопросы по теме урока
- Почему первый XY-столик рисовал букву L вместо линии?
- Пока step() первого мотора досчитывает шаги, второй стоит: вызовы выполняются строго по очереди. Нужна неблокирующая езда обеих осей в одном времени. step() блокирует loop(): оси работали по очереди
- Какая пропорция шагов нужна для прямой из (0,0) в (600,200)?
- 600 и 200 относятся как 3:1: на каждый шаг поперечной оси продольная делает три. Двести тактов по три и одному шагу. 3:1
- Почему в AccelStepper::HALF4WIRE пины записаны как (8, 9, 10, 11) при распайке IN1 к 8, IN2 к 10, IN3 к 9?
- Внутри AccelStepper последовательность фаз ожидает третий пин раньше второго. Отсюда перестановка 9 и 10 относительно привычной записи для Stepper. Библиотека ждёт пины в порядке pin1, pin3, pin2, pin4
- Цели осей — 1200 и 400 полушагов. Какие maxSpeed приведут обе оси в цель одновременно?
- Скорости должны повторять пропорцию целей: 1200/300 и 400/100 — по четыре секунды. Равные скорости приведут быструю ось к цели втрое раньше. 300 и 100
- Сколько позиций на оборот вала даёт режим HALF4WIRE у 28BYJ-48?
- Полушаг удваивает число позиций: 2048 полных шагов превращаются в 4096 полушагов по 0,088 градуса. 4096