Ультразвуковой дальномер HC-SR04: от эха до парктроника

Время полёта звука, pulseIn с таймаутом, медианный фильтр и превращение расстояния в писк и шкалу огней

HC-SR04 меряет расстояние временем полёта звука, и весь датчик — это два вывода и pulseIn. Разбираем расчёт, поправку на температуру, фильтр ложных отсчётов и парктроник с писком и шкалой на 74HC595.

Задняя парковка без датчиков — это водитель, который выкручивает шею, и бампер, который узнаёт о столбе последним. Парктроник решает задачу одним датчиком и зуммером: чем ближе препятствие, тем чаще писк. Самый доступный датчик для такого проекта — HC-SR04, модуль с двумя цилиндрами-излучателями. Библиотека для него не нужна: всё, что он умеет, укладывается в один импульс и замер его ответа.

Ниже — как датчик меряет расстояние, как это посчитать в коде без ошибок в полпроцента и в полметра, как отсеять ложные отсчёты и как из расстояния сделать писк и шкалу огней.

Как дальномер слышит эхо

У модуля четыре вывода: питание, земля, TRIG и ECHO. Работа идёт в три шага:

  1. Контроллер подаёт на TRIG импульс длиной не меньше 10 мкс.
  2. Модуль излучает короткую пачку ультразвука на 40 кГц и ждёт отражения.
  3. На ECHO он держит высокий уровень ровно столько, сколько звук летел до препятствия и обратно.

Остальное — арифметика. Звук в воздухе при 20 °C идёт со скоростью около 343 м/с, то есть 0,0343 см за микросекунду. Путь двойной, поэтому:

расстояние, см = время, мкс × 0,0343 / 2 ≈ время / 58.

Отсюда удобное число: 58 микросекунд эха на каждый сантиметр. Препятствие в 40 см даёт импульс около 2320 мкс, в 4 м — около 23 мс. Рабочий диапазон модуля — от 2 до 400 см: ближе он не успевает переключиться с передачи на приём, дальше отражение тонет в шуме.

Замер в коде: импульс и таймаут

Пример измерения расстояния на ESP32 делает ровно это:

float measureCm() {
  digitalWrite(TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG, LOW);

  unsigned long us = pulseIn(ECHO, HIGH, 30000);
  if (us == 0) return -1;          // эхо не пришло
  return us * 0.0343 / 2.0;
}

Короткий LOW в начале гарантирует чистый фронт импульса, даже если вывод по какой-то причине остался в единице. Главное здесь — третий аргумент pulseIn. Это таймаут в микросекундах: если импульс не пришёл или не закончился за 30 мс, функция возвращает 0. Без него по умолчанию она ждёт целую секунду, и отвалившийся провод превращает прибор, опрашивающий датчик трижды в секунду, в прибор, который отвечает раз в секунду. 30 000 мкс / 58 ≈ 517 см — с запасом больше дальности датчика, так что настоящий замер таймаут не обрежет.

pulseIn блокирует программу на всё время замера. При 4 м это 23 мс, при отсутствии эха — все 30. Для парктроника это допустимо, для программы, которая параллельно обслуживает дисплей и кнопки, — уже заметно.

На Uno в примере парктроника со шкалой расчёт сделан целыми числами:

return us ? (us + 29) / 58 : 999;   // с округлением, а не отбрасыванием

У ATmega328P нет аппаратной плавающей точки, и каждое умножение float — сотни тактов программной эмуляции. Деление на 58 целое и быстрое, а прибавленные 29 — половина делителя — превращают отбрасывание дробной части в округление: 2349 мкс дадут 41 см, а не 40.

Скорость звука зависит от температуры

0,0343 см/мкс верно при 20 °C. Скорость звука растёт примерно на 0,6 м/с на каждый градус: c ≈ 331,3 + 0,606 × T. Если скетч считает по 343 м/с, а на улице другая температура, показания уплывут:

ТемператураСкорость звукаНастоящие 100 см покажутся как
−20 °C319 м/с107,5 см
0 °C331 м/с103,5 см
20 °C343 м/с100 см
35 °C352 м/с97,3 см

Для парктроника три-семь сантиметров на метре не важны: границы зон всё равно выбраны с запасом. Для измерителя уровня воды в баке или роста ребёнка на дверном косяке поправка уже нужна, и датчик температуры рядом с дальномером её даёт.

Любопытно, что и сам делитель 58 соответствует не 343, а примерно 345 м/с. Разница в полпроцента видна в симуляторе: модель датчика выдаёт ровно 58 мкс на сантиметр, и скетч с множителем 0,0343 на 40 см печатает 39.8.

Ультразвук: измерение расстояния

Поставьте дальномеру 40 см: в порту будет 39.8 см; замените формулу на us / 58.0 — станет 40.0, потому что модель датчика считает 58 мкс эха на сантиметр

Открыть дальномер

Ложные отсчёты и медиана из трёх

У настоящего ультразвукового датчика одиночные промахи — норма. Звук отразился от наклонной поверхности мимо приёмника, пришло эхо от предыдущего замера, мягкая ткань поглотила пачку — и один отсчёт из десятка оказывается диким. Если усреднять, выброс портит результат. Пусть три замера дали 52, 400 и 51 см: среднее — 167,7 см, парктроник замолкает на полсекунды у самого столба.

Медиана выбрасывает выброс целиком. Пример медианного фильтра находит средний из трёх двумя сравнениями, без сортировки:

float a = once(); delay(20);
float b = once(); delay(20);
float c = once();
if (a > b) { float t = a; a = b; b = t; }   // теперь a <= b
if (b > c) b = c;                           // b = min(b, c)
return a > b ? a : b;                       // max(a, min(b, c))

Для тех же 52, 400 и 51 медиана равна 52. Цена — три замера вместо одного и задержка около 50 мс. Функция once() при отсутствии эха возвращает 400: так «вне диапазона» участвует в медиане как очень большое расстояние, а не как ноль, который означал бы «вплотную».

От расстояния к писку

В парктронике на ESP32 расстояние превращается в период писка:

if (cm > 100) { ledcWriteTone(0, 0); delay(100); return; }
// Ближе 10 см — сплошной тон: «стоп».
if (cm < 10) { ledcWriteTone(0, 2000); delay(50); return; }
// 10 см -> писк каждые 60 мс, 100 см -> раз в секунду.
unsigned long period = map((long)cm, 10, 100, 60, 1000);

На 50 см: 60 + (50 − 10) × 940 / 90 ≈ 477 мс между писками. Сам писк — 30 мс тона 2000 Гц через ledcWriteTone.

Здесь есть ловушка, которую стоит знать про map(): она не ограничивает результат диапазоном. На 4 см получается 60 + (4 − 10) × 940 / 90 = −2, на 3 см — −13. Отрицательное число, записанное в unsigned long, превращается в огромное, условие «пора пищать» перестаёт выполняться, и парктроник замолкает именно в самой опасной зоне, у бампера. Поэтому в примере всё, что ближе 10 см, отсекается до map() и превращается в сплошной тон, как у настоящих парктроников. Другой способ — constrain(cm, 10, 100) перед map(); важно одно: на вход map() не должно попадать значение за пределами диапазона.

Версия на Uno так и сделана. Кроме писка, у неё шкала из восьми светодиодов на сдвиговом регистре 74HC595: три провода вместо восьми выводов. Данные уходят по аппаратному SPI (DS на D11, SHCP на D13), защёлка RCLK — на D10. Число огней считается так:

int lights = d >= FAR_CM ? 0 : map(constrain(d, STOP_CM, FAR_CM), FAR_CM, STOP_CM, 1, 8);
byte bits = n <= 0 ? 0 : (n >= 8 ? 0xFF : (1 << n) - 1);   // n младших битов в единицу

Пауза между писками — d * 8 миллисекунд, а ближе 10 см тон становится сплошным. Что покажет прибор на разных расстояниях:

РасстояниеОгнейПауза между писками
150 см0, в порту «чисто»тишина
80 см2640 мс
40 см5320 мс
20 см7160 мс
10 см и ближе8сплошной тон

Arduino Uno: парктроник со шкалой на 74HC595 и зуммером

Уменьшайте расстояние в свойствах дальномера: 80, 40, 20 см — в порту будет огней 2, 5 и 7; затем поменяйте FAR_CM на 150 и убедитесь, что на 80 см горят уже 4 огня вместо 2

Открыть парктроник

Типичные ошибки

  • Импульс TRIG короче 10 мкс. Датчик не начинает замер, и снаружи это неотличимо от неподключённого модуля.
  • pulseIn без таймаута. Секунда ожидания на каждый промах.
  • Замеры слишком часто. Эхо от прошлой пачки приходит во время следующего замера и даёт короткое ложное расстояние. Между замерами держат 50–60 мс; в терменвоксе для этого стоит delay(60).
  • Пять вольт на входе ESP32. Классический HC-SR04 питается от 5 В и выдаёт на ECHO пять вольт. В примерах PinPort для ESP32 модуль для простоты подключён к 3V3, но на настоящей плате ECHO заводят через делитель 1 кОм + 2 кОм или берут модуль, рассчитанный на 3,3 В.
  • map() без constrain(). Результат за пределами диапазона, о котором шла речь выше.

Что попробовать в симуляторе

Расстояние задаётся в свойствах дальномера на схеме — от 2 до 400 см, как у настоящего модуля. Модель отвечает эхом ровно 58 мкс на сантиметр и пишет каждый замер в журнал: «ответил эхом 2320 мкс — 40 см». Если поставить больше 400 см, журнал скажет, что эха не будет, а скетч получит таймаут. Если укоротить импульс TRIG, убрав delayMicroseconds(10), модель сообщит, что импульс короче 10 мкс и датчик не мерил, — полезно увидеть, как выглядит эта ошибка.

Для музыкального варианта есть терменвокс на дальномере: расстояние от 5 до 47 см разбито на ступени по 3 см, и каждая ступень — нота до мажора, от 262 до 988 Гц. На 10 см звучит ля второй октавы, 880 Гц, на 40 см — ми, 330 Гц, дальше 47 см — тишина.

Итог

HC-SR04 — это время полёта звука, делённое пополам: 58 микросекунд на сантиметр при комнатной температуре. Надёжным дальномер делают три вещи: таймаут в pulseIn, медиана вместо среднего и пауза между замерами. А парктроник получается, когда расстояние превращают в период писка и число огней — не забыв ограничить диапазон, чтобы прибор не замолкал у самого бампера.

Попробуйте в симуляторе

Ультразвук: медианный фильтр

Поставьте дальномеру 450 см: эха не будет, once() вернёт 400, и в порт пойдут строки 400.0 см — так фильтр обозначает выход за диапазон

Медианный фильтр

Парктроник

Поставьте дальномеру 3 см — зуммер пищит сплошным тоном. Уберите строку if (cm < 10) и повторите: писк пропадёт совсем, потому что map() даст отрицательный период

Парктроник на ESP32

Терменвокс на дальномере: играй рукой в воздухе

Поменяйте STEP_CM на 1: рабочая зона сожмётся до 19 см, и на 10 см вместо ля (880 Гц) зазвучит ре (587 Гц)

Терменвокс

Arduino Uno: дальномер HC-SR04

Поставьте дальномеру 500 см: вместо строки cm: в порту появится «нет ответа: слишком далеко или провод отошёл»

Дальномер на Uno

← Все статьи