Второй шлагбаум я делал для въезда в гараж, где стрела стоит вплотную к стене, и PIR-датчик из первой версии был обречён: его конус цеплял стену и полку с вёдрами, и стрела поднималась на каждую кошку в радиусе шести метров. Замена напрашивалась сама — дальномер HC-SR04, который меряет честную дистанцию до борта машины. Порог «ближе двадцати сантиметров — поднимать» я написал за десять минут, и до утра всё выглядело идеально. А утром машина остановилась ровно на границе: показания прыгали через порог туда-сюда, и стрела ходила как качели, пока водитель не доехал наконец до конца.
У урока про шлагбаум на PIR-датчике другой триггер: там датчик докладывает движение, а таймер тишины решает, когда опустить стрелу. Здесь сцена другая — въезд в стену, где вопрос звучит не «кто-то движется?», а «объект у стрелы и когда он ушёл?». Ответ на него лежит не в одном пороге, а в двух, и это главный сюжет урока.
Соберём сценарий целиком: два порога дистанции, тайм-аут опускания на случай залипшего датчика и автомат из четырёх состояний, в котором нет ни одного delay. В симуляторе ниже машину изображает ползунок препятствия — вся механика сценария видна вживую.
Один порог — плохой переключатель
Почему машина на границе ломает сценарий: показания дальномера никогда не стоят на месте. Капот отражает звук под углом, сквозняк гонит пыль, сам модуль шумит на единицу-две сантиметра — у моего стенда показания на неподвижном препятствии гуляли в пределах пары сантиметров. Порог один, и каждое колебание 19–21 см пересекает его: сначала «ближе — поднять», через секунду «дальше — опустить», и стрела отрабатывает обе команды. Датчик честен, сценарий — нет: он спросил у аналоговой величины цифровой вопрос.
Лечится разносом порогов, и приём ровно тот же, что в уроке про гистерезис для ручки, только роль другая. Там мёртвая зона в три единицы АЦП плавала вокруг текущего положения — серво не ехала на мелочи, но ехала куда угодно дальше. Здесь два фиксированных порога, прибитых к сцене: подъём при дистанции меньше 20 см, опускание при больше 35. Вся зона между ними означает «держим как было» — ни одна дрожь показаний не переведёт стрелу сама.
Числа я выбрал не на глаз. Дрожь показаний в 1–2 см разнос в 15 см перекрывает с семикратным запасом; слепая зона модуля начинается от двух-трёх сантиметров, поэтому нижний порог не имеет смысла ближе десяти; а габарит машины от капота до лобового — метр с лишним, так что зона между порогами меньше самой машины: она не может «жить» внутри неё. Пороги подбираются под место за один прогон, о способе — в конце урока.
Автомат из четырёх состояний без единого delay
#include <Servo.h> Servo strela; const int PIN_TRIG = 2; // триг дальномераconst int PIN_ECHO = 3; // эхо дальномераconst int PIN_LED = 12; // проезд разрешён const int OTKR_CM = 20; // ближе — подниматьconst int ZAKR_CM = 35; // дальше — опускатьconst unsigned long HOD_MS = 600; // пауза на доезд стрелыconst unsigned long MAX_OPEN_MS = 12000; // тайм-аут открытогоconst unsigned long T_ZAMER = 60; // период замеров int sost = 0; // 0 zakryt, 1 podem, 2 otkryt, 3 spuskint dist = -1; // -1 — эха не былоunsigned long tSost = 0;unsigned long tOpen = 0;unsigned long tMer = 0;unsigned long tLog = 0; int zamer() { digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2); digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PIN_TRIG, LOW); unsigned long us = pulseIn(PIN_ECHO, HIGH, 25000); if (us == 0) return -1; // эха нет — это НЕ нуль сантиметров return us / 58;} void setup() { strela.attach(11); strela.write(20); pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT); pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("Shlagbaum: porogi 20/35, timeout 12 s");} void loop() { unsigned long now = millis(); if (now - tMer >= T_ZAMER) { tMer = now; dist = zamer(); } if (sost == 0) { if (dist > 0 && dist < OTKR_CM) { sost = 1; tSost = now; tOpen = now; strela.write(110); digitalWrite(PIN_LED, HIGH); Serial.println("podyem"); } } else if (sost == 1) { if (now - tSost >= HOD_MS) { sost = 2; Serial.println("otkryt"); } } else if (sost == 2) { if (dist > ZAKR_CM) { sost = 3; tSost = now; strela.write(20); digitalWrite(PIN_LED, LOW); Serial.println("spusk: obekt ushel"); } else if (now - tOpen >= MAX_OPEN_MS) { sost = 3; tSost = now; strela.write(20); digitalWrite(PIN_LED, LOW); Serial.println("timeout: vynuzhdeny spusk"); } } else { if (now - tSost >= HOD_MS) { sost = 0; Serial.println("zakryt"); } } if (now - tLog >= 1000) { tLog = now; Serial.print("sost="); Serial.print(sost); Serial.print(" dist="); Serial.println(dist); }}Пройдём по решениям. Замер выполняется раз в 60 мс по собственному таймеру, а не в каждом проходе: у эха с таймаутом 25 мкс на сантиметр дальний пустой двор — это миллисекунды ожидания, и десяток замеров в секунду хватает с огромным запасом для машины, которая ползёт со скоростью пешехода. Читаться датчик чаще не мешает, но и не помогает: дребезг на границе лечится не частотой опроса, а разносом порогов.
Показание -1 — правило для любого асинхронного входа из PIR-урока: не верю — держу как было. Эхо могло уйти в сторону от наклонённого капота, капля могла повиснуть на излучателе — сценарий не обязан угадывать, он обязан сохранять состояние. О том, почему ноль от pulseIn нельзя трактовать как нуль сантиметров, — отдельная заметка ниже, это моя любимая грабля этого проекта.
Тайминг цикла считается от механики. SG90 проходит 90 градусов за 0,15 секунды — урок про скорость серво давал 0,1 с на 60°, — поэтому пауза хода 600 мс держит четырёхкратный запас даже для залежавшегося экземпляра с задубевшей смазкой. Полный пустой цикл «поднялся и опустился» — 1,2 секунды; водителю проще отсчитать его по стреле, чем по секундомеру. Углы 20 и 110 — те же, что в PIR-версии: стрела на верхнем положении уходит из зоны обзора датчика, чтобы не измерять саму себя.
Механику эха я сжал в функцию zamer(): триг 10 мкс, pulseIn с таймаутом, деление на 58. Откуда берутся эти 58 микросекунд на сантиметр и почему эхо дрожит — разбор в уроке про HC-SR04 и pulseIn; для сценария важна одна строка: таймаут обязателен, иначе пустой двор замораживает loop на целую секунду. Замер раз в 60 мс занимает доли этого окна, и кооперативный цикл здесь соблюдён без усилий: единственная длинная операция — печать в Serial — спрятана за секундным таймером и не мешает ни датчику, ни автомату.
| Состояние | Стрела | Светодиод | Переход |
|---|---|---|---|
| закрыт | 20° | погашен | дистанция меньше 20 см → подъём |
| подъём | 20 → 110° | горит | 600 мс — стрела доехала → открыт |
| открыт | 110° | горит | дистанция больше 35 см → спуск; 12 с сверху → принудительный спуск |
| спуск | 110 → 20° | погашен | 600 мс — стрела доехала → закрыт |
Тайм-аут опускания — страховка от датчика, который врёт в сторону «вижу». Капля смолы, паутинка, снег на рупоре — и показания застревают на пятнадцати сантиметрах навсегда: машина уехала, а стрела стоит. Условие now - tOpen >= MAX_OPEN_MS насильно переводит сценарий в спуск с честной записью в лог: на моём тестовом стенде тайм-аут 12 с, на живом въезде его растягивают до 30–60 секунд, чтобы не опустить стрелу перед медленным трактором. Принцип важнее числа: ни один вход не пользуется доверием вечно.
На реальном въезде дальномер ставится перпендикулярно направлению движения, на высоте 25–40 см — конус модуля узкий, градусов пятнадцать, и монтажный угол решает больше, чем прошивка. Конус, направленный вдоль стены, вернёт эхо от неё вместо машины; поверните рупор поперёк въезда — и сценарий станет измерять именно то, зачем его ставили.
В симуляторе датчик идеален: показания стоят как вкопанные, и может показаться, что два порога — перестраховка. На живой плате разница видна в первый же день: эхо от бампера плывёт на пару сантиметров, и однопороговая версия начинает своё качание. Заодно оцените темп: полный цикл с тайм-аутом занимает двенадцать с половиной секунд симуляторного времени — лог в Serial печатает каждую секунду состояние и дистанцию, по нему удобно следить за переходами.
И про грязь, о которой забыли все инструкции: капли воды на излучателе ломают показания наглухо — эхо отражается от капли в паре сантиметров, и датчик уверенно «видит» её вместо машины. Мой гаражный вариант живёт под козырьком из пластикового уголка, снимаемым для протирки: десять секунд обслуживания в месяц против сценария, в котором дальномер докладывает о стоящей капле круглые сутки.
Ручной режим стоит одной ветки: кнопка-дублёр на свободном цифровом пине поднимает стрелу тем же переходом в подъём, что и датчик, — для протирки козырька или визита без машины. Датчик при этом продолжает мерить, и приоритет остаётся за сценарной логикой: кнопка лишь добавляет заказ, а не перехватывает автомат целиком. Заодно оцените запас углов: 20 и 110 совпадают с границами из урока про упоры и constrain не случайно — отход от крайних положений на десяток градусов общая привычка для всех серво-механизмов, от стрелки до заслонки сортировщика.
- Один порог на границе дребезжит: показания HC-SR04 гуляют на 1–2 см, и стрела ходит вверх-вниз на каждой флуктуации
- Гистерезис — два порога: подъём при 20 см, опускание при 35, между ними состояние сохраняется
- Показание -1 — «эха нет»: pulseIn вернул ноль, и это не нуль сантиметров
- Тайм-аут опускания 12 с страхует от залипшего датчика — стрела не стоит поднятой вечно
- Весь цикл на millis: замер раз в 60 мс, ход стрелы 600 мс с запасом против 0,15 с механики, ни одного delay
- 1. Машина остановилась ровно на границе одного порога в 20 см. Что делает однопороговый сценарий?
- 2. Почему пороги подъёма и опускания разнесены на 15 см?
- 3. Что означает ноль, который вернул pulseIn с таймаутом?
- 4. Зачем в сценарии тайм-аут опускания?
- 5. Что происходит в зоне между порогами, например на 28 см?
Частые вопросы по теме урока
- Машина остановилась ровно на границе одного порога в 20 см. Что делает однопороговый сценарий?
- Показания дальномера дрожат на 1–2 см, и каждое колебание через порог — новая команда. Стрела ходит как качели, пока машина не отъедет от границы. Лечится разносом порогов. Водит стрелу вверх-вниз, пока показания прыгают через порог
- Почему пороги подъёма и опускания разнесены на 15 см?
- Зона гистерезиса перекрывает дрожь показаний с запасом: 15 см против 1–2 см шума. Внутри зоны сценарий ничего не делает — держит текущее состояние. Дрожь показаний в 1–2 см при таком разносе не пересекает ни одну границу
- Что означает ноль, который вернул pulseIn с таймаутом?
- Ноль — сигнал «эха нет», а не измеренное расстояние. Его превращают в -1 и держат прежнее состояние. Сравнение нуля с порогом даёт вечно поднятую стрелу на пустом дворе. Эха не дождались — измерения не было
- Зачем в сценарии тайм-аут опускания?
- Капля, снег или паутинка на излучателе застревают на «пятнадцати сантиметрах» навсегда. Тайм-аут переводит сценарий в спуск с записью в лог — ни один вход не пользуется доверием вечно. Если датчик залип в состоянии «вижу», стрела опустится принудительно
- Что происходит в зоне между порогами, например на 28 см?
- Зона между 20 и 35 см — мёртвая зона сценария: подъём уже не нужен, опускание ещё рано. Переходы происходят только при пересечении одного из двух порогов. Ничего — текущее состояние сохраняется