К дальномеру из прошлого урока у меня осталась претензия, которую я сформулировал на второй неделе: числа в мониторе порта хороши, пока у тебя есть свободный глаз. В гараже свободного глаза нет — он в зеркале, а телефон с монитором валяется на капоте и заводит разговор о начале ДТП. У заводских парктроников подсказка звуковая не для красоты: звук читается боковым зрением и ушами, голова остаётся в зеркалах. Задача этого урока — научить дальномер пищать: дальше метра двадцати — молчит, ближе — пикает всё чаще, вплотную — воит сплошным тоном. По дороге разберём две мои поломки первой версии: парктроник на delay() врал на полметра, потому что во время паузы слеп, и орал при каждом включении, ещё до первого замера, — и обе лечатся парой строк, если знать, куда смотреть. Правило, к которому я пришёл на этом проекте: если устройство обязано дёргать внимание человека, занятого глазами, сигнал уходит в звук — индикатор в углу экрана такие задачи не решает.
Дизайн сигнала: интервал, а не частота
Сначала дизайн сигнала — он важнее кода, потому что код потом просто исполняет договорённость. Звучать расстояние можно двумя способами: менять частоту тона или менять интервал между писками. Частотная шкала — «выше тон — ближе стена» — красиво выглядит на бумаге, но требует уха, натренированного на эталонах: я с ходу не отличал 1,2 килогерца от 1,8, а гараж — не консерватория. Интервальная шкала телеснее: «пик… пик… пик» с учащением читается без всякой тренировки, а сплошной вой — безусловный «стоп», его не перепутаешь ни с чем. Зоны, которые я теперь повторяю по умолчанию: дальше 120 сантиметров — тишина; от 120 до 20 — писк, интервал которого линейно растёт со сближением от 600 до 90 миллисекунд; ближе 10 — сплошной тон.
Линейное растяжение одного диапазона в другой делает map() — та самая функция, что в уроке про диммер на потенциометре разворачивала 0–1023 в яркость светодиода: map(dist, 20, 120, 90, 600). Сразу следом — constrain(): без него map честно улетает за границы на обоих краях, на дистанции десять сантиметров он отдал бы 39 — ниже пола, на двухстах — две тысячи с лишним, выше потолка. Пара map + constrain — это и есть вся математика парктроника; остальное — дисциплина цикла.
Границы зон я подбирал под свой гараж и потом два года не трогал. Полка тишины на ста двадцати сантиметрах — это длина капота с запасом: дальше неё манёвр ещё можно прекратить простым тормозом, и звук тут только отвлекает. Писк начинается там, где тормоз уже не успевает, а сплошной тон стоит на восьми-десяти сантиметрах — дальше начинается то, чего из зеркал не видно. Для детского велосипеда или тележки в узкий коридор шкалу стоит перевернуть вниз: map() остаётся прежним, меняются четыре числа-границы, и на этом правка заканчивается.
Наивная версия: пикнул и уснул
Мой первый парктроник был наивен и честен: замерил, пикнул, уснул. tone(ZUMMER, 2000, 50) — короткий писк на пятьдесят миллисекунд, delay(gap) — пауза из расстояния. Звучало правильно: ближе — delay короче — писк чаще. Врал вот чем: на время паузы скетч слеп. Полсекунды тишины на дистанции за метр — это полсекунды без единого замера, а машина на трёх километрах в час проезжает полметра в секунду: пока скетч спал, расстояние менялось сильнее, чем меняется сигнал. Я заметил это при выезде: парктроник пикнул «далеко», я продолжил выкатываться — и уже по звуку за спиной понял, что сигнал отставал от бампера на корпус. Симптом в журнале тот же, что в уроке про millis: строки идут пачками с дырами по полсекунды — датчик молчит ровно столько, сколько длится delay.
const int TRIG = 12;const int ECHO = 11;const int ZUMMER = 8; void setup() { pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); Serial.begin(9600);} void loop() { digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); long t = pulseIn(ECHO, HIGH, 30000); long dist = t / 58; if (dist < 8) dist = 8; long gap = map(dist, 8, 120, 90, 600); tone(ZUMMER, 2000, 50); delay(gap); // пока спим — не меряем и не печатаем}Рабочая версия: замер и звук живут раздельно
Лечится разлеплением: замер и звук становятся независимыми делами, а скетч не спит вовсе. Опрос идёт на каждом обороте loop с паузой 30 миллисекунд — каждая итерация приносит свежий dist и строку dist= gap= в журнал, дыры исчезают. Писк отложен до millis-гейта: запищим, только если с прошлого писка прошло больше gap миллисекунд. Тот же приём, что в светофоре из урока про конечный автомат: не «сделал и уснул», а «проверил, пора ли». tone(ZUMMER, 2000, 60) хорош тем, что сам затихает: длительность указана третьим аргументом, noTone не нужен. Нужен он в двух других местах — в зоне тишины дальше 120 сантиметров и на выходе из сплошного тона ближе десяти: если из непрерывного тона не выйти явно, зуммер продолжит жить своей жизнью — механику tone() мы разбирали в уроке про зуммер.
Отдельная зона, которую новички забывают, — слепота самого датчика. Если эха нет, pulseIn вернёт ноль, и наивный скетч молча примет это за «далеко». Я считаю это неправдой журнала: слепой датчик должен кричать, поэтому в полной версии ниже нулевой замер раз в секунду даёт низкий писк на 500 герцах — сигнал «меня ослепло, почини мишень». Мнение с границей: если дальномер стоит на роботе, который сам выбирает, куда ехать, эта зона обязательна; в настольном приборчике можно ограничиться печатью dist=-1.
const int TRIG = 12;const int ECHO = 11;const int ZUMMER = 8; unsigned long lastPisk = 0;unsigned long lastSlep = 0;long dist = 400; // старт из безопасной зоны: «далеко» long zamerMks() { digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); return pulseIn(ECHO, HIGH, 30000);} void setup() { pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); Serial.begin(9600); Serial.println("Parktronik: >120 tishina, 20..120 pisk, <=8 sploshnoy");} void loop() { long t = zamerMks(); long gap = 600; if (t == 0) { dist = 400; if (millis() - lastSlep >= 1000) { // слепой датчик кричит lastSlep = millis(); tone(ZUMMER, 500, 300); } } else { dist = t / 58; gap = map(dist, 20, 120, 90, 600); gap = constrain(gap, 90, 600); if (dist > 120) { noTone(ZUMMER); } else if (dist <= 8) { tone(ZUMMER, 2000); // сплошной: без длительности } else if (millis() - lastPisk >= gap) { lastPisk = millis(); tone(ZUMMER, 2000, 60); // писк сам затихнет } } Serial.print("dist="); Serial.print(dist); Serial.print(" gap="); Serial.println(gap); delay(30);}Пара слов о железе звука, чтобы не было сюрпризов на сборке. Пьезо-зуммер из набора новичка — пассивный: ему нужен прямоугольник от tone(), и частотой он управляется честно. Активный зуммер, у которого внутри свой генератор и который пищит от любого HIGH, к парктронику подключить можно — но частотная и интервальная части скетча сведутся к «пищит всегда»: одну ноту он играть обязан. Громкость у обоих фиксированная — слышишь ровно настолько, насколько расчищено отверстие в корпусе; я сверлю четыре миллиметра и не жалею. Пол шкалы в девяносто миллисекунд поставлен не случайно: отдельные вспышки ухо перестаёт различать где-то на десяти-двенадцати в секунду, и трескотня у нижней границы читается ровно как «тормози сейчас». Частоту 2000 герц я выбрал после недели слушания: она звенит громче и противнее низких, а противный здесь — комплимент. Если захочется не писка, а мелодии по зонам — частоты и длительности складываются в массивы, как в уроке про мелодии, только это уже развлечение, а не безопасность.
И проверка сигнала. Пары dist= gap= из полной версии отлично ложатся на плоттер Serial: dist — пила от слайдера препятствия, gap — ступенчатая лесенка, и места, где лесенка упирается в пол 90 и потолок 600, читаются глазами сразу. Мой тестовый маршрут не меняется годами: провести препятствие от 400 до 5 сантиметров и обратно за десять секунд — если лесенка ровная, а вой включился до пятёрки, парктроник готов к корпусу.
| Расстояние | Сигнал | Что делает водитель |
|---|---|---|
| Дальше 120 см | тишина, в журнале gap=600 | едет обычным ходом, датчик молчит |
| 20–120 см | писк с интервалом map(dist, 20, 120, 90, 600) мс | замедляется: чем чаще писк, тем меньше воздуха |
| 10–20 см | писк на полу шкалы — 90 мс, почти трескотня | ползёт на сцеплении, рука на ручнике |
| Ближе 10 см | сплошной тон 2000 Гц без пауз | стоп: дальше начинается бампер |
| Эха нет (t = 0) | низкий писк 500 Гц раз в секунду | почини мишень: датчик рапортует о слепоте |
TRIG и эхо вживую: слепая зона 2 см, конус ±15°, таймаут pulseIn и ткань, глушащая отражение.
Подгружаем интерактив…
- Интервальная шкала телеснее частотной: дальше 120 см — тишина, 20–120 — писк с интервалом map(dist, 20, 120, 90, 600) мс, ближе 10 — сплошной тон.
- delay(gap) делает скетч слепым на время паузы: полсекунды тишины на трёх километрах в час — это полметра без данных. Опрос — на каждом обороте loop, звук — на millis-гейте.
- tone(пин, 2000, 60) сам затихает через 60 мс; noTone нужен в зоне тишины и на выходе из сплошного тона — иначе зуммер живёт своей жизнью.
- Переменные сигнала инициализируйте безопасным значением: dist = 400, а не 0 — иначе скетч до первого замера живёт в вымышленной близости и стучит писком.
- Слепой датчик должен кричать: нет эха — один низкий писк в секунду, а не благородное молчание, которое неотличимо от «далеко».
- 1. Почему парктроник на delay(gap) врал на полметра?
- 2. Что вернёт map(24, 20, 120, 90, 600) перед constrain?
- 3. Парктроник застучал писком сразу после включения, до первого замера. Первое действие?
- 4. Чем tone(ZUMMER, 2000, 60) удобнее связки tone() и noTone() через delay(60)?
Частые вопросы по теме урока
- Почему парктроник на delay(gap) врал на полметра?
- На трёх километрах в час машина проезжает полметра в секунду, и все эти полметра delay() остаются без единого замера. Сигнал отстаёт от бампера — лечится millis-гейтом и опросом на каждом обороте loop. Во время delay скетч не замеряет: полсекунды тишины — это полметра проезда без данных
- Что вернёт map(24, 20, 120, 90, 600) перед constrain?
- 24 попадает внутрь входного диапазона: 90 + (24 - 20) / 100 * 510, целочисленно 110. Constrain нужен на краях: dist=10 отдала бы 39 — ниже пола, dist=400 — выше потолка. 110 — точка внутри диапазона считается линейно
- Парктроник застучал писком сразу после включения, до первого замера. Первое действие?
- Переменная дистанции до первого замера живёт в вымышленной близости: map(0) прижимается к полу шкалы, и скетч честно пищит «стой». Безопасный старт — максимум шкалы или замер до первого решения. Проверить инициализацию dist: ноль map трактует как «вплотную», нужен старт из dist = 400
- Чем tone(ZUMMER, 2000, 60) удобнее связки tone() и noTone() через delay(60)?
- С длительностью в аргументе писк выключается сам, без второго вызова и без задержки в цикле. noTone остаётся для случаев, когда тишину нужно включить принудительно — зона тишины и выход из сплошного тона. Третий аргумент — длительность: зуммер затихнет сам через 60 мс, и loop не блокируется