EnergyMeter

EnergyMeter mit digitalen Modbus Zähler SDM630 V2 und ESP32

Schaltung:
Der SDM630 hat zwei Impulsausgänge welche einen offenen Kollektor-Ausgang haben.
Die am Zähler mit + bezeichneten Anschlüsse werden mit der positiven Versorgungsspannung des ESP32 verbunden. Die mit – bezeichneten Anschlüsse (offene Kollektor) werden über eine Entprellschaltung (10k, 1k, 1µF) an D2 und D4 des ESP32 angeschlossen. Wenn man die Impulse nicht benötigt, da die elektrischen Kennwerte über die Modbus-Schnittstelle ausgelesen werden, kann man die 10kΩ und 1kΩ Widerstände und die beiden 1µF Kondensatoren weglassen. Der 2200µF Kondensator dient zur Glättung/Stabilisierung der Betriebsspannung.

Um den Modbus-Ausgang des Zählers zu lesen, verwendet man am besten ein MAX485* Modul.
Anschlüsse wie folgt:

MAX485 ESP32 SDM630
VCC 3V3
GND GND
RO RX2
DI TX2
DE und RE D5
A A
B B

Der übliche 120Ω Abschlusswiderstand (DC-Terminierung) parallel zu den AB des Zählers war bei mir nicht notwendig, da nur ca. 10 cm Kabellänge zwischen Zähler und MAX485-Modul waren. Außerdem ist auf dem Modul schon ein 120Ω (R7) verlötet.

Aufbau:
Platine mit passiven Bauteilen

Platine mit aktiven Bauteilen

Platine eingebaut

Software:
Bei Github gibt es eine Bibliothek, die man in seine Arduino-Entwicklungsumgebung einbinden sollte. Da die Bibliothek zum jetzigen Zeitpunkt nicht im Bibliotheksverwalter von Arduino angeboten wird, lädt man sich am besten die zip-Datei von Github herunter und kopiert den Inhalt manuell in das libraries-Verzeichnis von Arduino. In der zip ist auch Beispielcode enthalten, mit dem man anfangen sollte.

Auszug Quellcode (modbus.h):

// Arduino-Board-Auswahl: DOIT ESP32 DEVKIT V1
/*                 esp32 30PIN
             _______________________
            /      ESP-WROOM-32     \
            |o EN             D23 o |
            |o VP             D22 o |
            |o VN             TX0 o |
            |o D34            RX0 o |
            |o D35            D21 o |
            |o D32            D19 o |
            |o D33            D18 o |
            |o D25            D5  o |<--- DE_RE     (MAX485)
            |o D26         17 TX2 o |<--- HW-Serial (MAX485)
            |o D27         16 RX2 o |<--- HW-Serial (MAX485)
            |o D14            D4  o |<--- S0_2      (SDM630)
            |o D12            D2  o |<--- S0_1      (SDM630)
            |o D13            D15 o |
            |o GND            GND o |<--- -
    +5V --->|o VIN    _____   3V3 o |---> MAX485
            |_________|___|_________|

  //------------------------------------------------------------------------------*/

//#define debug

#include <WiFi.h>
#include <WiFiUdp.h>
#include <ESPmDNS.h>

#include <SDM_Config_User.h>  //d:\Save\Arduino\libraries\SDM_Energy_Meter\SDM_Config_User.h
#define DERE_PIN      5       //D5 am ESP32 - 30PIN-Variante (DERE_PIN auch in SDM_Config_User.h definiert)
#include <SDM.h>              //https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h

#include "index_page.h"

#ifdef USE_HARDWARESERIAL     // in \libraries\SDM_Energy_Meter\SDM_Config_User.h definiert

// for ESP32
//              ______________________________________hardware serial reference
//             |      ________________________________baudrate(optional, default from SDM_Config_User.h)
//             |     |           _____________________dere pin for max485(optional, default from SDM_Config_User.h)
//             |     |          |            _________hardware uart config(optional, default from SDM_Config_User.h)
//             |     |          |           |    _____rx pin number(optional, default from SDM_Config_User.h)
//             |     |          |           |   |    _tx pin number(optional, default from SDM_Config_User.h)
//             |     |          |           |   |   |
//SDM sdm(Serial, 9600, NOT_A_PIN, SERIAL_8N1, 13, 15);
SDM sdm(Serial1, 9600, DERE_PIN, SERIAL_8N1, 16, 17);   // 9600 muss auch im Zähler eingestellt werden
//SDM sdm(Serial1, 38400, DERE_PIN, SERIAL_8N1, 16, 17);  // 38400 muss auch im Zähler eingestellt werden

#else

// for ESP8266 and ESP32
SoftwareSerial swSerSDM;
//                ________________________________________software serial reference
//               |      __________________________________baudrate(optional, default from SDM_Config_User.h)
//               |     |           _______________________dere pin for max485(optional, default from SDM_Config_User.h)
//               |     |          |              _________software uart config(optional, default from SDM_Config_User.h)
//               |     |          |             |    _____rx pin number(optional, default from SDM_Config_User.h)
//               |     |          |             |   |    _tx pin number(optional, default from SDM_Config_User.h)
//               |     |          |             |   |   |
//SDM sdm(swSerSDM, 9600, NOT_A_PIN, SWSERIAL_8N1, 13, 15);
SDM sdm(swSerSDM, 9600, DERE_PIN, SWSERIAL_8N1, 16, 17);

#endif


time_t zeit;
unsigned long readtimeStart;
unsigned long readtimeEnde;

typedef volatile struct {
  volatile float regvalarr;
  const uint16_t regarr;
  char* varname;
  int dezi;
  char* masseinheit;
  int idx;
} sdm_struct;


/* Created with btnModbus - D:\Save\Arduino\_stuff\SDM630\Tools.xlsb - 21.12.2023 21:12:39
  https://bg-etech.de/download/manual/SDM630Register1-5.pdf
  https://github.com/reaper7/SDM_Energy_Meter/blob/master/SDM.h */

volatile uint32_t S0_1, S0_2;
//volatile float L1_L2_V,L2_L3_V,L3_L1_V,L1_V,L2_V,L3_V,L1_A,L2_A,L3_A,L1_W,L2_W,L3_W,L1_VA,L2_VA,L3_VA,L1_VAr,L2_VAr,L3_VAr,L1_PF,L2_PF,L3_PF,L1_GRAD,L2_GRAD,L3_GRAD,L1_kWh,L2_kWh,L3_kWh,L1_kVArh,L2_kVArh,L3_kVArh,L1_V_TDH,L2_V_TDH,L3_V_TDH,L1_A_MAX,L2_A_MAX,L3_A_MAX,V_AVG,I_SUM,P_SUM,VA_SUM,VAr_SUM,PF,kWh,Export_kVArh,P_MAX,VA_MAX,Hz,I_NULL;

//------------------------------------------------------------------------------

#define NBREG 48           //bitte in der index_page.h dieselbe zahl (arr-len) in der for schleife eingeben
//{float Value, REGISTER NAME, Bezeichnung, Dezimalstellen, Maßeinheit, Index}
volatile sdm_struct sdmarr[NBREG] = {
  {0.00, SDM_LINE_1_TO_LINE_2_VOLTS, (char*)"L1_L2_V", 1, (char*)"V", 0},
  {0.00, SDM_LINE_2_TO_LINE_3_VOLTS, (char*)"L1_L2_V", 1, (char*)"V", 1},
  {0.00, SDM_LINE_3_TO_LINE_1_VOLTS, (char*)"L1_L2_V", 1, (char*)"V", 2},
  {0.00, SDM_PHASE_1_VOLTAGE, (char*)"L1_V", 1, (char*)"V", 3},
  {0.00, SDM_PHASE_2_VOLTAGE, (char*)"L1_V", 1, (char*)"V", 4},
  {0.00, SDM_PHASE_3_VOLTAGE, (char*)"L1_V", 1, (char*)"V", 5},
  {0.00, SDM_PHASE_1_CURRENT, (char*)"L1_A", 3, (char*)"A", 6},
  {0.00, SDM_PHASE_2_CURRENT, (char*)"L1_A", 3, (char*)"A", 7},
  {0.00, SDM_PHASE_3_CURRENT, (char*)"L1_A", 3, (char*)"A", 8},
  {0.00, SDM_PHASE_1_POWER, (char*)"L1_W", 1, (char*)"W", 9},
  {0.00, SDM_PHASE_2_POWER, (char*)"L1_W", 1, (char*)"W", 10},
  {0.00, SDM_PHASE_3_POWER, (char*)"L1_W", 1, (char*)"W", 11},
  {0.00, SDM_PHASE_1_APPARENT_POWER, (char*)"L1_VA", 1, (char*)"VA", 12},
  {0.00, SDM_PHASE_2_APPARENT_POWER, (char*)"L1_VA", 1, (char*)"VA", 13},
  {0.00, SDM_PHASE_3_APPARENT_POWER, (char*)"L1_VA", 1, (char*)"VA", 14},
  {0.00, SDM_PHASE_1_REACTIVE_POWER, (char*)"L1_VAr", 1, (char*)"VAr", 15},
  {0.00, SDM_PHASE_2_REACTIVE_POWER, (char*)"L1_VAr", 1, (char*)"VAr", 16},
  {0.00, SDM_PHASE_3_REACTIVE_POWER, (char*)"L1_VAr", 1, (char*)"VAr", 17},
  {0.00, SDM_PHASE_1_POWER_FACTOR, (char*)"L1_PF", 2, (char*)"cos Phi", 18},
  {0.00, SDM_PHASE_2_POWER_FACTOR, (char*)"L1_PF", 2, (char*)"cos Phi", 19},
  {0.00, SDM_PHASE_3_POWER_FACTOR, (char*)"L1_PF", 2, (char*)"cos Phi", 20},
  {0.00, SDM_PHASE_1_ANGLE, (char*)"L1_GRAD", 1, (char*)"Grad", 21},
  {0.00, SDM_PHASE_2_ANGLE, (char*)"L1_GRAD", 1, (char*)"Grad", 22},
  {0.00, SDM_PHASE_3_ANGLE, (char*)"L1_GRAD", 1, (char*)"Grad", 23},
  {0.00, SDM_L1_IMPORT_ACTIVE_ENERGY, (char*)"L1_kWh", 1, (char*)"kWh", 24},
  {0.00, SDM_L2_IMPORT_ACTIVE_ENERGY, (char*)"L1_kWh", 1, (char*)"kWh", 25},
  {0.00, SDM_L3_IMPORT_ACTIVE_ENERGY, (char*)"L1_kWh", 1, (char*)"kWh", 26},
  {0.00, SDM_L1_EXPORT_REACTIVE_ENERGY, (char*)"L1_kVArh", 1, (char*)"kVArh", 27},
  {0.00, SDM_L2_EXPORT_REACTIVE_ENERGY, (char*)"L1_kVArh", 1, (char*)"kVArh", 28},
  {0.00, SDM_L3_EXPORT_REACTIVE_ENERGY, (char*)"L1_kVArh", 1, (char*)"kVArh", 29},
  {0.00, SDM_PHASE_1_LN_VOLTS_THD, (char*)"L1_V_TDH", 3, (char*)"%", 30},
  {0.00, SDM_PHASE_2_LN_VOLTS_THD, (char*)"L1_V_TDH", 3, (char*)"%", 31},
  {0.00, SDM_PHASE_3_LN_VOLTS_THD, (char*)"L1_V_TDH", 3, (char*)"%", 32},
  {0.00, SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND, (char*)"L1_A_MAX", 3, (char*)"A", 33},
  {0.00, SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND, (char*)"L1_A_MAX", 3, (char*)"A", 34},
  {0.00, SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND, (char*)"L1_A_MAX", 3, (char*)"A", 35},
  {0.00, SDM_AVERAGE_L_TO_N_VOLTS, (char*)"V_AVG", 1, (char*)"V", 36},
  {0.00, SDM_SUM_LINE_CURRENT, (char*)"I_SUM", 3, (char*)"A", 37},
  {0.00, SDM_TOTAL_SYSTEM_POWER, (char*)"P_SUM", 1, (char*)"W", 38},
  {0.00, SDM_TOTAL_SYSTEM_APPARENT_POWER, (char*)"VA_SUM", 1, (char*)"VA", 39},
  {0.00, SDM_TOTAL_SYSTEM_REACTIVE_POWER, (char*)"VAr_SUM", 1, (char*)"VAr", 40},
  {0.00, SDM_TOTAL_SYSTEM_POWER_FACTOR, (char*)"PF", 2, (char*)"cos Phi", 41},
  {0.00, SDM_TOTAL_ACTIVE_ENERGY, (char*)"kWh", 1, (char*)"kWh", 42},
  {0.00, SDM_EXPORT_REACTIVE_ENERGY, (char*)"Export_kVArh", 1, (char*)"kVArh", 43},
  {0.00, SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND, (char*)"P_MAX", 1, (char*)"W", 44},
  {0.00, SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND, (char*)"VA_MAX", 1, (char*)"VA", 45},
  {0.00, SDM_FREQUENCY, (char*)"Hz", 3, (char*)"Hz", 46},
  {0.00, SDM_NEUTRAL_CURRENT, (char*)"I_NULL", 3, (char*)"A", 47}
};

/* End Created with D:\Save\Arduino\_stuff\SDM630\Tools.xlsb ------------------ */

//------------------------------------------------------------------------------

#ifdef debug
void getModbusValue() {
  char buf[64];
  for (uint8_t i = 0; i < NBREG; i++) {
    sprintf(buf, "%s %.3f %s", sdmarr[i].varname, sdmarr[i].regvalarr, sdmarr[i].masseinheit);
    Serial.println(buf);
  }
}
#endif

//------------------------------------------------------------------------------

bool plausiMesswerte() {
  //https://www.netzfrequenz.info/aktuelle-netzfrequenz-full#:~:text=Abweichungen%20von%20%2B%2F%2D180mHz%20sind,von%20Erzeugungskapazit%C3%A4ten%20oder%20Abnehmern%20auftreten.
  //  return (P_SUM > 0 && Hz >= 49 && Hz <= 51);
  return (sdmarr[0].regvalarr > 0);
}

//------------------------------------------------------------------------------

void sdmRead() {

  readtimeStart = millis();
  zeit = DateTime.now();
  uint32_t sdmSerial = sdm.getSerialNumber();

  float tmpval = NAN;

  for (uint8_t i = 0; i < NBREG; i++) {

    tmpval = sdm.readVal(sdmarr[i].regarr);

    if (isnan(tmpval)) {
      sdmarr[i].regvalarr = 0.00;
    } else {
      sdmarr[i].regvalarr = tmpval;
    }

    yield();

  }

  readtimeEnde = millis();

}

//------------------------------------------------------------------------------

void setupModbus() {

  sdm.begin();

#ifdef USE_HARDWARESERIAL
  Serial.println("setupModbus, verwende HardwareSerial");
#else
  Serial.println("setupModbus, verwende SoftwareSerial");
#endif

}

//------------------------------------------------------------------------------

void loopModbus() {

  sdmRead();

  uint16_t lasterror = sdm.getErrCode(true);
  Serial.print("ErrCode: "); Serial.print(lasterror); Serial.print(" - ");
  if (lasterror == SDM_ERR_NO_ERROR)  Serial.println("SDM_ERR_NO_ERROR");
  else if (lasterror == SDM_ERR_ILLEGAL_FUNCTION) Serial.println("SDM_ERR_ILLEGAL_FUNCTION");
  else if (lasterror == SDM_ERR_ILLEGAL_DATA_ADDRESS) Serial.println("SDM_ERR_ILLEGAL_DATA_ADDRESS");
  else if (lasterror == SDM_ERR_ILLEGAL_DATA_VALUE) Serial.println("SDM_ERR_ILLEGAL_DATA_VALUE");
  else if (lasterror == SDM_ERR_SLAVE_DEVICE_FAILURE) Serial.println("SDM_ERR_SLAVE_DEVICE_FAILURE");
  else if (lasterror == SDM_ERR_CRC_ERROR) Serial.println("SDM_ERR_CRC_ERROR");
  else if (lasterror == SDM_ERR_WRONG_BYTES) Serial.println("SDM_ERR_WRONG_BYTES");//  bytes b0,b1 or b2 wrong
  else if (lasterror == SDM_ERR_NOT_ENOUGHT_BYTES) Serial.println("SDM_ERR_NOT_ENOUGHT_BYTES");//  not enough bytes from sdm
  else if (lasterror == SDM_ERR_TIMEOUT) Serial.println("SDM_ERR_TIMEOUT");
  else if (lasterror == SDM_ERR_EXCEPTION) Serial.println("SDM_ERR_EXCEPTION");
  else if (lasterror == SDM_ERR_STILL_WAITING) Serial.println("SDM_ERR_STILL_WAITING");
  sdm.clearErrCode();

#ifdef debug
  getModbusValue();
#endif

  yield();

}
//------------------------------------------------------------------------------

Wer sich etwas mit PHP, JavaScript und HTML auskennt, kann unter Verwendung der Arduino-Bibliothek HTTPClient, die SDM630 Daten an einen Server übertragen, so dass man sich die Verbrauchsdaten auch grafisch anzeigen kann.

void sendeDaten() {

  if (plausiMesswerte()) {

    if (WiFi.status() == WL_CONNECTED) {

      char httpRequestData[1024];
      String payload;

      // Your Domain name with URL path or IP address with path
      http.begin(clientINET, serverURLweb);

      // Specify content-type header
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");

      // Send HTTP POST request
      String ts = DateFormatter::format("%d-%m-%Y_%H:%M:%S", zeit);
      //      sprintf(httpRequestData, "type=sdm630&data=zeit|%s|P_SUM|%.0f|L1_V|%.0f|", ts.c_str(), P_SUM, L1_V);

      /* Created with btnSende - D:\Save\Arduino\_stuff\SDM630\Tools.xlsb - 21.12.2023 21:47:11 */

      sprintf(httpRequestData, "type=sdm630&data=%s|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.3f|%.3f|%.3f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.2f|%.2f|%.2f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.1f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.1f|%.3f|%.1f|%.1f|%.1f|%.2f|%.1f|%.1f|%.1f|%.1f|%.3f|%.3f", ts.c_str(), sdmarr[0].regvalarr, sdmarr[1].regvalarr, sdmarr[2].regvalarr, sdmarr[3].regvalarr, sdmarr[4].regvalarr, sdmarr[5].regvalarr, sdmarr[6].regvalarr, sdmarr[7].regvalarr, sdmarr[8].regvalarr, sdmarr[9].regvalarr, sdmarr[10].regvalarr, sdmarr[11].regvalarr, sdmarr[12].regvalarr, sdmarr[13].regvalarr, sdmarr[14].regvalarr, sdmarr[15].regvalarr, sdmarr[16].regvalarr, sdmarr[17].regvalarr, sdmarr[18].regvalarr, sdmarr[19].regvalarr, sdmarr[20].regvalarr, sdmarr[21].regvalarr, sdmarr[22].regvalarr, sdmarr[23].regvalarr, sdmarr[24].regvalarr, sdmarr[25].regvalarr, sdmarr[26].regvalarr, sdmarr[27].regvalarr, sdmarr[28].regvalarr, sdmarr[29].regvalarr, sdmarr[30].regvalarr, sdmarr[31].regvalarr, sdmarr[32].regvalarr, sdmarr[33].regvalarr, sdmarr[34].regvalarr, sdmarr[35].regvalarr, sdmarr[36].regvalarr, sdmarr[37].regvalarr, sdmarr[38].regvalarr, sdmarr[39].regvalarr, sdmarr[40].regvalarr, sdmarr[41].regvalarr, sdmarr[42].regvalarr, sdmarr[43].regvalarr, sdmarr[44].regvalarr, sdmarr[45].regvalarr, sdmarr[46].regvalarr, sdmarr[47].regvalarr);

      /* End Created with D:\Save\Arduino\_stuff\SDM630\Tools.xlsb ------------------ */

      int httpResponseCode = http.POST(httpRequestData);

      Serial.print("httpRequestData: "); Serial.println(httpRequestData);

      if (httpResponseCode > 0) {
        Serial.print("HTTP Response code: ");
        Serial.println(httpResponseCode);
        payload = http.getString();
        Serial.println(payload);
      } else {
        Serial.print("Error code: ");
        Serial.println(httpResponseCode);
      }

      http.end();// Free resources

      //serverantwort in bufdebug speichern
      sprintf(bufdebug, "%s - %s", payload.c_str(), DateFormatter::format("%H:%M:%S", t0).c_str());


      //die gleichen Daten noch einmal an den raspberry senden
      http.begin(clientINET, serverURLraspi);
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      httpResponseCode = http.POST(httpRequestData);
      http.end();

    }
  }
}

Fortsetzung folgt…

*MAX485 ermöglicht die Kommunikation über den RS485-Bus (Industriestandard für Datenkommunikation)
Verwendung in Heizung, Klima, Ampeln, Stromzähler…

© 12/2023