Двухпроходный homing — быстрый проезд к краю и медленный финал — мы собирали в уроке про поиск нуля, и там же честно говорилось, что ноль этот плавает. У моей задвижки плавание было наглядным: каждое утро концевик щёлкал на два-шесть шагов в другом месте, и за неделю набежала разница в полсантиметра. Механика не гуляла — гуляло касание: контакт замыкался, когда каретка ещё ехала, инерция дожимала остальные шаги, и точка «нажат» каждый раз получалась чуть другой.
Этот урок — про момент касания во всей его мелочи. Разберём, почему INPUT_PULLUP обязателен и что дребезг делает с фиксацией нуля, зачем после первого срабатывания отступать на 5-10 шагов и как жить дальше в координатах от нуля. Скетч остаётся мелким, но каждая константа в нём подобрана по чужим граблям — в основном по моим.
Железо то же, что и в прошлый раз: механический микрик за полсотни рублей, одна нога на D2, вторая на GND. Докупать ничего не нужно — весь смысл в том, что происходит в сотне миллисекунд вокруг щелчка.
Почему нельзя просто проехать до упора на полной скорости и обнулиться? Можно — раз в жизни. Упор принимает удар на каждом старте, каретка соскакивает с зубьев на пике, а точка первого срабатывания гуляет на десять шагов из-за разброса контакта. Homing — это процедура с регламентом: скорость подхода, реакция на первое касание, отступ, назначение нуля. Двухпроходный регламент уже собран в соседнем уроке; здесь добираем те части, о которых он сказал «мелочи» — а на них и держится разница между нулём и псевдонулём.
Касание: INPUT_PULLUP и дребезг
pinMode(2, INPUT_PULLUP) — и подтяжка к питанию внутри платы делает вход определённым: не нажат — HIGH, нажат — LOW. Убирать подтяжку нельзя: висящий в воздухе вход ловит наводки от обмоток, и концевик «срабатывает» сам, без всякого рычага — у меня такое случилось на макете с длинными проводами к задвижке. Физику подтяжки и чтение кнопки по шагам разбирал урок про digitalRead и кнопки; здесь её роль строже: от стабильности входа зависит, где механизм назначит свой ноль.
Дребезг контакта длится одну-пять миллисекунд: пара пружинящих отскоков рычага. Для остановки это пустяк — мы не считаем нажатия, мы ловим первое LOW, а оно наступает в первый же зазор. А вот счётчик касаний или индикация «нажат/отпущен» на экране от дребезга сходит с ума: приёмы глушения собраны в уроке про дребезг кнопки. В скетче ниже на дребезг опираться не приходится, и не нужно: остановка реагирует на первый контакт и дальше его не отпускает.
Второй подвох — сам ход в момент касания. Подтяжка даёт мгновенный LOW, но каретка не мгновенна: между «контакт замкнулся» и «мотор реально остановился» драйвер успевает выдать ещё шаги. На медленном подходе их два-три, на быстром — десяток. Отсюда два правила скетча ниже: к концевику едем медленно и после касания отступаем на несколько шагов, прежде чем назначать ноль.
Какой концевик брать? Механический микрик с рычагом и роликом прощает перебег: рычаг изгибается и срабатывает раньше жёсткого касания, а разброс срабатывания у него меньше плунжерного. Кнопочный вариант с плоским штоком требует точной остановки — годится для медленных подходов. Лопасть-флажок на валу, перекрывающая оптопару, — следующий уровень точности, но для первой калибровки микрик за пятьдесят рублей решает задачу целиком.
Длина провода до концевика тоже имеет цену: дальше полуметра подтяжка слабеет против наводок от моторных проводов, и вход начинает дёргаться. Помогают витая пара и отдельная трасса от силовых жгутов, а на длинных линиях — конденсатор десять-сто нанофарад между входом и землёй прямо у платы: фронт чуть замедлится, для концевика это безразлично, наводка уйдёт.
#include <AccelStepper.h> // Каретка 28BYJ-48 через ULN2003. Концевик: нога на D2, вторая на GND.AccelStepper rail(AccelStepper::FULL4WIRE, 8, 10, 9, 11); const int STOP_PIN = 2; // концевик: нажат = LOW (INPUT_PULLUP)const int BACKOFF = 8; // отступ после касания, шаговconst long STROKE = 2000; // рабочий ход от нуля, шаговconst int SEEK_SPEED = 150; // скорость поиска, шагов в секундуconst int RUN_SPEED = 320; // рабочая скорость, шагов в секунду int homed = 0; void setup() { pinMode(STOP_PIN, INPUT_PULLUP); rail.setMaxSpeed(RUN_SPEED); rail.setAcceleration(200); Serial.begin(9600); Serial.println("Poisk nulya..."); home();} void loop() { if (homed == 0) return; // без нуля координаты бессмысленны rail.moveTo(STROKE); // координаты от нуля: 0..2000 rail.run(); if (rail.distanceToGo() == 0) { rail.moveTo(0); } rail.run();} void home() { unsigned long t0 = millis(); // фаза 1: медленный ход к концевику rail.setMaxSpeed(SEEK_SPEED); rail.move(-3000); // в сторону упора, с запасом while (rail.run()) { if (digitalRead(STOP_PIN) == LOW) break; // первое касание if (millis() - t0 > 20000) break; // концевик молчит - стоянка } if (digitalRead(STOP_PIN) != LOW) { Serial.println("Konzevik molchit - homing otklonen"); return; } // фаза 2: инерция дожала - назначаем ноль и отступаем rail.setCurrentPosition(0); // точка касания = ноль (временно) rail.move(BACKOFF); // уводим на 8 шагов от упора while (rail.run()) {} rail.setCurrentPosition(0); // рабочий ноль - в 8 шагах от упора rail.setMaxSpeed(RUN_SPEED); homed = 1; Serial.println("Nol najden, rabotaem ot 0");}Медленный поиск — не перестраховка, а условие повторяемости. Сто пятьдесят шагов в секунду — это 6,7 миллисекунды на шаг: каретка подходит к упору с запасом времени на остановку, и шаги не срываются на последнем сантиметре. Скорость рабочего хода возвращается только после калибровки. Сорвёте шаги на подходе — ноль «втирается» в упор с разной силой, и плавание, с которого начался урок, возвращается через самое точное место скетча — точку касания.
setCurrentPosition(0) — команда, ради которой всё затевалось: она записывает в счётчик текущее значение и гасит движение. В скетче её два вызова, и это не причуда. Первый ставит ноль в точку касания — чтобы у отступа была точка отсчёта. Второй ставит ноль уже в восьми шагах от упора: иначе каждое включение давит каретку в упор на полный ток удержания, а так упор видит только короткое касание на поиске. Величину BACKOFF подгоняйте на месте: поднимайте, пока каретка в нулевой позиции перестанет касаться упора.
Рабочий цикл после home() говорит языком координат: moveTo(STROKE) и moveTo(0). Никаких «минус триста от текущего» — вся арифметика от нуля, и команды совпадают с чертежом механизма. Защиту от выезда за границы размечает урок про лимиты хода: там soft limits держат каретку внутри STROKE даже в том случае, если концевик не доехал.
Отдельная грабля — каретка, припаркованная прямо на концевике. Скетч ждёт перехода «не нажат → нажат» внутри хода: если контакт уже замкнут при старте, фаза поиска кончается в первом же проходе, ноль назначается там, где стояла каретка, и отступ уводит её на BACKOFF от случайного места. Если парковка на концевике для вас норма, проверяйте вход до старта и решайте явно: считать ли текущее место нулём или сначала отъехать и найти упор честно.
Числа для наглядности: ход STROKE в 2000 шагов — это 0,976 оборота 28BYJ-48, середина хода — moveTo(1000), и никакой тригонометрии. На ремённой механике GT2 со шкивом на шестнадцать зубьев оборот тянет 32 миллиметра, один шаг — 15,6 микрона, и те же координаты читаются в миллиметрах после одного деления: пересчёт из шагов в миллиметры я собирал в уроке про ремень и каретку. Пока арифметика в шагах — не смешивайте единицы: одно преобразование на границе, внутри скетча только шаги.
| Стратегия подхода | Скорость калибровки | Стабильность нуля |
|---|---|---|
| Полная скорость до упора | 2-4 секунды | худшая: удар и разброс в 5-10 шагов |
| Один медленный проход | 20-30 секунд | нормальная, но упор принимает ход каждый раз |
| Быстро-медленно, два прохода | 5-8 секунд | хорошая: финал на 100-150 шагов/с |
| Медленный подход плюс отступ 8 шагов | 15-20 секунд | лучшая из простых: касание вне координаты |
Координаты от нуля
После home() весь скетч живёт в координатах от нуля до STROKE, и это главное, что даёт ноль: команды совпадают с чертежом. «Открыть» — moveTo(STROKE), «закрыть» — moveTo(0), «половина» — moveTo(STROKE / 2). Пересчёт «сколько шагов от текущего» исчезает вместе с целым классом промахов, где знак терялся между разворотами. В механиках с двумя валами тот же приём держит обе оси — раскладку пары координат я разбирал в уроке про два мотора и координаты.
Homing при каждом включении — мой выбор без исключений. Хранить ноль в EEPROM и пропускать калибровку заманчиво, но шаги, пропущенные пока питание было выключено, EEPROM не видит: механику могли крутить руками, ремень мог сползти на зуб, концевик мог дребезжать при парковке. Пятнадцать секунд калибровки дешевле одной вмятины; экономия тут мнимая, и я дважды платил за неё деталью.
И про соседей по скетчу: во время home() loop() занят while — это осознанная блокировка на пятнадцать секунд старта, и я её принимаю. Дальше работает обычный run() в живом цикле, шаблон разобран в уроке про неблокирующий run.
AccelStepper в движке симулятора нет, концевик заменяет кнопка D2: нажали — контакт замкнулся на землю, как у настоящего микрика с INPUT_PULLUP. Ход к концевику модель ведёт порциями step() по два шага — это честная замена run() в рамках поддерживаемого API, а обнуление и отступ на восемь шагов после касания скопированы со скетча выше.
Прогон: запустите и смотрите Serial — модель поедет в сторону концевика, отпечатает координату касания, отступит и назначит ноль в восьми шагах от упора. Дальше LED D12 повторяет концевик: нажмите кнопку — и увидите, что нулевая координата лежит на расстоянии BACKOFF от точки касания, а не на самом упоре.
Защита от молчащего концевика в модели та же, что в скетче: если кнопка не нажалась за отведённое время, поиск останавливается с сообщением в Serial — на настоящей плате это спасает каретку от минуты давления в упор. Отпустите кнопку сразу после касания: модель доживёт до рабочего режима и покажет LED, повторяющий концевик. А нажмите заранее, до старта — и увидите мгновенное назначение нуля там, где стояла каретка: та самая грабля припаркованного концевика.
- 1. Какой уровень на входе D2 у нажатого концевика, замкнутого на GND, с INPUT_PULLUP?
- 2. Зачем после первого срабатывания концевика отступать на 5-10 шагов?
- 3. Что делает setCurrentPosition(0) в AccelStepper?
- 4. Концевик NO оборвался. Что сделает каретка без защиты?
- 5. Какая скорость подхода к концевику даёт повторяемый ноль?
- Концевик: нога на D2, вторая на GND, pinMode(2, INPUT_PULLUP); нажат = LOW, без подтяжки вход ловит наводки
- Дребезг в 1-5 мс остановке не мешает: ловим первое LOW, а не считаем нажатия
- Касание и ноль — не одна точка: инерция дожимает 2-10 шагов, отступ BACKOFF 5-10 шагов и второй setCurrentPosition(0)
- Подход к концевику — на 100-150 шагов/с; рабочая скорость возвращается только после калибровки
- Вся работа — в координатах 0..STROKE через moveTo; homing при каждом включении дешевле одной вмятины
Частые вопросы по теме урока
- Какой уровень на входе D2 у нажатого концевика, замкнутого на GND, с INPUT_PULLUP?
- Подтяжка прижимает вход к питанию, концевик замыкает его на землю: не нажат — HIGH, нажат — LOW. LOW
- Зачем после первого срабатывания концевика отступать на 5-10 шагов?
- Между замыканием контакта и остановкой драйвер выдаёт ещё шаги. Отступ выносит рабочий ноль из зоны разброса. Инерция дожимает каретку после касания
- Что делает setCurrentPosition(0) в AccelStepper?
- Команда записывает в счётчик заданное значение (текущая позиция = 0) и останавливает мотор на месте. Обнуляет счётчик позиции и гасит движение
- Концевик NO оборвался. Что сделает каретка без защиты?
- Обрыв для NO выглядит как «концевик не нажат»: каретка едет дальше. NC-концевик при обрыве даёт ложное срабатывание — и каретка встанет у края. Проедет мимо и вдавится в упор
- Какая скорость подхода к концевику даёт повторяемый ноль?
- На быстром подходе шаги срываются и упор принимает удар — ноль плавает. Финальный проход делают медленным. Медленная, 100-150 шагов/с