Створку ворот во двор я перевёл на серво осенью: лёгкая створка, тяга из стальной проволоки, SG90 на кронштейне. Скетч писал write(20), диод «закрыто» зажигался, и месяц всё выглядел безупречно — пока однажды утром я не нашёл створку приоткрытой. Тяга соскочила с качалки ещё вечером, и всё это время плата честно считала ворота закрытыми: команда отправлена, диод горит, отчётность сходится. Никто её не обманывал — она и не спрашивала у механики, чем всё кончилось.
Ставка тут не эстетическая. Эти ворота держали двор от соседской собаки, заходившей по ночам проверять мусорку; сутки открытой створки — это разгромленный мешок с кормом и отпечатки всей этой истории на утренней плитке. С тех пор я формулирую требование жёстче: «написал угол» и «ворота закрыты» — разные утверждения, и второе нужно чем-то подтвердить. Разница между ними стоит один концевик и вечер работы, а не вторую серво и переделку механизма, как кажется издалека.
Вот тут и живёт главная привычка этого урока: write() — приказ, а не отчёт. Библиотека не возвращает «доехала» и не умеет спрашивать створку, где та стоит. Разрыв тяги, севший винт качалки, ветка в направляющей — электрически всё исполнено, механически ничего не случилось. Барьер на дальномере мы собирали в уроке про шлагбаум с двумя порогами, и там тайм-аут страховал сенсор; здесь ниже — второй этаж той же мысли: концевик, который подтверждает физическое состояние, и тайм-аут, который ловит всё, что подтверждения не дождалось.
Разберём, куда лепить концевик и как его читать, соберём конечный автомат из пяти фаз с тайм-аутом на millis, а в конце — про повторные попытки и честную тревогу. Автоматы на именованных состояниях я подробно раскладывал на светофоре в уроке про конечный автомат — здесь та же машина, только ввод у неё не таймер, а железо.
Концевик: контакт, который щёлкает механика
Концевик — кнопка, на которую давит не палец, а сама створка. В закрытом положении кулачок на качалке (или крюк защёлки) нажимает на рычажок, контакт замыкается, и плата впервые за весь цикл получает факт, а не мнение скетча. Читается он как обычная кнопка: пин на INPUT_PULLUP, контакт между пином и землёй, нажат — LOW. Я ставлю копеечные рычажные из старых принтеров и микроволновок: у них длинный рычаг прощает неточность установки на пару миллиметров, чего миниатюрным тумблерам не хватает.
Дребезг контакта здесь лечится тем же способом, что и у кнопки под пальцем, — в уроке про анти-дребезг разобраны все приёмы, мне хватает простого окна в 30 мс. А вот выбор контакта — уже политика. Нормально открытый (НО) замыкается у стены и молчит в остальное время. Нормально закрытый (НЗ) у стены размыкается — и обрыв провода выглядит как «ворота не закрыты», то есть как тревога, а не как тишина. Для ворот за двести метров от розетки я беру НЗ: провод перегрызла собака, коннект выскочил из макетки — автомат это увидит и скажет вслух.
Кулачок на качалке настраивается винтом, и я всегда оставляю створке три-пять градусов запаса до физического упора: концевик должен щёлкать чуть раньше, чем механизм упрётся. Серво, дожимающая створку об раму каждую ночь, работает на стопорном токе — об этом у меня отдельный счёт в уроке про пик тока и сброс платы.
Уличная эксплуатация добавляет контактам две заботы. Первая — влага: рычажный концевик живёт в герметичной коробке со степенью защиты от IP54, а контакты раз в сезон получают каплю контактной смазки — без неё за два года окисление съедает щелчок, и автомат честно рапортует о сбое при закрытых воротах. Вторая — длина линии: на двух метрах макетного провода всё работает, на десяти метрах улицы я тяну витую пару и ставлю керамику 100 нФ прямо на клеммы концевика — без неё наводка от рядом лежащей силовой линии читается как щелчок, и ворота открываются сами.
#include <Servo.h> Servo vorota; // створка, D11; концевик — D2, тревога — D12 const int PIN_KONCEVIK = 2; // INPUT_PULLUP: нажат = LOWconst int PIN_TREVOGA = 12; const byte ZAKRYTO = 0;const byte OTKRIVAEM = 1;const byte OTKRYTO = 2;const byte ZAKRYVAEM = 3;const byte SBOL = 4; byte faza = ZAKRYTO;int tek = 20; // угол закрытых воротunsigned long tHod = 0;unsigned long tFaza = 0;unsigned long tMig = 0;byte popytki = 0;byte migaet = 0; const unsigned long T_HOD = 12; // 1 градус за 12 мс: 90° за 1,08 сconst unsigned long TAIKMAUT = 4000; // запас к ходу почти вчетвероconst unsigned long T_PAUZA = 10000; // пауза между попыткамиconst int ZAKR = 20, OTKR = 110; void setup() { Serial.begin(9600); pinMode(PIN_KONCEVIK, INPUT_PULLUP); pinMode(PIN_TREVOGA, OUTPUT); vorota.attach(11); vorota.write(tek); Serial.println("gotov: o - otkryt, r - sbros trevogi");} void loop() { unsigned long now = millis(); bool koncov = digitalRead(PIN_KONCEVIK) == LOW; // команды из монитора: o — открыть, r — снять тревогу после ремонта if (Serial.available()) { char komanda = Serial.read(); if (komanda == 'o' && faza == ZAKRYTO) { faza = OTKRIVAEM; tFaza = now; Serial.println("faza=otkrivaem"); } if (komanda == 'r' && faza == SBOL) { faza = ZAKRYTO; tFaza = now; popytki = 0; digitalWrite(PIN_TREVOGA, LOW); migaet = 0; Serial.println("trevoga snyata"); } } switch (faza) { case OTKRIVAEM: if (tek < OTKR && now - tHod >= T_HOD) { tHod = now; tek++; vorota.write(tek); } if (tek >= OTKR) { faza = OTKRYTO; tFaza = now; popytki = 0; } if (now - tFaza >= TAIKMAUT) { // подтверждения нет faza = SBOL; tFaza = now; Serial.print("sboj: tek="); Serial.println(tek); } break; case ZAKRYVAEM: if (tek > ZAKR && !koncov && now - tHod >= T_HOD) { tHod = now; tek--; vorota.write(tek); } if (koncov) { // факт закрытия faza = ZAKRYTO; tFaza = now; popytki = 0; digitalWrite(PIN_TREVOGA, LOW); migaet = 0; Serial.println("podtverzhdeno: zakryto"); } if (now - tFaza >= TAIKMAUT && faza == ZAKRYVAEM) { faza = SBOL; tFaza = now; Serial.print("sboj: tek="); Serial.println(tek); } break; case OTKRYTO: if (now - tFaza >= 5000) { faza = ZAKRYVAEM; tFaza = now; Serial.println("faza=zakryvaem"); } break; case SBOL: if (now - tMig >= 300) { // мигаем тревогой tMig = now; if (migaet) { digitalWrite(PIN_TREVOGA, LOW); migaet = 0; } else { digitalWrite(PIN_TREVOGA, HIGH); migaet = 1; } } if (now - tFaza >= T_PAUZA && popytki < 2) { popytki++; faza = ZAKRYVAEM; tFaza = now; digitalWrite(PIN_TREVOGA, LOW); migaet = 0; Serial.print("popytka "); Serial.println(popytki + 1); } break; }}| Контакт | Поведение у стены | Обрыв провода | Когда выбираю |
|---|---|---|---|
| НО — нормально открытый | замыкается: LOW на входе | выглядит как «не закрыто» только при проверке | ворота в метре от платы, провод в трубе |
| НЗ — нормально закрытый | размыкается: HIGH | сразу видно: постоянная тревога | выносные ворота, длинные линии, улица |
| Кулачок на качалке | щёлкает за 3-5° до упора | не применимо | всегда: створка не бьётся о раму |
| Геркон в защёлке | замкнут, пока крюк вставлен | как НЗ — по схеме включения | когда рычагу некуда встать |
В симуляторе концевик играет кнопка: физический концевик остаётся замкнутым, пока створка у стены, поэтому кнопку держите, а не щёлкайте. Не нажали вовремя — увидите sboj в мониторе, мигающий диод и попытку номер два через десять секунд; три неудачи подряд оставляют ворота в аварии до вашего вмешательства, и это осознанное решение: лучше стоящие приоткрытыми ворота, чем серво, давящая на сорванную тягу до третьего часа ночи.
Числа тайм-аута не с потолка. Полный ход у меня 90 градусов при тике 12 мс — это 1,08 секунды; тайм-аут в четыре секунды даёт запас почти вчетверо на подгнившую тягу и холодную смазку, но всё ещё не даёт серво давить на заклинивший механизм дольше пары секунд с запасом. Ставить двадцать секунд я не советую ни для SG90, ни для MG996R: весь этот период серво либо ест стопорный ток, либо греется на удержании — цену обоих режимов я считал в уроке про ток удержания. Весь цикл выше держится на двух таймерах millis и ни разу не засыпает — каркас тот же, что в уроке про millis вместо delay.
Отдельный нюанс фронта: digitalRead ловит уровень, а автомат должен отвечать на состояние фазы. В фазе ZAKRYVAEM уровень «нажат» — ровно то, что нужно; в ZAKRYTO тот же уровень просто подтверждает покой. Если захотите реагировать на событие «щёлкнул», а не на уровень, — это ловля фронта: запоминаем прошлое состояние и сравниваем с ним на каждом проходе, сама по себе миниатюрный автомат из двух состояний.
Отладку я веду по Serial и на столе. Печатайте смены фаз: строка faza=zakryvaem после каждого перехода превращает непонятное поведение в читаемую хронику. Пока автомат не прогнан на столе десять циклов подряд — включая два искусственных сбоя, когда кнопка-концевик просто не нажата, — на ворота его не ставлю. И сезонная поправка к тайм-ауту: летом ход 90° занимает 1,1 секунды, при минус десяти густая смазка растягивает его до полутора-двух; тайм-аут, посчитанный впритык по летнему столу, зимой начнёт ловить сбои на исправных воротах. Мои четыре секунды считались по зимнему двору — летом они просто прощают лишнее.
Углы ZAKR и OTKR я настраиваю без перепрошивки: команды u и d в мониторе двигают текущий угол по градусу, команда s печатает оба рабочих числа — едешь к воротам с ноутбуком, ловишь момент щелчка глазами и записываешь готовые углы. Пять минут на месте экономят вечер пересборки, а заодно проверяется и сам концевик: если при ручном дожиме до щелчка sboj не появляется — контакт уже щёлкает, а автомат его не видит, и дело в проводе или пине, а не в механике.
Попытки, тревога и что делать дальше
Повторная попытка — не перестраховка, а реальный случай: ветка попала в направляющую, первый заход её вытолкнул, второй закрыл ворота. Я ограничиваю счётчик двумя повторами, потому что третий подряд уже не про механику, а про мою невнимательность: разрыв тяги концевик не вылечит. После исчерпания попыток автомат уходит в SBOL и мигает диодом — на реальных воротах сюда просится зуммер или уведомление, но логика останется той же.
Мой чек-лист на вырост: счётчик попыток и состояние фазы стоят памяти — два байта; если тревогу нужно пережить перезапуск питания, их место в EEPROM, а как считать записи бережно — предмет следующего урока. Второй этаж — ручной сброс: в фазе SBOL команда r из монитора возвращает автомат к ZAKRYTO, но только после того, как причина устранена руками; тревога, снимающаяся сама через минуту, учит игнорировать тревоги. Третий этаж — датчик снаружи вдобавок к концевику: пара «сенсор во дворе плюс концевик на створке» закрывает и чужую машину, и сорванную тягу — я ставлю её всегда, когда ворота дороже серво.
И стыковка с расписанием: утренняя створка курятника выходит по будильнику RTC-модуля, вечерняя — по тайм-ауту этого урока, и ни одна новая ветка автомата не понадобится. Как будить плату аппаратным будильником часов, я разбирал в уроке про RTC-будильник — концевик в этой связке остаётся судьёй: расписание приказывает, механика подтверждает.
- write() — приказ без отчёта: факт закрытия даёт только концевик
- НЗ-контакт превращает обрыв провода в тревогу, а не в тишину
- Автомат из пяти фаз на millis: едем по градусу, ждём факт, ловим тайм-аут
- Тайм-аут 4 с при ходе 1,08 с — запас вчетверо; длиннее — уже ток стопора
- Две попытки, потом тревога: третий повтор не лечит разрыв тяги
- 1. Почему нельзя считать ворота закрытыми сразу после write(20)?
- 2. Чем нормально закрытый концевик лучше НО для ворот у забора?
- 3. Ход 90° при тике 12 мс. Какой тайм-аут я ставлю и почему?
- 4. Что делает автомат после двух неудачных попыток закрытия?
- 5. Как читать концевик, чтобы обойти дребезг контакта?
Частые вопросы по теме урока
- Почему нельзя считать ворота закрытыми сразу после write(20)?
- write() не возвращает статус исполнения. Разрыв тяги, соскочивший винт, ветка в направляющей — всё это невидимо без датчика факта. Это приказ: разрыв тяги или клин электрически не видны
- Чем нормально закрытый концевик лучше НО для ворот у забора?
- НЗ разомкнут только у стены; обрыв или выскочивший коннект читаются как «не закрыто» постоянно — автомат это увидит. Обрыв провода выглядит как тревога, а не как покой
- Ход 90° при тике 12 мс. Какой тайм-аут я ставлю и почему?
- Полный ход занимает 1,08 с; четыре секунды покрывают тугую механику, но не дают серво давить на клин дольше необходимого. 4 с — запас вчетверо к ходу 1,08 с
- Что делает автомат после двух неудачных попыток закрытия?
- После исчерпания попыток включается мигающий диод и пауза: разрыв тяги повторами не лечится, нужен человек. Уходит в тревогу и ждёт вмешательства
- Как читать концевик, чтобы обойти дребезг контакта?
- Контакт концевика дребезжит как кнопка: короткое окно 30 мс отделяет щелчок от болтанки. Приёмы — в уроке про анти-дребезг. Окно подтверждения порядка 30 мс