Шторку на окне я посадил на 28BYJ-48 и месяц гордился, пока не забыл про упоры. Автомат «открой полностью» гонит мотор в край: редуктор трещит, шаги срываются, пластиковая шестерёнка выламывает зубья — а скетч об этом не знает и продолжает слать команды. Счёт шагов врёт с первого касания упора, позиция уплывает, и через неделю шторка стоит наполовину, потому что половина шагов ушла в пластик. То же самое ждёт заслонку, слайдер и каретку: у всякого линейного хода есть края, и мотор про них спрашивать некому.
Спрашивать будут концевики: два тактовых или рычажных выключателя на краях хода, сигнальные ноги в D2 и D3 с INPUT_PULLUP. Доехали до края — digitalRead увидел замыкание, автомат глушит мотор и, если задача того просит, даёт реверс. Одна тонкость отделяет рабочий автомат от дребезжащего: пока каретка стоит на краю, концевик остаётся замкнутым, и наивный код будет бесконечно «стоповать» уже стоящий мотор. Лечится гистерезисом: первые сорок шагов отъезда краю не верим. Ниже — скетч с командами из Serial и разбор ловушки.
Два концевика, порции шагов и отъезд
Блокирующий flap.step(2048) в этой задаче запрещён: пока он крутится, концевик мёртв, и стоп возможен только по окончании — то есть никогда. Поэтому порции: slide.step(4) съедает около сорока миллисекунд при 5 RPM, и каждую порцию цикл успевает спросить оба концевика. Четыре шага при 2048 на оборот — это 0,7 градуса: мимо концевика на такой точности не проскочишь, а отзывчивость остаётся на уровне десятков миллисекунд. Анатомию мотора и его редуктора мы разбирали в уроке про 28BYJ-48.
Гистерезис живёт в счётчике sinceEdge: шагов с момента последнего края. Пока он меньше сорока, сигналы концевиков игнорируются — каретка ещё стоит на замкнутом выключателе, и «стоп» означал бы вечный паралич. Сорок шагов — это семь градусов: физически каретка уже съехала с рычага, и краевой сигнал снова становится честным. Число подбирается под механику: больше рычаг — больше запас, я ставил от сорока до шестидесяти.
#include <Stepper.h> const int STEPS_PER_REV = 2048;Stepper slide(STEPS_PER_REV, 8, 10, 9, 11); const int PIN_TOP = 2; // верхний концевик: замкнут у верхнего краяconst int PIN_BOT = 3; // нижний: замкнут у нижнего краяconst int HYST = 40; // шагов отъезда, прежде чем верить краю снова int pos = 0; // шаги от нижнего краяint dir = 0; // 0 стоим, -1 вниз, 1 вверхint sinceEdge = 100; // стартуем "вдали от краёв" bool hitTop() { return digitalRead(PIN_TOP) == LOW; }bool hitBottom() { return digitalRead(PIN_BOT) == LOW; } void setup() { pinMode(PIN_TOP, INPUT_PULLUP); pinMode(PIN_BOT, INPUT_PULLUP); slide.setSpeed(5); Serial.begin(9600); Serial.println("u - вверх, d - вниз, s - стоп");} void loop() { if (Serial.available() > 0) { int cmd = Serial.read(); if (cmd == 'u') dir = 1; if (cmd == 'd') dir = -1; if (cmd == 's') dir = 0; } if (dir == 0) return; // стоим: мотор и концевики молчат slide.step(4); // порция 4 шагов, ~40 мс при 5 RPM pos += dir * 4; sinceEdge += 4; if (dir > 0 && sinceEdge > HYST && hitTop()) { dir = 0; Serial.print("верхний край, pos="); Serial.println(pos); } if (dir < 0 && sinceEdge > HYST && hitBottom()) { dir = 0; pos = 0; // нижний край - наш ноль отсчёта Serial.println("нижний край, pos=0"); }}Три состояния живут в одном int dir, и вся логика — два if с условием по направлению. Ехали вверх, концевик замкнулся и гистерезис пройден — стоп. Команда «вниз» после края отрабатывает честно: первые сорок шагов краевой сигнал игнорируется, потом каретка съезжает с рычага и контроль возвращается. Нулём назначен нижний край: даже если счёт уплыл после срыва, доезжание до нижнего концевика обнуляет координату — тот же приём, что в уроке про homing, только там ноль ищется при старте, а здесь живёт постоянно.
Обратите внимание на порядок условий: sinceEdge проверяется раньше digitalRead. Поменяете местами — при команде реверса с края первое же чтение концевика снова остановит мотор, и вы будете любоваться автоматом, который не может покинуть край. Этот порядок и есть гистерезис, всё остальное — декорации.
- Концевик — обычная кнопка: одна нога на пин с INPUT_PULLUP, вторая на GND, никаких резисторов
- Для краёв хода берите нормально замкнутые модели: обрыв провода выглядит как сработавший край, и автомат не долбит упор по живой механике
- Порции шагов по 2-8 шагов дают отзывчивость 20-80 мс; целые обороты через step() превращают концевик в украшение
- Нижний край назначьте нулём: обнуление позиции по концевику лечит накопленный срыв шагов
- Гистерезис 40-60 шагов на 28BYJ-48 (7-10 градусов) покрывает любые рычаги; для короткоходных оптопар хватит и восьми
- 1. Каретка стоит на нижнем концевике, ему дана команда «вниз». Что делает гистерезис?
- 2. Почему шаги идут порциями по 4, а не flap.step(2048)?
- Концевик — кнопка у края: INPUT_PULLUP, замкнут — LOW, вторая нога на GND
- Стоп на краю работает, только пока loop жив: шаги порциями по 2-8, не блокирующий step() на весь ход
- Гистерезис — счётчик шагов от края: пока он мал, краевой сигнал не верят; проверяется до digitalRead
- Нижний край — ноль координаты: доезжание до него стирает накопленный срыв шагов
Частые вопросы по теме урока
- Каретка стоит на нижнем концевике, ему дана команда «вниз». Что делает гистерезис?
- Концевик остаётся замкнутым, пока каретка на рычаге. Без счётчика шагов с края первое же чтение снова остановит мотор — вечный паралич у упора. Игнорирует краевой сигнал первые 40 шагов отъезда
- Почему шаги идут порциями по 4, а не flap.step(2048)?
- Блокирующий step(2048) при 5 RPM держит loop четыре минуты: за это время концевик ничего не спасёт. Порция в 40 мс даёт стоп за долю секунды. Порция оставляет loop() живым: концевики и Serial опрашиваются каждую итерацию