Дачный бак на двухметровой стойке умеет портить настроение одним вопросом: сколько в нём воды. Крышка открывается с табуреткой, фонарик в другую руку — и всё ради взгляда вниз. Монитор порта на кухне не живёт на улице, а числовой экран на баке читается с трёх шагов плохо. Панель с бумажной шкалой у входа в дом оказалась решением, которое пережило три сезона: серво-стрелка показывает уровень, как барометр показывает давление, — видно издалека и без интерпретаций.
Стрелочный прибор из урока про серво-вольтметр брал любой аналоговый сигнал с соседнего входа. Здесь источник дальше: дальномер HC-SR04 в крышке бака меряет расстояние до поверхности воды, и вся соль урока — в превращении сантиметров в проценты и в поведении стрелки на живой, дрожащей воде. Геометрия бака диктует и разметку шкалы, и пороги тревоги.
В симуляторе поверхность воды — тот же ползунок препятствия, что у шлагбаума: тянете от «пусто» до «полно», стрелка и огонёк отрабатывают. На живом баке после сборки остаётся две калибровки — порожняя и полная отметки по факту.
Геометрия бака: из сантиметров в проценты
Датчик смотрит вниз с крышки. Порожний бак для моего восьмидесятилитрового — 110 сантиметров от датчика до дна, полный — около 10: зеркалу воды нужен зазор, во-первых, у самого модуля слепая зона от двух-трёх сантиметров, во-вторых, поверхность дрожит и при зазоре в пять сантиметров эхо уже ловит брызги. Уровень в процентах — одна строка: (110 − дистанция) / 100 × 100. Знаменатель — рабочий ход бака в сантиметрах, и он у каждого свой: именно эту пару констант вы калибруете по факту, а не берёте из статьи.
Вода — плохое зеркало для ультразвука. Рябь от ветра, капли с крышки, пена от струи при заполнении — показания гуляют на один-два сантиметра, это один-два процента шкалы, и стрелка, повторяющая каждую волну, выглядит больной. Два слоя защиты: медиана из трёх замеров глушит одиночные выбросы, а сглаживание хода не даёт стрелке выписывать рябь на бумаге. Медиану я выбрал против среднего из урока про усреднение восьми замеров сознательно: всплеск на один замер у среднего тянет результат, у медианы — нет.
Механика замера та же, что у любого дальномерного проекта: триг 10 мкс, pulseIn с таймаутом 30 мс, деление на 58 — пересчёт сантиметров разобран в уроке про HC-SR04. Таймаут 30 мс — это потолок в полметра: дальше воды в моих баках от мембраны не бывает, а если эхо ушло в стенку — честный минус единица лучше выдуманной дистанции.
Разметка шкалы — отдельное удовольствие. Угол 0–180 серво не нужен: края по 20 градусов оставляю как запас против упоров. Итого 20° — пусто, 160° — полно, и каждые 25 процентов уровня — по 35 градусов: 20, 55, 90, 125, 160. Но риски на бумаге я ставлю не арифметикой, а фактом: порожний бак — метка, полный — метка, потом две последовательные середины. Бак бывает конусным: литры и высота у него расходятся, и шкала в процентах высоты честнее шкалы в процентах объёма — об этом честная оговорка ниже.
#include <Servo.h> Servo strelka; const int PIN_TRIG = 2;const int PIN_ECHO = 3;const int PIN_LED = 12; // «полно» — пора закрывать кран const int DIST_PUSTO = 110; // см от датчика до днаconst int DIST_POLNO = 10; // зазор при полном бакеconst int UGOL_PUSTO = 20;const int UGOL_POLNO = 160;const unsigned long T_ZAMER = 1000; // замер раз в секундуconst unsigned long T_SHAG = 15; // шаг сглаживания int ugol = UGOL_PUSTO; // где стрелка сейчасint cel = UGOL_PUSTO; // куда должна доехатьunsigned long tZamer = 0;unsigned long tShag = 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, 30000); if (us == 0) return -1; return us / 58;} void setup() { strelka.attach(11); pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT); pinMode(PIN_LED, OUTPUT); Serial.begin(9600); Serial.println("Urovenmer: media iz 3, shag 15 ms");} int median3(int a, int b, int c) { return max(min(a, b), min(max(a, b), c));} void loop() { unsigned long now = millis(); if (now - tZamer >= T_ZAMER) { tZamer = now; int d1 = zamer(); delay(25); int d2 = zamer(); delay(25); int d3 = zamer(); int dist = median3(d1, d2, d3); if (dist > 0) { int uroven = map(dist, DIST_PUSTO, DIST_POLNO, 0, 100); uroven = constrain(uroven, 0, 100); digitalWrite(PIN_LED, uroven >= 95 ? HIGH : LOW); cel = map(uroven, 0, 100, UGOL_PUSTO, UGOL_POLNO); Serial.print("dist="); Serial.print(dist); Serial.print(" uroven="); Serial.println(uroven); } } if (ugol != cel && now - tShag >= T_SHAG) { tShag = now; if (ugol < cel) ugol = ugol + 1; else ugol = ugol - 1; strelka.write(ugol); }}В листинге два слоя движения. Цель cel пересчитывается из свежего уровня раз в секунду, а блок в хвосте loop() каждые 15 мс двигает угол на один градус к цели — скорость догона 66 градусов в секунду. Изменение уровня на четверть бака стрелка проезжает за секунду с хвостом, а волнение поверхности в сантиметр-два для неё не существует. Мгновенный write каждой медианы дал бы стрелку, живущую в такт ряби, — ровно то, за что я ругал однопороговый шлагбаум из первого урока. Приём догона цели без блокировки подробно разобран в уроке про интерполяцию без delay — здесь он подключён к дальномеру вместо потенциометра, и loop остаётся свободным в те же 95 процентов времени, о которых говорил планировщик на millis.
Медиана из трёх замеров с паузой 25 мс между ними занимает меньше сотни миллисекунд раз в секунду — loop при этом жив: замер один, печать одна, всё остальное время скетч свободен. Если захочется видеть обе кривые — сырую и медиану — на одном графике, графиком плоттера рисует их двумя рядами, и зазор между ними — цена фильтра вживую.
| Уровень | Дистанция до воды | Угол стрелки | Метка на бумаге |
|---|---|---|---|
| 0% — порожний | 110 см | 20° | нижняя риска, калибруется по факту |
| 25% | 85 см | 55° | риска после двух середин |
| 50% | 60 см | 90° | центр шкалы |
| 100% — полный | 10 см | 160° | верхняя риска + светодиод |
Капли на мембране и конусный бак
Теперь грязная часть проекта, о которой молчат картинки с глянцевыми баками. Капли на излучателе ломают показания наглухо: эхо отражается от воды на самой мембране, и датчик уверенно докладывает «полно» в порожнем баке — та же беда, что и у шлагбаумной капли, только внизу бака её ставит конденсат. Лечение: козырёк над мембраной внутри крышки, съёмный кронштейн для протирки при визитах и геометрия — датчик строго вертикально, иначе конус в пятнадцать градусов начнёт ловить стенку бака раньше воды.
И вторая честная оговорка — про конусные баки. map() линеен по высоте, но объём в конусе растёт кубически: половина высоты — далеко не половина литров. Моя шкала в процентах высоты — не обман, а договор: я измеряю то, что меряю, и подписываю риски словами «порожний», «половина по высоте», «полный». Для поливного графика этого достаточно; если нужны литры — калибровочная таблица из кувшина и пара вечеров с Excel.
Симуляторная вода — идеальное зеркало без ряби, и на первый взгляд медиана выглядит лишней. Потяните ползунок рывком: стрелка не прыгает к цели, а едет по градусу — на живом баке та же механика съедает и волнение, и одиночные выбросы эха. На реальном баке к этому добавляется то, чего в симе не видно: показания плывут, пока поверхность занята брызгами от струи, и медиана из трёх в эти минуты — единственное, что держит стрелку в здравом уме.
Пара слов про монтаж, который я переделывал один раз. Первая версия висела на кронштейне у стенки — конус ловил стену, и уровень «стоял» на одном значении в любых условиях. Перенос в центр крышки решил всё: в баке правильного цилиндра датчик должен смотреть в самую глубокую точку, а не мимо. Секунда на проверку: порожний бак должен показывать ровно константу DIST_PUSTO — если нет, датчик смотрит не туда.
Кабель от датчика на крышке до панели у двери — четыре провода и два метра: питание, грунт, триг и эхо. На такой длине триг уже ловит наводки от зарядки телефона в соседней розетке — если показания пойдут вразнос, скрутки замените пайкой, а провод эха подальше от силовой проводки. Питание панели у меня от того же блока 5 В, что и серво: стрелка едет редко, ток удержания SG90 в пару сотен миллиампер — урок про ток удержания считает подробнее — блок на ампера хватает с запасом.
- Уровень считается из геометрии: (порожняя дистанция − дистанция) / рабочий ход, константы калибруются по факту
- Вода дрожит на 1–2 см: медиана из трёх глушит выбросы, сглаживание по градусу не выписывает рябь на шкале
- Зазор полного бака — 10 см: слепая зона модуля и брызги требуют запаса
- Ноль от pulseIn — до медианы: иначе порожний бак докладывает «полно»
- Шкала 20–160° с рисками каждые 25% — но на конусном баке проценты высоты не равны процентам объёма
- 1. Почему зазор полного бака взят 10 см, а не минимальные 2–3 см датчика?
- 2. Почему медиана из трёх замеров, а не среднее из восьми?
- 3. Зачем стрелка едет к цели по градусу каждые 15 мс, а не пишет угол сразу?
- 4. Что случится, если два нуля от pulseIn попадут в медиану без проверки?
- 5. Как честно разметить бумажную шкалу под конкретный бак?
Частые вопросы по теме урока
- Почему зазор полного бака взят 10 см, а не минимальные 2–3 см датчика?
- Поверхность колышется на сантиметры, струя при заполнении даёт брызги, а слепая зона модуля начинается от 2–3 см. Зазор в 10 см держит измерение в честной зоне весь цикл заполнения. Зеркало воды дрожит, и запас перекрывает брызги и слепую зону
- Почему медиана из трёх замеров, а не среднее из восьми?
- Среднее смешивает выброс с честными замерами и уводит результат; медиана выбрасывает его сама. Восьмизамерное среднее из урока про серво-вольтметр — родня, но для воды с брызгами медиана честнее. Одиночный выброс не тянет результат в сторону
- Зачем стрелка едет к цели по градусу каждые 15 мс, а не пишет угол сразу?
- Мгновенный write каждой медианы заставил бы стрелку жить в такт волне. Шаг по градусу на 66°/с съедает волнение и выглядит как поведение прибора, а не метронома. Рябь поверхности не выписывается на шкале, ход выглядит спокойным
- Что случится, если два нуля от pulseIn попадут в медиану без проверки?
- Нуль сантиметров в этой геометрии — «вода у самой крышки». Два нуля из трёх дают медиану-нуль, map тянет её в максимум, и порожний бак рапортует о переливе. Ветка нуля стоит до медианы. Порожний бак покажет «полно» со светодиодом
- Как честно разметить бумажную шкалу под конкретный бак?
- Паспортные дистанции грешат на пару сантиметров против реального кронштейна. Две фактические метки и две середины дают шкалу именно вашего бака; для конусного — проценты высоты подписывают словами, литры идут отдельной калибровочной таблицей. Пометить порожний и полный по факту, затем делить серединами