Полный фарш: MPPT, балластный нагрев воды и собственный контроллер на ESP32 с PID-регулированием
Система делится на два независимых контура управления, которые не должны мешать друг другу:
Ветряк — это третий, отдельный контур, у него свой контроллер (КЭВ) со своим балластом. Не смешивайте его с ESP32-контроллером, иначе получите кашу.
Если разомкнуть цепь ветрогенератора, он останется без нагрузки и раскрутится до оборотов, при которых лопасти отрываются или генератор горит. Поэтому балласт для ветряка обязателен, и управляет им штатный контроллер КЭВ, а не ESP32.
Панель можно отключить без последствий. Но если вы хотите использовать избыток — например, чтобы греть воду в теплоаккумуляторе — нужен отдельный дивертер. Это и есть наша ESP32-система.
| Тип | Где стоит | Кто управляет | Задача |
|---|---|---|---|
| Защитный балласт ветряка | На выходе ветряка | КЭВ DOMINATOR | Не дать ветряку разогнаться |
| Утилизирующий балласт (дивертер) | На AC-стороне инвертора | ESP32 | Слить избыток солнца в тепло |
| Компонент | Модель | Зачем |
|---|---|---|
| MCU | ESP32 DevKit v1 / S3 | PID, Wi-Fi, веб-морда |
| Датчик мощности | PZEM-004T v3.0 | Меряет U, I, P, энергию по UART |
| Силовой ключ | SSR 40A или симистор BTA41 + MOC3063 | Коммутация ТЭНа 2–3 кВт |
| Диммер (опция) | RobotDyn AC Dimmer 2 кВт | Плавное ШИМ-регулирование мощности |
| Дисплей (опция) | OLED SSD1306 I2C | Показ P, duty, режима |
| ТЭН | 2–3 кВт, 220 В | Греет воду в теплоаккумуляторе |
| Питание ESP32 | Hi-Link HLK-PM01 (5V 3W) | Питание от 220 В |
Для ТЭНа 2 кВт при 220 В ток ≈ 9 А. SSR на 40 А — нормально, но обязательно на радиаторе. Для 3 кВт берите 50–60 А.
Цель: держать мощность обмена с сетью около нуля. Если P_grid < 0 — есть избыток, греем сильнее. Если P_grid > 0 — берём из сети, греем слабее.
// Уставка: 0 Вт обмена (всё, что даёт солнце, уходит в дом + ТЭН)
float setpoint = 0.0;
float error = setpoint - P_grid;
float output = Kp*error + Ki*integral + Kd*derivative;
// output → duty 0..100% для SSR/диммера
Начинайте с Ki=0, Kd=0, поднимайте Kp до лёгкого колебания, затем добавьте Ki для устранения смещения.
/* ============================================================
ESP32 Solar Diverter — PID ballast controller
Читает PZEM-004T, управляет SSR/диммером ТЭНа
============================================================ */
#include <WiFi.h>
#include <WebServer.h>
#include <PZEM004Tv30.h>
#include <ArduinoOTA.h>
// ---------- Пины ----------
#define SSR_PIN 25 // через оптопару к SSR
#define ZC_PIN 26 // zero-cross (если диммер)
#define PZEM_RX 16
#define PZEM_TX 17
// ---------- PZEM ----------
HardwareSerial pzemSerial(2);
PZEM004Tv30 pzem(pzemSerial, PZEM_RX, PZEM_TX);
// ---------- PID ----------
float Kp = 0.8, Ki = 0.05, Kd = 0.1;
float setpoint = 0.0;
float integral = 0, prevError = 0;
float duty = 0; // 0..100 %
const float DUTY_MAX = 100.0;
const float DUTY_MIN = 0.0;
// ---------- Фильтр ----------
float P_filtered = 0;
const float ALPHA = 0.2; // EMA
// ---------- Веб ----------
WebServer server(80);
unsigned long lastPid = 0;
const unsigned long PID_MS = 1000;
float P_grid = 0, V_grid = 0, I_grid = 0;
float P_heat = 0;
// ============================================================
void setup() {
Serial.begin(115200);
pzemSerial.begin(9600, SERIAL_8N1, PZEM_RX, PZEM_TX);
pinMode(SSR_PIN, OUTPUT);
digitalWrite(SSR_PIN, LOW);
WiFi.begin("YOUR_SSID", "YOUR_PASS");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nWiFi OK: " + WiFi.localIP().toString());
server.on("/", [](){
String html = "<html><head><meta charset=utf-8><meta http-equiv=refresh content=3></head><body>";
html += "<h2>Solar Diverter</h2>";
html += "<p>P_grid: " + String(P_grid,1) + " W</p>";
html += "<p>U: " + String(V_grid,1) + " V, I: " + String(I_grid,2) + " A</p>";
html += "<p>Duty: " + String(duty,1) + " %</p>";
html += "<p>P_heat ~ " + String(P_heat,0) + " W</p>";
html += "</body></html>";
server.send(200, "text/html", html);
});
server.begin();
}
// ============================================================
void loop() {
server.handleClient();
if (millis() - lastPid >= PID_MS) {
lastPid = millis();
readPzem();
computePid();
applyDuty();
}
}
// ============================================================
void readPzem() {
V_grid = pzem.voltage();
I_grid = pzem.current();
P_grid = pzem.power();
if (isnan(V_grid)) V_grid = 0;
if (isnan(I_grid)) I_grid = 0;
if (isnan(P_grid)) P_grid = 0;
// EMA-фильтр, чтобы облака не дёргали PID
P_filtered = ALPHA * P_grid + (1.0 - ALPHA) * P_filtered;
}
// ============================================================
void computePid() {
float error = setpoint - P_filtered;
integral += error * (PID_MS / 1000.0);
// анти-виндап
if (integral > 50) integral = 50;
if (integral < -50) integral = -50;
float derivative = (error - prevError) / (PID_MS / 1000.0);
prevError = error;
float out = Kp*error + Ki*integral + Kd*derivative;
// P_grid > 0 — берём из сети, значит уменьшаем нагрев
// P_grid < 0 — избыток, увеличиваем нагрев
duty -= out * 0.1; // знак и шаг подбираются под вашу систему
if (duty > DUTY_MAX) duty = DUTY_MAX;
if (duty < DUTY_MIN) duty = DUTY_MIN;
}
// ============================================================
void applyDuty() {
// Простой вариант: SSR вкл/выкл с окном 1 сек
// (для ТЭНа инерция воды огромная, ШИМ 1 Гц — ок)
static unsigned long winStart = 0;
static bool on = false;
const unsigned long WINDOW = 1000;
if (millis() - winStart >= WINDOW) {
winStart = millis();
on = false;
}
if (!on && (millis() - winStart) < (WINDOW * duty / 100.0)) {
digitalWrite(SSR_PIN, HIGH);
} else {
digitalWrite(SSR_PIN, LOW);
}
// оценка мощности нагрева (если ТЭН 2000 Вт при 100%)
P_heat = 2000.0 * duty / 100.0;
}
duty -= out * 0.1 — зависит от того, как PZEM включён в цепь (прямой/обратный ток).applyDuty() на фазовое управление с прерыванием по ZC_PIN.Теплоаккумулятор (ТА) — это бак 500–2000 л с водой и ТЭНом. Логика:
| Параметр | Значение |
|---|---|
| Объём ТА | 1000 л |
| ТЭН | 2 кВт (9 А) |
| ΔT за 4 ч солнца | ≈ 17 °C (при 100% duty) |
| Термостат | механический на 85 °C — обязателен |
Поставьте два ТЭНа: один на 1 кВт (низкий порог, для пасмурных дней) и один на 2 кВт (высокий порог, для яркого солнца). ESP32 переключает между ними SSR-ами. Это даёт более широкий диапазон утилизации.