Действительно умная гидропоника. Часть 3.1 Датчики и сенсоры. PH сенсор
По просьбам трудящихся, я постараюсь сделать это в виде некоего обучающего рассказа что ли. Но конечно не step by step инструкции. Краткие выдержки кода и описание алгоритмов и чего куда вообще совать что бы эти ваши показания получать. Сегодня поговорим о PH сенсорах и моем опыте. Я подразобью на более короткие посты для удобства чтения.
PH Sensor
Я пробовал 2 сенсора PH. от DF-Robot и дешевый из синего маркетплейса.


Вот такие сенсоры
Ссылки на все я приведу в конце поста.
Сейчас я использую pH-датчик от DFRobot — он отлично подходит для непрерывного мониторинга 24/7. Показания стабильны, со временем не «плывут» (дрейф отсутствует — проверено на практике), а калибровка реализована удобно и просто. Чуть позже покажу, как она устроена на примерах кода.
А вот с дешёвым noname-датчиком всё гораздо веселее. На плате два подстроечных резистора: один, судя по всему, отвечает за калибровку нулевой точки (pH 7.0), второй — за чувствительность. Документации — минимум. Настроить этот модуль так, чтобы он давал стабильные показания, — почти как пройти Super Meat Boy на 100%… и не сломать ничего от сгоревшей пятой точки..
Стоит ли покупать дешёвый pH-сенсор? Нет и ещё раз нет. Даже если ваш код написан идеально, это не спасёт от нестабильной работы самого датчика. Через день-два показания могут начать «плавать» — pH то завышается, то занижается на 3–4 единицы без видимых причин. Сенсор просто начинает вести себя непредсказуемо.
И если у вас на такие данные завязана автоматика (например, дозаторы pH- или pH+), последствия будут неприятными: система начнёт вносить корректоры на основе ложных показаний. В итоге — испорченный раствор и потеря времени, так ещё и потраченные впустую корректоры PH.
Сенсоры от DFRobot мне показались гораздо стабильнее и надёжнее. Да, они стоят дороже, но по опыту — это оправданно. Поэтому далее всё что написано, относится к конкретно H-101 сенсору.
Подключение РH.
Подключение pH-сенсора — дело довольно простое. Информация по распиновке обычно указана прямо на сайте производителя или в описании товара (в том числе на AliExpress). Всё, что нужно — подключить выход сенсора к аналоговому входу вашей платы: будь то ESP32, Arduino или любая другая платформа.
Этот конкретный сенсор, судя по данным и практике, выдаёт сигнал до 3.3 В, так что его можно безопасно подключать напрямую к пинам ESP32 — без делителя напряжения. Я так и делаю, и всё работает стабильно. Делитель нужен, только если сенсор выдаёт до 5 В (что бывает у некоторых моделей), а ваша плата работает от 3.3 В. В случае с классической Arduino делитель не нужен, так как она спокойно воспринимает до 5 В на аналоговых входах.
Калибровка PH.
Калибровка pH-сенсоров всегда выполняется с помощью буферных растворов — это жидкости с точно заданным значением pH. Калибровка может быть двух- или трёхточечной: чем больше точек, тем точнее можно подогнать датчик под реальный диапазон.
Буферные растворы бывают в виде порошков (которые нужно растворить) или уже готовые к использованию. Их подбирают так, чтобы они охватывали рабочий диапазон измерений.
В моём случае я использую двухточечную калибровку — по значениям pH 4.0 и pH 7.0, чего вполне достаточно для контроля большинства питательных растворов.
Сама калибровка проходит в два последовательных этапа. Сначала сенсор промывается в дистиллированной воде и аккуратно просушивается (я использую салфетку, не протирая стекло — просто промакиваю). После этого сенсор помещается в буферный раствор с pH 4.0. Ждём, пока значения напряжения стабилизируются, и нажимаем кнопку калибровки по pH 4.0 — первый этап завершён.
Далее — всё то же самое, но уже с раствором pH 7.0. После второго нажатия кнопки калибровки сенсор готов к работе.
А вот и фрагмент кода, который у меня отвечает за калибровку pH-сенсора. Он используется при нажатии кнопки калибровки (по pH 4.0 или 7.0):
void setCalPH(float& calValue, int eepromAddr, bool& flag, const char* label) {
bool stable = false;
float stddev = 0.0;
float voltage = readStablePHVoltage(stable, stddev);
String logMsg = String("[Калибровка] ") + label + ": " + String(voltage, 2) + " мВ, отклонение: " + String(stddev, 2);
logMsg += stable ? " — стабильно" : " — нестабильно";
logToWeb(logMsg);
logMessage(stable ? LOG_INFO : LOG_WARN, logMsg);
if (!stable) {
//logToWeb(String("[Калибровка] Напряжение нестабильно при ") + label + ". Попробуйте снова.");
logMessage(LOG_WARN, "[Калибровка] Калибровка прервана: нестабильное напряжение");
return;
}
calValue = voltage;
flag = true;
EEPROM.put(eepromAddr, calValue);
EEPROM.commit();
String confirm = String("[Калибровка] ") + label + " установлена: " + String(voltage, 2) + " мВ";
logToWeb(confirm);
logf(LOG_INFO, "[Калибровка] %s успешно установлена: %.2f мВ", label, voltage);
}
Что делает эта функция:
Считывает напряжение с pH-сенсора и проверяет, стабилизировалось ли оно (внутри readStablePHVoltage).
Если сигнал нестабилен, калибровка прерывается и выводится предупреждение в лог.
Если всё в порядке, сохраняет измеренное значение в EEPROM, устанавливает флаг flag = true, и выводит подтверждение об успешной калибровке.
Используется отдельно для pH 4.0 и pH 7.0, передавая соответствующую метку (label), адрес в EEPROM и переменную для сохранения значения.
Функции logToWeb/logF/logMessage - это обертки для логирования, вместо них вы можете использовать простые Serial.println и тд.
Функция считывания напряжения для калибровки, выглядит вот так:
float readStablePHVoltage(bool& stable, float& stddevOut) {
const int numSamples = 20;
float values[numSamples];
float sum = 0.0f;
float minV = VREF, maxV = 0.0f;
String rawValues = "[PH] Измерения мВ: ";
for (int i = 0; i < numSamples; i++) {
int raw = analogRead(PH_PIN);
float v = (raw / (float)ADC_MAX) * VREF;
values[i] = v;
sum += v;
minV = min(minV, v);
maxV = max(maxV, v);
rawValues += String(v, 2) + " ";
delay(10);
}
float avg = sum / numSamples;
float variance = 0.0f;
for (int i = 0; i < numSamples; i++) {
// выбрасываем выбросы >500 мВ
if (abs(values[i] - avg) > 500.0f) values[i] = avg;
variance += pow(values[i] - avg, 2);
}
float stddev = sqrt(variance / numSamples);
stddevOut = stddev;
stable = (stddev < 30.0f);
if (!stable) {
logf(LOG_WARN, "⚠️ pH-сенсор нестабилен: σ=%.2f mВ", stddev);
}
// Логи
logMessage(LOG_DEBUG, rawValues);
logf(LOG_DEBUG, "[PH] Среднее: %.2f мВ, Мин: %.2f, Макс: %.2f", avg, minV, maxV);
logf(LOG_DEBUG, "[PH] σ=%.2f мВ → %s", stddev, stable ? "Стабильно" : "Нестабильно");
return avg;
}
Функция readStablePHVoltage используется для получения надёжного и стабильного значения напряжения с pH-сенсора перед калибровкой.
Сначала она делает 20 аналоговых замеров с небольшими паузами между ними. Значения переводятся в милливольты и сохраняются в массив.
После этого вычисляется среднее напряжение, а слишком сильные выбросы (если разница с средним превышает 500 мВ) игнорируются — они заменяются на усреднённое значение.
Далее считается стандартное отклонение (σ) — если оно меньше 30 мВ, сигнал считается стабильным и пригодным для калибровки. В противном случае функция сообщает в лог о нестабильности и отменяет процесс.
Кроме самого результата, в лог отправляется подробная информация: все замеры, среднее, минимум, максимум и разброс значений. Это помогает при отладке и повышает надёжность всей системы.
Измерения PH
При измерениях любых параметров жидкости необходимо учитывать её температуру, так как это напрямую влияет на точность показаний. Особенно это критично для pH — с повышением температуры активность ионов водорода возрастает, и даже идеально откалиброванный сенсор начнёт показывать отклонения. В модуле который использую я, по заявлениям уже учитывается этот фактор. Поэтому в моих функциях нет реализации температурной компенсации.
float readPH() {
static unsigned long lastLog = 0;
float voltage = readPHVoltage();
float phValue = calculatePH(voltage);
phValue = roundf(phValue * 10.0f) / 10.0f;
// Логируем не чаще, чем раз в 10 секунд
unsigned long now = millis();
if (now - lastLog > 10000) {
logf(LOG_DEBUG, "[PH] Напряжение: %.2f mВ, pH: %.2f",
voltage, phValue);
lastLog = now;
}
return phValue;
}
Собственно сама функция чтения PH. В ней мы уже обращаемся к другой функции считывания напряжения. Она гораздо больше фильтрует значения напряжения, и не мешает при этом калибровке. Именно поэтому их тут две.
float readPHVoltage() {
// 1) Усреднение по N замерам
const int numMeasurements = 30;
float sum = 0.0f;
for (int i = 0; i < numMeasurements; i++) {
int raw = analogRead(PH_PIN);
float v = (raw / (float)ADC_MAX) * VREF;
sum += v;
delay(5);
}
float avg = sum / numMeasurements;
lastRawVoltage = avg;
// 2) Медианный фильтр
medianBuf[medianIdx++] = avg;
if (medianIdx >= MEDIAN_SAMPLES) { medianIdx = 0; medianFull = true; }
int cntMed = medianFull ? MEDIAN_SAMPLES : medianIdx;
// копируем в tmp для сортировки
static float tmpMed[MEDIAN_SAMPLES];
memcpy(tmpMed, medianBuf, cntMed * sizeof(float));
// простой пузырьковый сорт для маленького массива
for (int i = 0; i < cntMed - 1; i++)
for (int j = i + 1; j < cntMed; j++)
if (tmpMed[j] < tmpMed[i]) {
float t = tmpMed[i];
tmpMed[i] = tmpMed[j];
tmpMed[j] = t;
}
float med = tmpMed[cntMed/2];
// 3) Скользящее среднее
movavgBuf[movavgIdx++] = med;
if (movavgIdx >= MOVAVG_SAMPLES) { movavgIdx = 0; movavgFull = true; }
int cntMov = movavgFull ? MOVAVG_SAMPLES : movavgIdx;
float sumMov = 0.0f;
for (int i = 0; i < cntMov; i++) sumMov += movavgBuf[i];
float mavg = sumMov / cntMov;
// 4) IIR‑фильтр
if (isnan(iirVoltage)) {
iirVoltage = mavg;
} else {
iirVoltage = iirVoltage * (1 - IIR_ALPHA) + mavg * IIR_ALPHA;
}
float filt = iirVoltage;
// Логи для отладки
logf(LOG_DEBUG,
"[PH] raw:%.2f mV → med:%.2f → movavg:%.2f → filt:%.2f",
avg, med, mavg, filt);
return filt;
}
Для получения чистого и стабильного сигнала с pH-сенсора я использую многоступенчатую фильтрацию.
Всё начинается с простого усреднения — берутся 30 замеров подряд, и по ним считается среднее напряжение.
Затем подключается медианный фильтр: он отсеивает выбросы, которые могли случайно попасть в серию замеров. После этого применяется скользящее среднее, чтобы ещё больше сгладить возможные резкие скачки.
И в конце — финальный этап: IIR-фильтр (экспоненциальное сглаживание), который делает сигнал максимально плавным, но при этом достаточно быстрым в реакции на изменения. Такой подход позволяет получать надёжные и устойчивые данные, на которые уже можно опираться при расчёте текущего уровня pH или при управлении системой.
Заключение.
Таким образом я измеряю PH своего раствора.
Вот обещанные ссылки на компоненты из этого поста:
https://aliexpress.ru/item/1005001317266554.html?spm=a2g2w.o...
https://aliexpress.ru/item/1005006366896348.html?sku_id=1200...
https://www.ozon.ru/product/komplekt-kalibrovochnyh-rastvoro...
Дальше останется только привязать это к вашим интерфейсам взаимодействия с пользователем и в целом готово. Не претендую на какую то 100% правильность, вы можете сделать это иначе, но сейчас у меня работает отлично всё, напомню, что я в первую очередь 3д художник, который открыл для себя мир программирования и контроллеров)
Спасибо за внимание! Надеюсь скоро выпущу про TDS/EC сенсор, температурный датчик.













































































