ShineLAN-X: Initiale Firmware + Hardware-Diagnose

- STM32F103RBT6 Firmware für Growatt ShineLAN-X
- Bitbang-SPI (EthernetENC) auf Port C (PC6/PC7/PC8/PC9)
- UART-Debug auf USART1 (PA9/PA10), Modbus temporär deaktiviert
- SO-Aktivitätstest und ESTAT-Register-Scan bestätigen:
  ENC28J60 läuft (SO aktiv), SI/MOSI-Verbindung unterbrochen
- Nächster Schritt: Pin 5 ENC28J60 nachlöten oder Bodge-Draht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-04-16 19:10:31 +02:00
parent 69ebf5d2de
commit 852ec90e6b
39 changed files with 19445 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
#include <Arduino.h>
#include <SPI.h>
#include <EthernetENC.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>
#include "config.h"
// Debug-UART: USART1, TX=PA9, RX=PA10 (Testpunkt auf Platine)
// HINWEIS: Modbus ist temporär deaktiviert — erst Ethernet/MQTT bestätigen,
// dann Debug entfernen und Modbus wieder aktivieren.
HardwareSerial DebugSerial(PA10, PA9);
// ============================================================
// Sensor-Definition
// ============================================================
struct Sensor {
const char* id;
const char* name;
uint16_t address;
bool isDword; // true = 2 Register (32 bit), false = 1 Register (16 bit)
float scale;
const char* unit;
const char* deviceClass;
const char* stateClass;
const char* icon;
};
// Sensor-Liste — entspricht den Modbus-Registern des SPH 5000 TL3-BH-UP
// Für andere Modelle nicht zutreffende Sensoren auskommentieren.
const Sensor SENSORS[] = {
// --- PV Eingang ---
{"pv1_voltage", "PV1 Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
{"pv1_current", "PV1 Current", 4, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"},
{"pv1_power", "PV1 Power", 5, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
{"pv2_voltage", "PV2 Voltage", 7, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
{"pv2_current", "PV2 Current", 8, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"},
{"pv2_power", "PV2 Power", 9, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
// --- AC Ausgang / Netz ---
{"ac_power_total", "AC Output Power Total", 35, true, 0.1f, "W", "power", "measurement", "mdi:flash"},
{"grid_frequency", "Grid Frequency", 37, false, 0.01f, "Hz", "frequency", "measurement", "mdi:sine-wave"},
{"grid_voltage_l1", "Grid Voltage L1", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l1", "Grid Current L1", 39, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"grid_voltage_l2", "Grid Voltage L2", 42, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l2", "Grid Current L2", 43, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"grid_voltage_l3", "Grid Voltage L3", 46, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l3", "Grid Current L3", 47, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
// --- Energie PV ---
{"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
{"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
// --- Temperatur ---
{"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
// --- Batterie ---
{"bat_discharge_power", "Battery Discharge Power", 1009, true, 0.1f, "W", "power", "measurement", "mdi:battery-minus"},
{"bat_charge_power", "Battery Charge Power", 1011, true, 0.1f, "W", "power", "measurement", "mdi:battery-plus"},
{"bat_voltage", "Battery Voltage", 1013, false, 0.1f, "V", "voltage", "measurement", "mdi:battery"},
{"bat_soc", "Battery State of Charge", 1014, false, 1.0f, "%", "battery", "measurement", "mdi:battery"},
{"bat_temperature", "Battery Temperature", 1040, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
// --- Netz- und Batterie-Energiezähler (für Energie-Dashboard) ---
{"energy_import_total", "Energy Import Total", 1046, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-import"},
{"energy_export_total", "Energy Export Total", 1050, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-export"},
{"bat_discharge_total", "Battery Discharge Total", 1054, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-minus"},
{"bat_charge_total", "Battery Charge Total", 1058, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-plus"},
};
const uint8_t SENSOR_COUNT = sizeof(SENSORS) / sizeof(SENSORS[0]);
// ============================================================
// Globale Objekte
// ============================================================
// HardwareSerial modbusSerial(PA10, PA9); // USART1 — temporär deaktiviert (teilt sich UART mit DebugSerial)
byte mac[] = {MAC_ADDRESS};
EthernetClient ethClient;
PubSubClient mqtt(ethClient);
// ModbusMaster modbus; // temporär deaktiviert
// ============================================================
// RS485 Richtungssteuerung (Callbacks für ModbusMaster)
// ============================================================
void preTransmission() { digitalWrite(RS485_DE_PIN, HIGH); }
void postTransmission() { digitalWrite(RS485_DE_PIN, LOW); }
// ============================================================
// MQTT Hilfsfunktionen
// ============================================================
// Veröffentlicht alle Sensor-Discovery-Pakete für Home Assistant
void publishDiscovery() {
char topic[128];
char payload[640];
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
const Sensor& s = SENSORS[i];
snprintf(topic, sizeof(topic),
"homeassistant/sensor/%s_%s/config",
DEVICE_ID, s.id);
snprintf(payload, sizeof(payload),
"{"
"\"name\":\"%s\","
"\"unique_id\":\"%s_%s\","
"\"state_topic\":\"growatt/shinelan/%s\","
"\"unit_of_measurement\":\"%s\","
"\"device_class\":\"%s\","
"\"state_class\":\"%s\","
"\"icon\":\"%s\","
"\"device\":{"
"\"identifiers\":[\"%s\"],"
"\"name\":\"%s\","
"\"model\":\"%s\","
"\"manufacturer\":\"%s\""
"}}",
s.name,
DEVICE_ID, s.id,
s.id,
s.unit,
s.deviceClass,
s.stateClass,
s.icon,
DEVICE_ID, DEVICE_NAME, DEVICE_MODEL, DEVICE_MFR);
mqtt.publish(topic, payload, true); // retained = true
}
}
bool mqttReconnect() {
DebugSerial.print("MQTT connecting... ");
bool ok;
if (strlen(MQTT_USER) > 0) {
ok = mqtt.connect(MQTT_CLIENT, MQTT_USER, MQTT_PASSWORD);
} else {
ok = mqtt.connect(MQTT_CLIENT);
}
if (ok) {
DebugSerial.println("OK");
publishDiscovery();
} else {
DebugSerial.print("FAIL rc=");
DebugSerial.println(mqtt.state());
}
return ok;
}
// ============================================================
// Setup
// ============================================================
void setup() {
// Debug-UART zuerst starten (USART1: TX=PA9 = Testpunkt auf Platine)
DebugSerial.begin(115200);
delay(10);
DebugSerial.println("\r\n=== Growatt ShineLAN-X ===");
DebugSerial.println("Build: " __DATE__ " " __TIME__);
// RS485 DE/RE Pin
pinMode(RS485_DE_PIN, OUTPUT);
digitalWrite(RS485_DE_PIN, LOW); // Empfangsmodus
// ENC28J60 Reset (Hardware-Reset vor init)
DebugSerial.println("ETH: reset...");
pinMode(ETH_RST_PIN, OUTPUT);
digitalWrite(ETH_RST_PIN, LOW);
delay(50);
digitalWrite(ETH_RST_PIN, HIGH);
delay(200);
// SO-Aktivitätstest: Ist der ENC28J60 überhaupt am Leben?
// PC8 mit Pull-Down → wenn ENC28J60 SO aktiv HIGH treibt = Chip antwortet
// Wenn PC8 bleibt LOW = SO ist High-Z = Chip tot/kein Takt/keine Power
pinMode(ETH_CS_PIN, OUTPUT); digitalWrite(ETH_CS_PIN, HIGH);
pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW);
pinMode(PC8, INPUT_PULLDOWN);
DebugSerial.println("SO-Aktivitaetstest (PC8 = INPUT_PULLDOWN):");
// Test 1: CS hoch (SO sollte High-Z sein → LOW erwartet)
uint8_t so_cs_high = digitalRead(PC8);
DebugSerial.print(" CS=H SO="); DebugSerial.print(so_cs_high);
DebugSerial.println(so_cs_high == 0 ? " (High-Z, erwartet)" : " (getrieben! unerwartet)");
// Test 2: CS runter (ENC28J60 soll SO aktivieren)
digitalWrite(ETH_CS_PIN, LOW);
delayMicroseconds(50);
uint8_t so_cs_low = digitalRead(PC8);
digitalWrite(ETH_CS_PIN, HIGH);
DebugSerial.print(" CS=L SO="); DebugSerial.print(so_cs_low);
if (so_cs_low == 1) DebugSerial.println(" --> Chip treibt SO! Chip ist am Leben.");
else DebugSerial.println(" --> SO bleibt LOW = Chip antwortet nicht (kein Takt? kein Strom?)");
// Test 3: Reset-Zyklus, dann CS low
digitalWrite(ETH_RST_PIN, LOW); delay(10);
digitalWrite(ETH_RST_PIN, HIGH); delay(100);
digitalWrite(ETH_CS_PIN, LOW);
delayMicroseconds(50);
uint8_t so_after_reset = digitalRead(PC8);
digitalWrite(ETH_CS_PIN, HIGH);
DebugSerial.print(" nach Reset, CS=L SO="); DebugSerial.print(so_after_reset);
if (so_after_reset == 1) DebugSerial.println(" --> Chip lebt!");
else DebugSerial.println(" --> immer noch kein Leben");
// ============================================================
// Roher SPI-Test: ESTAT-Register lesen (kein EthernetENC)
// ENC28J60 ESTAT = Bank0, Addr 0x1D
// Read Control Register: opcode 000 | addr 11101 = 0x1D
// Erwarteter Wert nach Reset: 0x01 (CLKRDY-Bit gesetzt)
// ============================================================
// Langer Reset-Zyklus und Wartezeit für CLKRDY
pinMode(ETH_RST_PIN, OUTPUT);
digitalWrite(ETH_RST_PIN, LOW); delay(20);
digitalWrite(ETH_RST_PIN, HIGH); delay(800); // 25 MHz Quarz braucht <1 ms
pinMode(ETH_CS_PIN, OUTPUT); digitalWrite(ETH_CS_PIN, HIGH);
pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW);
pinMode(ETH_MISO_PIN, INPUT); // kein Pull — Chip treibt SO direkt
// Inline-Hilfsfunktion: 1 Byte über SPI senden und empfangen
// MOSI-Pin wird als Parameter übergeben (Kandidaten-Scan)
auto spiXfer = [](uint8_t mosiPin, uint8_t out) -> uint8_t {
uint8_t in = 0;
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(mosiPin, (out >> i) & 1);
digitalWrite(ETH_SCK_PIN, HIGH);
in = (in << 1) | digitalRead(ETH_MISO_PIN);
digitalWrite(ETH_SCK_PIN, LOW);
}
return in;
};
auto readESTAT = [&](uint8_t mosiPin) -> uint8_t {
digitalWrite(ETH_CS_PIN, LOW);
spiXfer(mosiPin, 0x1D); // RCR ESTAT
uint8_t val = spiXfer(mosiPin, 0x00);
digitalWrite(ETH_CS_PIN, HIGH);
return val;
};
// MOSI-Kandidaten
const uint8_t MOSI_CANDIDATES[] = { PC9, PB14, PB15, PB10, PB11, PA5, PA7 };
const char* MOSI_NAMES[] = {"PC9","PB14","PB15","PB10","PB11","PA5","PA7"};
const uint8_t NUM_CAND = sizeof(MOSI_CANDIDATES) / sizeof(MOSI_CANDIDATES[0]);
DebugSerial.println("SPI ESTAT-Scan (Erwartung: 0x01):");
for (uint8_t c = 0; c < NUM_CAND; c++) {
uint8_t pin = MOSI_CANDIDATES[c];
pinMode(pin, OUTPUT); digitalWrite(pin, LOW);
// Chip-Reset zwischen Kandidaten
digitalWrite(ETH_RST_PIN, LOW); delay(5);
digitalWrite(ETH_RST_PIN, HIGH); delay(50);
uint8_t estat = readESTAT(pin);
DebugSerial.print(" MOSI="); DebugSerial.print(MOSI_NAMES[c]);
DebugSerial.print(" -> ESTAT=0x"); DebugSerial.print(estat, HEX);
if (estat == 0x01) DebugSerial.println(" <-- TREFFER! SPI OK");
else if (estat == 0xFF) DebugSerial.println(" (kein Kontakt)");
else DebugSerial.println(" (unbekannt)");
pinMode(pin, INPUT); // danach wieder floating lassen
}
DebugSerial.println("Scan fertig.");
DebugSerial.println("Setup done (Ethernet deaktiviert bis MOSI gefunden).");
}
// ============================================================
// Loop
// ============================================================
unsigned long lastUpdate = 0;
void loop() {
// MQTT Verbindung halten
if (!mqtt.connected()) {
if (!mqttReconnect()) {
delay(5000);
return;
}
}
mqtt.loop();
if (millis() - lastUpdate < UPDATE_INTERVAL) return;
lastUpdate = millis();
char stateTopic[64];
char valueStr[16];
// Modbus temporär deaktiviert — Sensor-Loop übersprungen
DebugSerial.println("loop: MQTT ok, Modbus disabled");
(void)stateTopic;
(void)valueStr;
}