Финальный скетч шторы я собрал быстро: профиль из прошлого урока, кнопка «открыть-закрыть», светодиод-индикатор. Включил — и панель умерла на первом же ходе: кнопки не отвечали, светодиод застыл. Симптом был один в один с панелью на Stepper.h: кнопка жмётся, свет мигает, мотор едет — но всё это по очереди, а не вместе. В коде красовалась строка stepper.runToPosition() — выбрал её, потому что она «сама доводит до цели», и лишь потом понял: она доводит и loop. Функция крутится внутри себя, пока вал не доедет, и по поведению это тот же блокирующий step() из урока про первый поворот, только в новой обёртке.
Забавность в том, что я честно считал скетч неблокирующим: библиотека же «неблокирующая»! Подвёл словарь. Неблокирующим бывает не библиотека, а конкретный вызов. У AccelStepper их четыре — run, runToPosition, runToNewPosition, runSpeed — и два из них запирают loop так же надёжно, как step() у Stepper.h. Слово «неблокирующая» на странице библиотеки относилось к run(), а я прочитал его шире, чем следовало. Разница паузы стоит: run() отдаёт управление после каждого шага, runToPosition() держит его до цели — а цель у шторы дальняя.
Здесь разложим все четыре вызова по полочкам, соберём правильную схему — moveTo один раз, run в каждом проходе, distanceToGo для контроля — и доведём шторку до ума: движение, кнопка и светодиод живут одновременно. К финалу урока панель заработает и в симуляторе: кнопка дописывает маршрут на лету, мигалка не сбивается. Плюс честные грабли: даже при run() в нужном месте долгий delay убивает движение не хуже runToPosition.
Четыре вызова: кто запирает, кто ведёт
run() — рабочая лошадь: максимум один шаг за вызов и сразу возврат. Ей нужен подготовленный маршрут: moveTo(n) назначает абсолютную цель в позициях, move(n) — относительную, от текущей точки: плюс 512 — пол-оборота вперёд, минус 256 — четверть назад. Знак в move — это направление: минус ведёт назад, отдельной функции реверса в библиотеке нет — направление живёт в знаке остатка. Пара moveTo/move не конфликтует: цель в библиотеке одна, последний вызов перезаписывает её — это и есть переназначение маршрута на лету, ровно то, что делает кнопка в скетче ниже. Пара distanceToGo() отвечает, сколько шагов осталось до цели; ноль означает, что маршрут доехан. Из этих трёх деталей собирается вся неблокирующая механика раздела.
runToPosition() — ловушка с приятным лицом: она гонит run() в цикле, пока distanceToGo() не станет нулём, то есть блокирует loop до приезда. runToNewPosition(n) — то же с заездом: сама ставит цель и сама ждёт. Строки короче, скетч чище — а кнопки мертвы на весь переезд. Проверяется просто: поставьте печать после runToPosition() — она выйдет на экран только после приезда вала, и задержка будет равна времени хода, а не нулю. Симптом тот же, что и у step(): Serial молчит, светодиод стоит, панель не опрашивается, и выглядит это как зависание, хотя программа честно ведёт вал.
Четвёртый вариант — runSpeed(): крутит вал на текущей скорости без трапеции, один шаг за вызов, без блокировки. Она хороша, когда профиль не нужен: конвейер, вращающийся столик с постоянным темпом. runSpeed() при этом не знает про цель: крутит, пока не остановите, — для шторы не годится, для мешалки в самый раз. Моё правило выбора короткое: рядом с кнопками, экраном или Serial — только run(); разовая калибровка при включении — допустима runToPosition; между этими двумя полюсами выбирать не из чего.
| Вызов | Блокирует loop? | Когда брать |
|---|---|---|
| run() | нет — один шаг за вызов | движение рядом с кнопками, экранами, Serial |
| runToPosition() | да — до цели | разовая калибровка в setup(): вывести механизм в ноль |
| runToNewPosition(n) | да — ставит цель и ждёт | служебные переезды, когда больше ничего не запущено |
| runSpeed() | нет — один шаг за вызов | постоянный темп без трапеции: конвейер, столик |
#include <AccelStepper.h> AccelStepper stepper(AccelStepper::HALF4WIRE, 8, 10, 9, 11); const int BTN = 2;const int LED = 12; int prevBtn = HIGH; // прошлый уровень кнопки — для фронта void setup() { pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); stepper.setMaxSpeed(500); stepper.setAcceleration(1000); stepper.move(1024); // стартовый ход: четверть оборота от текущей точки Serial.begin(9600);} void loop() { // кнопка добавляет ход — живёт, потому что loop никто не запирает int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { stepper.move(512); Serial.println("Dobavil 512 shagov"); } prevBtn = btn; // светодиод горит, пока есть остаток пути, — индикатор без delay digitalWrite(LED, stepper.distanceToGo() != 0 ? HIGH : LOW); // движение: максимум один шаг за проход stepper.run();}Разбор по слоям. Кнопка читается фронтом: нажатие ловится один раз на переход HIGH-LOW через переменную prevBtn — урок про цифровой ввод кнопки раскладывал это по шагам, а дребезг настоящей кнопки лечится так, как показано в уроке про debounce; в симуляторе фронты чистые, фильтр не нужен. Фронт не украшение, а защита: while с move внутри поймал бы дребезг и раздул маршрут втрое. Заметьте: ни одной строки, отвечающей за ожидание, — движение не прерывается ни на миллисекунду, кнопка просто дописывает маршрут между шагами.
Светодиод привязан к distanceToGo(): есть остаток — горит, доехали — погас. Это первый примитив обратной связи, доступный без датчиков: счёт шагов библиотека ведёт сама. Погасший позже расчётного — тоже информация: маршрут вырос от повторных нажатий, и это видно без всякого графика. В боевых механизмах к нему добавляют репер — концевик или энкодер; про прерывания от энкодера EC11 у нас отдельный урок, но для шторки индикатора хватает за глаза.
Число с условием про Serial: на 9600 бод символ занимает около миллисекунды, строка «Dobavil 512 shagov» — 19 миллисекунд тишины для run(), и вал это время просто стоит. Поэтому скетч печатает только по событию — по нажатию, — а не в каждом проходе. Печать в каждом проходе на 9600 бод разорвала бы движение: тридцать символов — тридцать миллисекунд паузы на каждую строку. Печатайте по событию или раз в 200-250 мс через millis — приём из скетча с профилем скорости. Если печати много, поднимите порт на 115200 бод: там символ стоит около 0,09 миллисекунды, и короткие строки перестают рвать движение.
Пара moveTo — move заслуживает отдельной минуты. moveTo(1024) всегда ведёт в позицию 1024 от нуля: сколько бы вал ни проехал, цель абсолютна. move(1024) прибавляет ход: вызвали трижды по 512 — библиотека сложит их в 1536 и повезёт складывать по дороге. Разница всплывает при повторных вызовах: двойной move(512) уедет на 1024, двойной moveTo(512) — на месте, цель-то одна. Для шторы естественнее move: «ещё пол-оборота» звучит как команда человека, а не координата из чертежа. Для заслонки с калиброванным нулём — moveTo: она ведёт в точку, проверенную концевиком.
Ноль задаётся setCurrentPosition(0): библиотека принимает текущую точку за начало координат, и все дальнейшие цели отсчитываются от неё. Без нуля абсолютные координаты абстрактны: после включения питания библиотека считает нулём то место, где вал стоял в момент подачи питания, — а он там стоял случайно. Отсюда мой ритуал: setup() дёргает вал до упора или до концевика, ставит ноль, и только потом начинается работа. Проверить его легко: после setCurrentPosition(0) currentPosition() обязан печатать ровно 0, что бы ни делал вал до этого.
Читается всё это в loop тремя строками: distanceToGo() — сколько осталось, currentPosition() — где мы, targetPosition() — куда едем. Если цель пришла из Serial числом через parseInt, маршрут вписывается одной строкой move(n) — урок про parseInt показывал, как числа добираются до скетча. Разворот в конце маршрута — ветка с проверкой distanceToGo() == 0: в ней удобно назначать следующую серию, так собираются циклические механизмы без единого delay. В симуляторе ниже та же логика показана без библиотеки: цель, остаток, разворот.
Кооперативный цикл в симуляторе
- runToPosition и runToNewPosition запирают loop до цели — по поведению это step() в новой обёртке
- Неблокирующая схема: moveTo или move назначает цель, run() в каждом проходе ведёт вал, distanceToGo() показывает остаток
- runSpeed() — постоянный темп без трапеции: вариант для конвейеров и вращающихся столиков
- Кнопка живёт рядом с run(), Serial печатает по событию или раз в 200-250 мс: на 9600 бод символ стоит около миллисекунды
- Долгий delay() убивает движение так же, как runToPosition; блокировка легальна в setup() — ноль механизма плюс setCurrentPosition(0)
- 1. Чем runToPosition() отличается от run()?
- 2. Что вернёт distanceToGo() за 100 шагов до цели?
- 3. Кнопка не отвечает, пока вал едет после runToPosition(). Причина?
- 4. Что делает delay(3000) в loop со скетчем на run()?
- 5. Где блокирующий вызов допустим без потерь?
Три урока об AccelStepper уложились в одну схему: конструктор с режимом, пара setMaxSpeed и setAcceleration, маршрут через moveTo — run — distanceToGo. Схема переносится между железом: подставьте режим DRIVER вместо HALF4WIRE — и она же поведёт крупный мотор через A4988, поменяются пины и числа профиля. В симуляторе выше кооперативность видна в чистом виде: мигалка не сбивается, когда вал едет, потому что ни одна строка loop не ждёт дольше одного шага. И это главный вывод, который унесём из урока: loop никто не запирает, каждый проход делает шаг — поэтому кнопки, свет и Serial остаются живыми на любом маршруте.
Частые вопросы по теме урока
- Чем runToPosition() отличается от run()?
- runToPosition() крутит run() внутри себя до нуля distanceToGo: loop заперт на весь переезд. run() делает один шаг и возвращается. runToPosition() не выходит, пока вал не доедет
- Что вернёт distanceToGo() за 100 шагов до цели?
- Остаток до цели в шагах, со знаком по направлению. Ноль означает: маршрут доехан, вал на месте. 100
- Кнопка не отвечает, пока вал едет после runToPosition(). Причина?
- Пока runToPosition() не доехала, следующие строки loop не выполняются: опрос кнопки стоит. Это та же блокировка, что у step(). loop заперт внутри runToPosition()
- Что делает delay(3000) в loop со скетчем на run()?
- run() живёт вызовами: три секунды без вызова — три секунды стоит вал, профиль замер. Догонять библиотека не умеет, времени для неё в паузе нет. Останавливает движение на три секунды: run() не вызывается
- Где блокирующий вызов допустим без потерь?
- В setup() кнопки ещё не нужны: runToPosition() поставит заслонку в ноль, setCurrentPosition(0) закрепит начало координат — дальше только run(). В setup() при выводе механизма в ноль