Парктроник я собирал второй раз, и первый вышел глухонемым: HC-SR04 читал расстояние из базового урока про pulseIn, зуммер пищал по одному порогу, как в уроке про парктроник без экрана, — и всё равно прибор в гараже врал. Полоса прыгала на 5-10 см на одном и том же месте, а после включения до первой парковки ловила «призраков» на пустой зоне. Не хватало двух вещей: экрана, на котором расстояние видно боковым зрением, и дисциплины измерения — паузы между пингами, таймаута и фильтра. Первый урок этой пары — про второй пункт: без него экран только красиво показывает мусор.
Зоны — 100, 50 и 20 сантиметров. Дальше метра — тишина и пустая шкала; от метра до полуметра — редкий писк; от полуметра до двадцати — писк всё чаще; внутри двадцати — сплошной тон: парковаться пора было вчера. Смысл не в самих числах, а в плотности: чем ближе препятствие, тем чаще прибор говорит, и в последних полметра молчание становится громче звука. Полоса-градусник дублирует то же самое глазами — заполнение растёт по мере сближения, и габарит ловится взглядом раньше, чем ухо успевает сосчитать писки.
В уроке три части: геометрия полосы из fillRect с метками зон, звуковая кривая — период писка от 500 до 90 мс и сплошной тон внутри 20 см — и главная грабля дальномера, паспортная пауза 60 мс между измерениями, без которой следующий пинг ловит хвост эха предыдущего. Сверху — медиана из трёх замеров: одиночный выброс не проходит, два честных замера всегда сильнее одного вруна.
Полоса и зоны: геометрия ста сантиметров
Шкала привязана к сантиметрам один к одному: 100 пикселей ширины, один пиксель за сантиметр. Рамка drawRect(0, 16, 100, 10), заполнение fillRect(0, 16, zap, 10), где zap = 100 − расстояние, зажатое в 0..100. Метки зон — две вертикальные линии drawLine на 50-м и 80-м пикселях: заполнение пересекает их глазами раньше, чем зуммер успевает дописать свой писк. Направление подобрано под ощущение опасности — чем ближе, тем полнее шкала: перекос не приходится помнить головой, и с водительского места кадр читается за долю секунды. Про построение полос из примитивов я подробнее писал в уроке про прогресс-бар.
Цветных зон у монохромного SSD1306 нет: зелёный, жёлтый и красный превращаются в слова DALOKO, BLIZKO, STOP крупным шрифтом size 2 по центру кадра. Слово дублирует полосу на случай, когда глаз занят рулём: setTextSize(2) стоит ноль, а различимость с трёх метров даёт, — расклад размеров я считал в уроке про размеры шрифтов. Цифру расстояния печатаю в шапке с округлением до сантиметра: точность датчика всё равно плюс-минус несколько, десятые доли тут украшение для покупателей, а не информация.
Частичное обновление сэкономило бы миллисекунды шины: перерисовывать только заполнение и цифру, шапку не трогать — приём из урока про частичное обновление из ОЗУ. Но при кадре раз в 60 мс я сознательно остался на полном кадре: 23-25 мс передачи при 400 кГц оставляют запас вдвое, а код без веток «что изменилось» читается с листа. Экономия станет обязательной, когда на те же A4/A5 сядут часы или второй датчик — тогда каждая посылка делит шину с чужим трафиком, и полный кадр начнёт жать на соседей.
| Зона | Расстояние | Звук | Что делать водителю |
|---|---|---|---|
| DALOKO | дальше 100 см | тишина | кати спокойно: до препятствия больше метра |
| BLIZKO | 100-50 см | писк раз в 500-250 мс | сбавь скорость, включи заднюю передачу |
| BLIZKO | 50-20 см | писк раз в 250-90 мс | ползи по сантиметру, смотри на полосу |
| STOP | ближе 20 см | сплошной тон | тормози: бампер ближе, чем кажется |
60 мс, эхо-хвосты и медиана из трёх
Паспортное ограничение, которое игнорируют в половине туториалов: производители HC-SR04 просят минимум 60 мс между циклами измерения — старое эхо должно рассеяться, иначе следующий пинг ловит хвост предыдущего и возвращает мусор. Датчик шлёт пачку из восьми импульсов 40 кГц, и звук гаснет не мгновенно: при пинге каждые 20 мс новый burst запускается в ещё живой хвост, приёмник ловит не своё эхо, и на дальней зоне появляются показания из воздуха. У меня без паузы полоса прыгала от пустоты к 80 см без единого препятствия — диагноз по таблице задержек, а не по коду. 60 мс — это 16 измерений в секунду: для парковки за глаза, машина не успевает уехать за сотую долю манёвра.
Второй слой защиты — медиана из трёх. Последние три замера лежат в буфере-кольце из трёх ячеек, на экран и в звук идёт средний по величине. Одиночный выброс — капля воды, засветка, чужой ультразвук — медиану не сдвигает: чтобы прибор соврал, врать должны минимум два замера подряд. Среднее арифметическое здесь хуже в разы: один дикий выброс на 200 см утащил бы среднее на десятки сантиметров и дёрнул бы полосу, а медиана его просто не заметила. Если зоны жёсткие — шлагбаум, станок, ограничитель — фильтр расширяется до пяти замеров: цена две строки и лишние 120 мс на цикл.
Скорость звука — не константа, а 331 + 0,6 на градус Цельсия. При 20 °C это 343 м/с и привычные 58 микросекунд на сантиметр туда-обратно; при −20 °C — уже 319 м/с и 62,6 мкс/см, плюс восемь процентов к каждому показанию. Зимой мой гаражный прибор показывал 92 сантиметра там, где рулетка находила 85 — арифметика сходилась с холодом сантиметр в сантиметр. Для парковки восемь процентов несущественны, но если строите из того же дальномера уровнемер или концевик станка, вводите температурную поправку или берите оптический TOF-дальномер — он скорости звука не слышит.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); const int TRIG = 2; // Trig дальномераconst int ECHO = 3; // Echo дальномераconst int ZUM = 8; // активный зуммер int buf[3]; // буфер-кольцо последних трёх замеровint uk = 0;unsigned long tPing = 0;unsigned long tBeep = 0; int med3(int a, int b, int c) { // средний по величине из трёх if (a > b) { int t = a; a = b; b = t; } if (b > c) { int t = b; b = c; c = t; } if (a > b) { int t = a; a = b; b = t; } return b;} void setup() { Serial.begin(9600); pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); pinMode(ZUM, OUTPUT); buf[0] = 100; buf[1] = 100; buf[2] = 100; display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); Wire.setClock(400000);} void loop() { if (millis() - tPing < 60) return; // паспортная пауза: эхо должно рассеяться tPing = millis(); digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); long dur = pulseIn(ECHO, HIGH, 30000); // таймаут 30 мс: потолок 4 м if (dur > 0) { // ноль — эха нет: буфер не трогаем buf[uk] = dur / 58; // мкс в сантиметры uk = uk + 1; if (uk > 2) uk = 0; } int cm = med3(buf[0], buf[1], buf[2]); int zap = 100 - cm; // чем ближе, тем полнее if (zap < 0) zap = 0; if (zap > 100) zap = 100; display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print("RASSTOYANIE " + String(cm) + " sm"); display.drawRect(0, 16, 100, 10, SSD1306_WHITE); // шкала 1 см = 1 px display.fillRect(0, 16, zap, 10, SSD1306_WHITE); display.drawLine(50, 14, 50, 27, SSD1306_WHITE); // метка 50 см display.drawLine(80, 14, 80, 27, SSD1306_WHITE); // метка 20 см display.setTextSize(2); display.setCursor(0, 36); if (cm < 20) display.print("STOP"); else if (cm < 50) display.print("BLIZKO"); else display.print("DALOKO"); display.display(); // звук: чем ближе, тем чаще, внутри 20 см — сплошной тон if (cm < 20) { tone(ZUM, 2400); } else { noTone(ZUM); if (cm < 100) { int per = map(cm, 20, 100, 90, 500); // период писка if (millis() - tBeep >= per) { tBeep = millis(); tone(ZUM, 1800, 40); } } } Serial.print("cm="); Serial.println(cm);}Раскладка пинов не случайна: триг на D2, эхо на D3, зуммер на D8. tone() на Uno крутится на Timer2 и глушит ШИМ на пинах 3 и 11 — посадишь на них диммируемый свет, и он замрёт на последней яркости; полный разбор таймерных конфликтов — в уроке про tone и таймеры, а гамма частот и громкость писка — в уроке про piezo и tone. Активный зуммер здесь намеренно: тон для прибора не главное, полоса важнее, и тратить под динамик два пина и таймер жалко.
Третий аргумент pulseIn — не опция, а страховка: без таймаута функция ждёт эхо до секунды по умолчанию, и когда препятствие уехало за 4 метра, loop провисает, полоса замирает, а кнопки перестают отвечать. Таймаут 30000 микросекунд покрывает весь паспортный диапазон датчика — максимальное эхо с 4 метров возвращается за 23200 мкс. Ноль в ответе значит «эха не было»: в этом случае буфер я не трогаю и показываю последний честный замер — мигание «ноль сантиметров» на пустой зоне нервирует сильнее, чем устаревшее число.
В симуляторе дальномер живой: ползунок 2-400 см, и pulseIn честно отдаёт время эха по 58 мкс на сантиметр. Проведите столбик сверху вниз и следите за тремя вещами сразу: заполнение полосы растёт на клетку за каждые пять сантиметров, слово зоны переключается на 50 и на 20, а паузы между писками укорачиваются — на 100 см раз в полсекунды, на 30 см раз в полторы десятых. Внутри двадцати сантиметров зуммер не отпускает тон вовсе — это и есть «тормози».
Медиану видно на резких движениях ползунка: рваный рывок по шкале прибор сглаживает, потому что средний из трёх замеров всегда запаздывает на два пинга — 120 миллисекунд. Это не баг, а цена фильтра: для парковки задержка невидима, а нервные прыжки полосы она убирает целиком. Живую полосу из настоящих прямоугольников с метками зон вы получите на плате из код-блока выше — движок рисует текстовую сетку, и fillRect в симуляторе остаётся заглушкой.
- 1. Почему производители просят паузу минимум 60 мс между измерениями HC-SR04?
- 2. Что вернёт медиана из трёх замеров 30, 200 и 32 см?
- 3. Насколько заполнена полоса при 20 см, если шкала 100 пикселей и 1 см = 1 пиксель?
- 4. Почему зуммер парктроника посажен на D8, а не на D3?
- 5. Что делает pulseIn(ECHO, HIGH) без третьего аргумента, когда препятствие уехало за 4 метра?
- Шкала 1 см = 1 пиксель: рамка drawRect, заполнение fillRect (zap = 100 − см), метки зон drawLine на 50-м и 80-м пикселях
- Зоны 100/50/20: тишина, писк от 500 до 90 мс, сплошной тон внутри 20 см — плотность информации растёт вместе с опасностью
- 60 мс между пингами — паспортное требование HC-SR04: старое эхо должно рассеяться, иначе дальняя зона ловит призраков
- Медиана из трёх в буфере-кольце глушит одиночные выбросы; среднее арифметическое вранья не замечает и дёргает полосу
- pulseIn всегда с таймаутом 30000 мкс: без него loop провисает до секунды на пустой зоне, ноль в ответе — не расстояние, а молчание эха
Частые вопросы по теме урока
- Почему производители просят паузу минимум 60 мс между измерениями HC-SR04?
- Пачка 40 кГц гаснет не мгновенно: при пингах чаще 60 мс приёмник ловит хвост старого эха и отдаёт мусор на дальней зоне. Старое эхо должно рассеяться, иначе следующий пинг ловит его хвост
- Что вернёт медиана из трёх замеров 30, 200 и 32 см?
- Медиана берёт середину отсортированной тройки: 200 — выброс, и полоса его не заметит, тогда как среднее утащило бы на десятки сантиметров. 32 — средний по величине
- Насколько заполнена полоса при 20 см, если шкала 100 пикселей и 1 см = 1 пиксель?
- zap = 100 − см: на 20 см заполнение 80 пикселей, метка 20 см нарисована на 80-м пикселе шкалы. На 80 пикселей — заполнение растёт по мере сближения
- Почему зуммер парктроника посажен на D8, а не на D3?
- Timer2 стоит за tone(), и пока он пищит, ШИМ на 3 и 11 замирает; D8 с таймерами не связан. tone() крутится на Timer2 и глушит ШИМ на пинах 3 и 11
- Что делает pulseIn(ECHO, HIGH) без третьего аргумента, когда препятствие уехало за 4 метра?
- Таймаут по умолчанию — секунда: loop провисает, полоса замирает. Третий аргумент 30000 мкс закрывает весь диапазон датчика. Ждёт эхо до секунды по умолчанию и возвращает 0