Турникет в мастерскую я собрал за вечер и почти неделю стыдился его работы. Логика простая: вал шаговика поворачивается ровно на девяносто градусов, пропускает человека, возвращается. На столе всё было красиво. В первый рабочий день турникет встал посреди поворота, загудел и замер: я выставил setSpeed(25), и на 25 оборотах в минуту мотор сорвал шаги — ротор с редуктором просто не успевал за импульсами. Через день повторилось наоборот: я снизил скорость до 5, всё поехало плавно, но Serial-лог показывал, что между стартом и финишем прошло вдвое больше времени, чем я планировал, и всё это время плата не отвечала ни на что — программа стояла внутри step().
Обе проблемы лечатся пониманием того, как библиотека Stepper обращается со временем и числом шагов. Посмотрим на скорость под микроскопом, научимся переводить градусы в шаги и сделаем движение туда-обратно, которое не замораживает плату. Заодно закроем историю с горячим мотором из прошлого урока — снимем 240 мА удержания с питания.
setSpeed: что скрывается за RPM
RPM — revolutions per minute, оборотов в минуту. Вызов stepper.setSpeed(10) обещает: вал сделает десять полных оборотов за 60 секунд. Переведём в шаги: 10 × 2048 / 60 ≈ 341 шаг в секунду, то есть один шаг каждые 2,9 миллисекунды. Библиотека сама считает задержку между переключениями обмоток — в loop() для этого делать ничего не нужно.
Почему нельзя задрать скорость до бесконечности? У каждого шага есть физический лимит: обмотке нужно время, чтобы магнитное поле наросло, а инерционная масса ротора и шестерёнок редуктора — чтобы доехать до следующего положения. Если импульсы идут чаще, чем механизм успевает, ротор не доезжает: мотор гудит, дрожит, а вал стоит на месте. Это и есть срыв шагов, и он коварен тем, что не считается ошибкой: программа честно отчиталась, что шаги сделаны, — а счёт позиции с этого момента врёт.
Практическая шпаргалка для 28BYJ-48 на 5 вольтах. Без нагрузки уверенно ходит 10-12 RPM; с небольшой нагрузкой — 5-8; после 15 начинается лотерея, отдельные экземпляры срываются уже на старте. Момент тоже не константа: чем выше RPM, тем меньше тяга — мотор может не провернуть на высокой скорости то, что спокойно тащит на низкой.
Поворот на заданный угол
Формула одна: шаги = угол × 2048 / 360. Девяносто градусов — 512 шагов, ровно и красиво. Тридцать градусов — 170,67, и здесь первый подвох: step() принимает целое число, дробь приходится округлять. Насколько это больно? Один шаг — 0,176 градуса, значит, ошибка округления никогда не превысит 0,09°. Для кормушки, турникета или заслонки — ничто; даже для поворотного столика предметной съёмки — приемлемо.
В коде формулу я записываю через дробную константу STEPS_PER_REV / 360.0 — и вот тут внимание к деталям. Если написать 360 без точки, C++ сначала выполнит целочисленное деление 2048 / 360, получит 5 — и вся точность полетит в мусорку: 45 градусов превратятся в 225 шагов, а это уже не 45°. Точка в 360.0 заставляет компилятор считать в дробях. Результат умножения я привожу к long — целому большого разрядности: на 2048 шагов хватило бы и int, но привычка не экономить разрядность спасает, когда число шагов за один вызов перевалит за 32 767.
Ещё один нюанс — накопление ошибки. Если механизм делает сотни ходов туда-обратно, каждая половинка шага от округления складывается: после тысячи циклов платформа уползёт на несколько шагов. Лекарство — репер в нулевой точке: концевик, геркон или оптопара. Раз в N циклов возвращаемся к реперу, обнуляем счёт и спим спокойно. На моём шлагбауме геркон появился именно поэтому, а не из паранойи.
#include <Stepper.h> const int STEPS_PER_REV = 2048;const float STEPS_PER_DEGREE = STEPS_PER_REV / 360.0; // 5.689 шага на градус Stepper stepper(STEPS_PER_REV, 8, 10, 9, 11); // угол в градусах: плюс — по часовой, минус — противvoid turnByAngle(float angle) { long steps = (long)(angle * STEPS_PER_DEGREE); stepper.step(steps);} void setup() { stepper.setSpeed(8); Serial.begin(9600); Serial.println("Turntable ready");} void loop() { Serial.println("Povorot na 90 gradusov"); turnByAngle(90); delay(2000); Serial.println("Nazad na 90"); turnByAngle(-90); delay(2000);}Долгие ходы: порции шагов и millis()
Метод step() не возвращается, пока не сделает все шаги. При 5 RPM полный оборот — это минута тишины: Serial молчит, кнопки не опрашиваются, датчики копят злость. Внутри step() — цикл «переключить обмотки, подождать рассчитанные миллисекунды, повторить», и всё это время loop() не живёт.
Лечение — дробить ход на порции. Заводим остаток маршрута и таймер; в loop() каждые несколько миллисекунд просим мотор step(4) — четыре шага, — вычитаем их из остатка и сразу возвращаемся к остальным делам. Остаток кончился — меняем знак направления и едем обратно. Паузы на краях тоже делаются таймером: запомнили millis() момента прибытия и ждём, пока разница не перевалит за три секунды.
Получается концепт «двигатель как фон»: loop() крутится быстро, мотор понемногу едет, Serial живёт. Расплата — чуть более хитрый код и контроль порции: step(4) при 12 RPM занимает около 10 миллисекунд, и эта десятка не должна ломать остальную логику цикла.
- Поверните вал на 137 градусов и проверьте арифметику: 137 × 2048 / 360 = 779,4, значит, мотор сделает 779 шагов, а ошибка составит меньше 0,09°.
- Найдите предел скорости своего экземпляра: поднимайте setSpeed с шагом в 2 RPM и логируйте millis() до и после step(2048). Реальное время оборота заметно выросло — начинается срыв. У моего экземпляра предел оказался 14 RPM.
- Сделайте «шлагбаум»: поворот на 90°, пауза 5 секунд, возврат. Паузу держите сравнением millis() с временем прибытия — без delay().
- Добавьте к качелям Serial-отчёт каждые 256 шагов — увидите, что loop() действительно живёт во время движения.
- setSpeed(RPM) — оборотов в минуту; 10 RPM = 341 шаг/с при 2048 шагах на оборот
- Потолок 28BYJ-48 на 5 В: 10-12 RPM без нагрузки, 5-8 с грузом; выше 15 — срыв шагов
- Шаги из угла: steps = угол × 2048 / 360; один шаг = 0,176°, ошибка округления не больше 0,09°
- В делении на градусы пишите 360.0 с точкой — иначе целочисленное деление убьёт точность
- step(N) блокирует loop(): оборот при 5 RPM — минута тишины; долгие ходы дробят на порции с таймером millis()
- digitalWrite(8/10/9/11, LOW) после остановки снимает 240 мА удержания, но вал теряет фиксацию
- 1. Сколько примерно шагов в секунду делает мотор при setSpeed(12)?
- 2. Мотор гудит и трясётся, но вал не вращается. Что произошло?
- 3. Сколько шагов нужно для поворота на 45°?
- 4. Почему после поворота на 30° вал может стоять не идеально точно?
- 5. Как заставить кнопку работать во время долгого перемещения мотора?
- 6. Что произойдёт, если после остановки обнулить все четыре сигнальных пина?
Мотор теперь послушный: едет на заданный угол, живёт в millis-цикле и не поджигает источник питания. Следующая ступень — драйверы с микрошагом (A4988 и моторы NEMA 17), где шаг дробится на шестнадцатые и мотор перестаёт гудеть; им будет посвящён отдельный раздел. А пока закрепите эксперименты — особенно поиск предела скорости: знание возможностей своего экземпляра избавляет от половины загадочных отказов.
Частые вопросы по теме урока
- Сколько примерно шагов в секунду делает мотор при setSpeed(12)?
- 12 × 2048 / 60 = 409,6 шага в секунду — один шаг каждые 2,4 мс. Около 410
- Мотор гудит и трясётся, но вал не вращается. Что произошло?
- При превышении скоростного предела ротор не успевает за полем. Позиция после срыва считается неверно. Срыв шагов: импульсы идут чаще, чем механизм успевает
- Сколько шагов нужно для поворота на 45°?
- 45 × 2048 / 360 = 256 шагов — делится без остатка. 256
- Почему после поворота на 30° вал может стоять не идеально точно?
- Шаг — минимальная единица движения. Дробная часть отбрасывается, худшая ошибка — половина шага. 30 × 2048 / 360 = 170,67 — step() берёт целое число, ошибка округления до 0,09°
- Как заставить кнопку работать во время долгого перемещения мотора?
- step() блокирует loop(), поэтому длинный ход нарезают на короткие куски с таймингом на millis(). Дробить перемещение на порции step() и опрашивать кнопку между порциями
- Что произойдёт, если после остановки обнулить все четыре сигнальных пина?
- Обмотки обесточены — мотор остывает и не ест 240 мА, но фиксацию позиции теперь обеспечивает только редуктор. Ток удержания упадёт почти до нуля, но вал перестанет держаться полем