Moin Leute,
ich habe das gleiche Problem wie viele hier: Oft ist mein PV-Überschuss kleiner als die Leistung des Heizstabes.
Ziel ist es, einen 15-Liter-Druckboiler in einer Gartenlaube als PV-Überschusslast an einem Victron Multiplus II 3000 zu betreiben – ohne Phasenanschnitt, ohne DC-Anteil, MP2-freundlich.
Hardware:
-
Victron Multiplus II GX 3000
-
GX IO-Extender 150 (PWM-Ausgang)
-
Arduino Nano
-
Fotek SSR-25DA (Zero-Crossing)
Funktionsweise:
Der GX IO-Extender gibt ein PWM-Signal (1562,5 Hz) aus, dessen Tastverhältnis den verfügbaren PV-Überschuss kodiert. Der Arduino liest dieses Signal per Flanken-Interrupt aus und wandelt es in ein verteiltes Schwingungspaket für das Zero-Crossing SSR um.
Pro Paket werden 8 Netzzyklen (160ms) geschaltet. Die eingeschalteten Zyklen werden dabei gleichmäßig über das Paket verteilt statt geblockt am Anfang (normale Schwingungspaketsteuerung) – dadurch sieht der Wechselrichter kleinere, regelmäßigere Lastwechsel. Das ergibt 8 Leistungsstufen von 0 bis 1500W in Schritten von ~188W.
Ein Stufenwechsel wird immer erst am Paketanfang übernommen, nie mittendrin – damit entstehen keine unvollständigen Halbwellen.
Die Steuerlogik (Überschussberechnung, Lastüberwachung, Temperatursensor) läuft in Node-RED direkt auf dem Multiplus II GX.
Was haltet ihr davon? Wird sich der MP2 über die seltsame Last beschweren?
BTW: Der Arduino Nano ist noch nicht gesetzt. Evtl wird es auch ein ESP32 mit Modbus und der GX IO-Extender 150 (PWM-Ausgang) wird wieder verkauft da ich ihn nicht wirklich nutze
Anbei der Arduino Code gezippt. SplitBurstController.zip (2,0 KB)
#include <TimerOne.h> // getestet mit TimerOne Lib Version 1.1.1 // ── Pins ────────────────────────────────────────────────── const int inputPin = 2; // PWM-Eingang vom GX IO-Extender (INT0) const int outputPin = 9; // Steuerausgang zum SSR // ── Schwingungspaket ────────────────────────────────────── const int packetSteps = 8; // Anzahl Netzzyklen pro Paket const long cycleDurationUs = 20000; // 20ms = 1 Netzzyklus bei 50Hz // ── PWM-Eingang ─────────────────────────────────────────── const float periodDurationUs = 640.0; // 1/1562,5Hz in Mikrosekunden // ── Bitmuster für gleichmäßige Lastverteilung ───────────── // Zeile = Stufe (0-8), Spalte = Zyklus (0-7) const bool pattern[9][8] = { {0,0,0,0,0,0,0,0}, // Stufe 0 = 0W {1,0,0,0,0,0,0,0}, // Stufe 1 = ~188W {1,0,0,0,1,0,0,0}, // Stufe 2 = ~375W {1,0,0,1,0,0,1,0}, // Stufe 3 = ~563W {1,0,1,0,1,0,1,0}, // Stufe 4 = ~750W {1,1,0,1,0,1,1,0}, // Stufe 5 = ~938W {1,1,1,0,1,1,1,0}, // Stufe 6 =~1125W {1,1,1,1,1,1,1,0}, // Stufe 7 =~1313W {1,1,1,1,1,1,1,1}, // Stufe 8 = 1500W }; // ── Geteilte Variablen (ISR + loop) ─────────────────────── volatile unsigned long risingTime = 0; // Zeitstempel steigende Flanke volatile unsigned long highDuration = 0; // gemessene HIGH-Dauer in µs volatile bool newValue = false; // neuer Messwert verfügbar // ── Paketsteuerung (nur in Timer-ISR verwendet) ─────────── volatile int counter = 0; // aktueller Zyklus im Paket (0..packetSteps-1) volatile int activeStep = 0; // aktive Stufe (wird am Paketanfang übernommen) volatile int nextStep = 0; // nächste Stufe (aus loop() geschrieben) // ── Timer-ISR: läuft exakt alle 20ms ───────────────────── void timerIsr() { // Neue Stufe nur am Paketanfang übernehmen → kein Halbwellenproblem if (counter == 0) { activeStep = nextStep; } // Ausgang anhand Bitmuster setzen if (pattern[activeStep][counter]) { digitalWrite(outputPin, HIGH); } else { digitalWrite(outputPin, LOW); } // Zähler weiterschalten counter = (counter + 1) % packetSteps; } // ── Flanken-ISR: misst HIGH-Dauer des PWM-Eingangs ─────── void edgeIsr() { if (digitalRead(inputPin) == HIGH) { risingTime = micros(); // steigende Flanke merken } else { highDuration = micros() - risingTime; // fallende Flanke: Dauer berechnen newValue = true; } } void setup() { pinMode(inputPin, INPUT); pinMode(outputPin, OUTPUT); // Flankenerkennung auf inputPin attachInterrupt(digitalPinToInterrupt(inputPin), edgeIsr, CHANGE); // Timer auf Zyklusdauer einstellen und ISR verknüpfen Timer1.initialize(cycleDurationUs); Timer1.attachInterrupt(timerIsr); } void loop() { if (newValue) { newValue = false; // Tastverhältnis berechnen und auf 0.0–1.0 begrenzen float dutyCycle = constrain(highDuration / periodDurationUs, 0.0, 1.0); // Auf Paketstufen abbilden und atomar schreiben noInterrupts(); nextStep = round(dutyCycle * packetSteps); interrupts(); } }


