Лифт столика для микросъёмки прожил три дня и однажды утром не доехал вниз. Причина выяснилась быстро и дорого: очередной сценарий из очереди попросил на два шага больше, чем позволяла направляющая, и каретка ночь упиралась валом в заглушку — ULN2003 честно давил обмотками, счёт позиции оставался в норме, а пластиковая шестерня редуктора получила люфт, который слышен до сих пор. Сценарий был бы безупречен на бумаге. Механизму без датчика на границе я с тех пор не доверяю, даже собственному коду.
Концевик — это стоп-кран: механический контакт, который замыкается раньше, чем вал дойдёт до жёсткого упора. Ставят его на обе границы длинных ходов или хотя бы на опасную; в уроке про homing тот же датчик работает поиском нуля при старте, здесь задача другая — прервать движение, которое программа посчитала легальным. Слои защиты у нас уже есть — программный лимит из урока про лимиты хода ловит промахи сценария, — но сбой питания, подгоревший контакт или чужая рука с кнопкой «поехали» ловит только физический датчик.
Здесь соберём полную цепочку: концевик между D2 и GND с внутренней подтяжкой, проверка после каждой порции шагов, мгновенное обесточивание обмоток, тревожный LED и автоматический отъезд от границы до выхода из зоны срабатывания. Плюс два вопроса, на которых ломаются половины самодельных концевиков: сколько времени мотор проезжает после нажатия и что делать с дребезгом контакта.
Концевик: проводка и логика уровня
Проводка простейшая: одна нога концевика на D2, вторая на GND, pinMode(2, INPUT_PULLUP). Внутренняя подтяжка примерно 20-50 кОм держит пин в HIGH, замыкание контакта тянет его в LOW — и vice versa: разомкнутый контакт читается как HIGH, замкнутый как LOW. Подтяжку к питанию внешним резистором на 10 кОм я ставлю только на длинных линиях за пределами корпуса: в метре провода наводок обычно не хватает, чтобы перевернуть подтянутый пин, а вот болтающийся вход без подтяжки читает погоду — это первое, что проверяю, когда «концевик срабатывает сам».
Проверка датчика вписывается в порционный цикл из первого урока почти без правок: сделал step(4) — спросил у D2, не LOW ли он — и только потом запланировал следующую порцию. Вопрос, который все забывают: сколько мотор проедет после нажатия? Ответ: максимум одна порция. При 10 RPM четыре шага — это около 12 миллисекунд, на таких скоростях инерция ротора с редуктором съедается за ту же порцию. Отсюда правило монтажа: концевик ставится с запасом два-три миллиметра до жёсткого упора — торможение должно закончиться в мягкой зоне, а не в стене. На быстрых осях с NEMA 17 арифметика другая: там порции короче по времени, но скорость выше, и тормозной путь считается отдельно.
Почему не прерывание? Вариант с attachInterrupt честно существует и разобран в уроке про прерывания, но для шаговика он избыточен: прерывание обещает реакцию за микросекунды, а мотор всё равно остановится не раньше конца текущей порции — 10-15 миллисекунд. Опрос между порциями даёт ту же задержку без возни с атомарностью переменных. Прерывание оправдано, когда порции большие или ось быстрая: там разница между 15 мс опроса и мгновенным флагом — это уже сантиметры пути.
#include <Stepper.h> const int STEPS_PER_REV = 2048;const int PORTION = 4;const long LIMIT_STEPS = 1024; // софт-лимит: пол-оборотаconst int STOP_PIN = 2; // концевик между D2 и GND Stepper stepper(STEPS_PER_REV, 8, 10, 9, 11); long pos = 0; // позиция в шагах от нуляint state = 0; // 0 едет, 1 стоп, 2 отъезд, 3 готовunsigned long tMotor = 0;unsigned long tDebounce = 0;int rpm = 10; void deenergize() { // снять ток удержания, ~240 мА digitalWrite(8, LOW); digitalWrite(10, LOW); digitalWrite(9, LOW); digitalWrite(11, LOW);} void setup() { stepper.setSpeed(rpm); pinMode(STOP_PIN, INPUT_PULLUP); pinMode(12, OUTPUT); deenergize(); Serial.begin(9600); Serial.println("Edem do konczevika na D2");} void loop() { unsigned long now = millis(); if (state == 0) { // ЕДЕТ: порция, потом вопрос датчику if (now - tMotor >= 15) { stepper.step(PORTION); pos += PORTION; tMotor = now; } if (digitalRead(STOP_PIN) == LOW && now - tDebounce >= 40) { state = 1; deenergize(); digitalWrite(12, HIGH); // тревога: граница Serial.print("Konczevik! Stop na shage "); Serial.print(pos); Serial.print(" (~"); Serial.print(pos * 360 / STEPS_PER_REV); Serial.println(" gradusov)"); tDebounce = now; } else if (pos >= LIMIT_STEPS) { state = 1; // софт-лимит сработал раньше датчика deenergize(); digitalWrite(12, HIGH); Serial.println("Sof-limit: do konczevika ne doehali"); } } else if (state == 1) { // СТОИТ: ждём, пока границу отпустят if (digitalRead(STOP_PIN) == HIGH) { state = 2; Serial.println("Otjezd nazad do nulya"); tMotor = now; } } else if (state == 2) { // ОТЬЕЗД: назад до нулевой позиции if (pos > 0 && now - tMotor >= 15) { stepper.step(-PORTION); pos -= PORTION; tMotor = now; } if (pos <= 0) { pos = 0; state = 3; deenergize(); digitalWrite(12, LOW); Serial.println("Na nule, obmotki snyaty"); } }}После стопа: обесточить, отъехать, успокоиться
Сам стоп — половина дела. Первое, что я делаю после срабатывания, — обнуляю четыре сигнальных пина через digitalWrite: ток удержания около 240 мА перестаёт греть мотор, и каретка не давит на заглушку мёртвой обмоткой, пока вы идёте за отвёрткой. Приём тот же, что в уроке про отключение катушек, с одной поправкой: вал теряет магнитную фиксацию, поэтому отъезд должен начаться до того, как механизм утянет каретку назад пружиной или грузом — у меня на вертикальном лифте между стопом и отъездом стоит реле тормоза, и без него каретка сползала на пол-оборота.
Отъезд — обязательный жест вежливости к датчику: мотор уходит назад, пока концевик не отпустит и позиция не вернётся к нулю. У оптических концевиков зона срабатывания короткая, у кнопочных контакт возвращается сразу, но уходить я всё равно советую до нуля, а не «до отпускания»: счёт позиции получает якорь, и накопленный за сессию дрейф обнуляется — тот же репер, о котором шла речь в уроке про точное позиционирование. Только после выхода из зоны срабатывания имеет смысл снова разрешать ход вперёд: повторное нажатие в зоне даёт повторный стоп, и сценарий зациклится.
Дребезг контакта — последняя мелочь, способная испортить картину. Механическая пара пружинит: после первого замыкания контакт отскакивает, и «нажатие» выглядит как серия импульсов на 10-20 миллисекунд. В нашей логике это безвредно — повторный LOW в состоянии «стоит» ничего не перезапускает, — но журнал в мониторе замусоривается тройными сообщениями, а если на стоп завязан счётчик аварий, цифры врут. Лечение — фильтр временем: реакция не раньше, чем LOW продержится 40 мс, тот же приём, что в уроке про дребезг кнопок. Вместо кнопочного концевика на улице я давно ставлю геркон с магнитом: контакт не видит влаги и не дребезжит заметно сильнее.
| Слой защиты | Что ловит | Цена промаха |
|---|---|---|
| Софт-лимит позиции | Переполненный сценарий, сбойный счёт | Мёртв при перезагрузке: счёт снова с нуля |
| Концевик на границе | Всё, что не учла программа: сбой, чужая рука | Без запаса до упора — торможение в стене |
| Запас 2-3 мм до упора | Инерцию порции и люфт механизма | Тормозной путь заканчивается на жёстком |
| Обесточивание после стопа | 240 мА удержания впустую, перегрев | Горячий мотор и вдавленный вал в упор |
Граница в симуляторе
Кнопка на D2 играет роль концевика: нажатие — контакт замкнулся на землю, отпускание — контакт вернулся. Мотор стартует с нуля и едет вперёд порциями; потенциометр на A0 задаёт RPM от 5 до 12. Когда вы зажмёте кнопку, автомат перейдёт в состояние стопа: LED на D12 загорится, Serial напечатает позицию в шагах и градусах, обмотки обесточатся.
Отпустите кнопку — автомат отъедет назад до нулевой позиции, снова снимет ток с обмоток и погасит LED. Дальше можно запускать новый заход. Отдельно попробуйте заход без вашего участия: мотор доедет до софт-лимита в 1024 шага и остановится сам — это и есть первая линия обороны, сработавшая раньше физического датчика.
- Поймайте задержку стопа. При 5 RPM зажмите кнопку и посмотрите в Serial позицию срабатывания: она кратна четырём — мотор доехал до конца порции, и это его тормозной путь.
- Убедите софт-лимит. Дайте мотору доехать без кнопки: стоп на 1024 шагах с сообщением про софт-лимит — первая линия обороны сработала раньше физики.
- Сравните скорости. На 12 RPM и на 5 RPM позиция срабатывания при зажатой кнопке может отличаться на пару шагов: так отражается инерция порции на разной скорости.
- Добавьте вторую границу. Кнопка одна, но по аналогии заведите верхний лимит и нижний — по два порога в коде, как на настоящих шторах с двумя концевиками.
- Концевик между D2 и GND с INPUT_PULLUP: разомкнут — HIGH, замкнут — LOW; болтающийся вход читает погоду
- Максимальный перебег после нажатия — одна порция: 4 шага при 10 RPM ≈ 12 мс; концевик ставят за 2-3 мм до упора
- Опрос между порциями даёт ту же реакцию, что прерывание, без атомарных тонкостей: мотор всё равно не остановится мгновенно
- После стопа — обесточить четыре пина: минус ~240 мА удержания и меньше нагрева; отъезд до нуля даёт якорь счёту позиции
- Слои защиты работают вместе: софт-лимит ставят на 10-15% раньше концевика — программа не должна узнавать о стене
- 1. Почему пин концевика конфигурируют INPUT_PULLUP?
- 2. Мотор при 10 RPM едет порциями по 4 шага. Сколько он проедет после нажатия концевика?
- 3. Зачем после стопа обнулять четыре сигнальных пина?
- 4. Концевик смонтировали вплотную к жёсткому упору. Что грозит механизму?
- 5. Чем стоп-концевик из этого урока отличается от homing?
Три урока сложились в одну лестницу: планировщик дал мотору соседей по loop(), очередь дала мотору сценарий, концевик дал сценарию границы. Дальше по маршруту раздела — AccelStepper с разгоном и драйверы с микрошагом, где та же логика живёт внутри библиотеки на скорости под сотню RPM. А пока закрепите последний скетч на настоящей плате: концевик за копейки и полчаса проводки стоят дешевле любой шестерни, которую вы потом не найдёте в продаже.
Частые вопросы по теме урока
- Почему пин концевика конфигурируют INPUT_PULLUP?
- Внутренняя подтяжка 20-50 кОм задаёт уровень без внешних резисторов. Замыкание на землю — самая помехоустойчивая схема для механического контакта. Подтяжка держит пин в HIGH, замыкание на GND даёт LOW: контакт до земли надёжнее висящего в воздухе входа
- Мотор при 10 RPM едет порциями по 4 шага. Сколько он проедет после нажатия концевика?
- Проверка стоит между порциями, поэтому перебег ограничен одной порцией. Отсюда правило: концевик ставят с запасом до жёсткого упора. Максимум порцию: около 12 мс и 4 шага
- Зачем после стопа обнулять четыре сигнальных пина?
- Обесточенные обмотки не держат ток покоя. Цена — вал теряет магнитную фиксацию: для вертикальных осей добавляют механический тормоз. Чтобы снять ~240 мА удержания: мотор не греется и не давит на упор
- Концевик смонтировали вплотную к жёсткому упору. Что грозит механизму?
- Датчик не отменяет инерцию: после LOW мотор проходит порцию. Без запаса в пару миллиметров торможение приходится на жёсткий упор. Тормозной путь порции плюс люфт закончатся уже в стене — удар по редуктору
- Чем стоп-концевик из этого урока отличается от homing?
- Один и тот же датчик, разные сценарии: homing при старте находит репер, стоп-кран во время работы обрывает легальный ход у границы. Здесь датчик — стоп-кран на границе; homing ищет нулевую позицию при старте по тому же датчику