Поворотный столик предметной съёмки работал у меня так: камера щёлкает кадр, столик проворачивается на сорок пять градусов, через восемь кадров — полный оборот. Скетч собрался сам собой: 45 × 2048 / 360 = 256 шагов, step(256), счётчик нарос. Первые две съёмки прошли идеально, и я перестал читать собственный код.
На третьей сессии, после ста двадцати восьми кадров, столик вдруг поехал длинной дорогой назад и отснял полсерии пустого сектора. Разбор занял вечер: счётчик позиции я держал в int, а 128 кадров по 256 шагов — это 32 768 шагов. У int предел 32 767, следующий шаг перевернул счётчик в минус 32 768, и вся логика решения, куда ехать, читала мусор. Мотору плевать — свои шаги он отработал честно. Врал мой счётчик.
В этом уроке обернём повороты в свои функции — rotateDeg, moveToDeg и home — и наведём порядок в счёте: где long обязателен, где хватает int, и почему возврат в ноль по счётчику почти всегда компромисс.
Три обёртки вместо размазанной арифметики
К четвёртому проекту со степпером я заметил, что таскаю одни и те же строки из скетча в скетч: пересчёт градусов, вызов step, наращивание счётчика. В одном из скетчей копия уехала с забытым минусом — вал двинулся не туда, и я полчаса искал железный дефект в исправном коде. После этого арифметика поселилась в функциях по правилу своих функций: один источник правды, ни одной размазанной копии.
Функций три, и у каждой своя работа. rotateDeg(deg) — относительный ход: едешь на столько-то градусов от текущего места, куда бы вал ни стоял. moveToDeg(deg) — абсолютная цель: функция сравнивает её со счётчиком и делает ровно нужную разницу. home() — возврат в ноль по счётчику. Внутри всех трёх одна формула: шаги = угол × 2048 / 360, в long, с округлением к ближайшему — та самая прибавка 180, о которой мы договаривались.
Свои функции тут не про красоту, а про цену правки: когда формула живёт в одном месте, вы чините её один раз, и ни одна копия не забудет минус. Счетчик позиции при этом заводится один — глобальный long — и каждая обёртка обязана его вести, иначе moveToDeg начнёт врать уже после первого вызова.
Свежие обёртки я прогоняю через Serial: parseInt читает угол из монитора — синтаксис мы разбирали — rotateDeg его исполняет, отчёт печатает фактическую позицию. Пять команд — плюс сорок пять, минус сорок пять, сто восемьдесят, ноль, снова сорок пять — и видно всё сразу: знак, округление, счётчик. Пять минут такого прогона заменяют час разборов потом, когда обёртка уехала в проект с камерой.
Теперь счётчик. Каждый ход меняет позицию на число шагов, и от неё зависит направление и длина следующего хода. int на плате держит от минус 32 768 до 32 767 — всего шестнадцать оборотов вала 28BYJ-48. Столик сломался на 128-м кадре по 256 шагов; кормушка с десятью циклами в день дожила бы до переворота за полгода; качели туда-обратно не сломаются никогда — их счёт и не выходит за размах. Пограничный случай — механизм с односторонним ходом: он переполняется молча, без единого сообщения в Serial.
long держит до 2,1 миллиарда шагов — это миллион оборотов вала, для настольных механизмов навсегда. Правило теперь у меня такое: позиция, цель и разница — только long, даже когда кажется, что хватит int. Исключение одно: в уроке про ручку и вал позиция зажата map() в диапазон 0-2048, там int честно хватает, потому что диапазон ограничен самим источником.
И про честность home(). Возврат по счётчику приводит не к механическому нолю, а к программному: люфт шестерён — порядка градуса, а один сорванный на высокой скорости шаг уводит счётчик навсегда, мы считали пределы. Для столика с камерой этого хватает; для прибора, у которого ноль — физический упор, ставят концевик и ищут ноль им. Отдельная плановая тема — homing по реперу, там концевик и обнуление работают в паре.
Отдельный вопрос — переживание выключения питания. Счётчик живёт в переменной, и после сброса скетч честно считает от нуля, а вал при этом физически где-то стоит: первый же moveToDeg уедет по новой базе. Лечится записью счётчика в EEPROM при остановке и чтением в setup — приём со счётчиком мы уже делали, меняется только размер: под long нужны четыре ячейки вместо двух. Люфт это всё равно не вылечит, но база хотя бы совпадёт с тем, что было на момент выключения.
| Вызов | Аргумент | Что делает | Когда брать |
|---|---|---|---|
| rotateDeg(45) | Градусы, можно с минусом | Относительный ход: 256 шагов от текущего места | Повторяющиеся ходы: кадры, порции подачи |
| moveToDeg(45) | Градусы шкалы | Едет к абсолютной отметке на нужную разницу | Точки позиций: заслонки, шторки, метки |
| home() | Нет | step(-pos) и обнуление счётчика | Старт скетча, конец цикла съёмки |
#include <Stepper.h> const int STEPS_PER_REV = 2048; Stepper stepper(STEPS_PER_REV, 8, 10, 9, 11); long pos = 0; // позиция в шагах: только long long degToSteps(long deg) { long a = deg; // модуль, округление, знак отдельно if (a < 0) { a = -a; } long s = (a * STEPS_PER_REV + 180) / 360; if (deg < 0) { s = -s; } return s;} void rotateDeg(long deg) { long s = degToSteps(deg); stepper.step(s); pos += s;} void moveToDeg(long deg) { long target = degToSteps(deg); stepper.step(target - pos); pos = target;} void home() { stepper.step(-pos); pos = 0;} void setup() { stepper.setSpeed(10); Serial.begin(9600);} void loop() { moveToDeg(45); // качание на 45 градусов delay(2000); moveToDeg(-45); delay(2000);}В degToSteps порядок действий не случайный: модуль берётся до умножения, знак прикладывается к готовому числу шагов. Прибавка 180 — округление к ближайшему шагу; на отрицательных углах она работает навыворот, и обходится это именно разбором модуля и знака. Умножение живёт в long: 45 × 2048 = 92 160, а int уже при 16 градусах лопается, мы считали пределы типов в своё время.
У moveToDeg есть честный предел: он едет по линейной шкале, без заезда через ноль. С позиции 270° команда moveToDeg(45) поведёт вал назад на 225 градусов, а не вперёд на 135, хотя вторая дорога короче: функция не знает, что шкала круговая. Если механизм крутится по кругу — считайте разницу по модулю 2048 сами и выбирайте короткую дугу; для заслонок и шторок линейности хватает за глаза.
Качание в loop() — это moveToDeg(45) и moveToDeg(-45) с паузами: 512 шагов на цикл, при 10 RPM полторы секунды движения из четырёх. И обратите внимание: парные качели возвращают счётчик к нулю сами — long раскрывается на тех механизмах, где ходы односторонние или суммируются. Возврат в ноль после цикла — home() одной строкой.
И про границы ответственности функций. degToSteps — единственное место с арифметикой; rotateDeg и moveToDeg зовут её и ведут счётчик, а home() вообще не считает — он просто откатывает вал на минус pos. Если когда-нибудь поменяете мотор на полушаговый вариант на 4096 шагов, править придётся одну константу и одну формулу: в этом весь выигрыш обёрток — они превращают смену железа из аудита всего скетча в правку одной строки.
Проверяем в симуляторе
В симуляторе те же обёртки и две кнопки: D2 — кадр съёмки, плюс 45 градусов, D3 — home. Нажимайте D2 и смотрите Serial: pos= растёт по 256 шагов, deg= печатает угол. Счётчик здесь long, поэтому нажимайте плюс сорок пять хоть сто двадцать восемь раз подряд — на настоящей плате с int ровно на этом ходу счётчик перевернулся бы, у меня это и случилось.
Кнопка D3 возвращает в ноль: вал проедет ровно минус pos шагов, счётчик обнулится. Попробуйте плюс, потом home, потом снова плюс — симметрия ходов видна на циферблате без всяких измерений. И сравните deg= в отчёте с позицией стрелки: расхождение там только в пределах округления к ближайшему шагу.
Печать deg= здесь идёт из long-счётчика с делителем с точкой — та же формула, что мы отлаживали во втором уроке, только теперь она живёт в функции report(). Если прикинуть в уме: чтобы дожать int-счётчик из сцены до переворота, нужно было нажать плюс сорок пять сто двадцать восемь раз подряд — в симуляторе это пара минут, и вы своими глазами увидите, что с long счётчик проезжает отметку без единого симптома.
- 1. Почему столик поехал назад после 128 кадров съёмки?
- 2. Что делает rotateDeg(45)?
- 3. Сколько полных оборотов вала помещается в int-счётчик?
- 4. moveToDeg(45) вызван с позиции 270°. Куда поедет вал?
- 5. Что вернёт home() без концевика?
- Арифметика поворотов живёт в одной функции: один источник правды, минус не забудется
- rotateDeg — относительный ход, moveToDeg — абсолютная отметка по счётчику, home — возврат в ноль
- Позиция, цель и разница — только long: int лопается на 32 767 шагах, это 16 оборотов вала
- Округление к ближайшему: (a × 2048 + 180) / 360, модуль до умножения, знак к готовому числу
- home по счётчику — программный ноль: люфт и срыв уводят его от механического, точный ноль ставит концевик
Трио управления собрано: ручка задаёт цель, кнопки шагают по шкале, функции держат арифметику в одном месте. Дальше по плану раздела — AccelStepper: разгон и торможение, неблокирующий run() вместо запирающего step() и profiles движения. А пока соберите свой интерфейс из этих трёх деталей и погоните счётчик в Serial: переполнение лучше поймать на столике, а не на съёмке.
Частые вопросы по теме урока
- Почему столик поехал назад после 128 кадров съёмки?
- 128 кадров по 256 шагов = 32 768, а предел int — 32 767. Счётчик перевернулся в минус, и логика moveToDeg повела вал по врённой позиции. Мотор тут ни при чём. Счётчик int перевернулся на 32 768-м шаге
- Что делает rotateDeg(45)?
- rotateDeg — относительный ход: плюс 256 шагов от того места, где вал стоит. Абсолютную отметку на шкале обеспечивает moveToDeg. Едет на 45 градусов от текущего места
- Сколько полных оборотов вала помещается в int-счётчик?
- 32 767 шагов делим на 2048 — почти ровно 16 оборотов. Односторонний механизм переполняет int молча, за часы или дни. Шестнадцать
- moveToDeg(45) вызван с позиции 270°. Куда поедет вал?
- moveToDeg едет по линейной шкале без заезда через ноль: разница 256 - 1536 = минус 1280 шагов. Круговую логику с выбором короткой дуги пишут сами по модулю 2048. Назад на 225 градусов по линейной шкале
- Что вернёт home() без концевика?
- home() едет на минус pos шагов и обнуляет счётчик. Люфт и сорванный шаг уводят программный ноль от механического — для точного нуля нужен концевик. Программный ноль по счётчику позиции