ESP32 und Arduino

ESP32 als Modbus-RS485-zu-MQTT-Gateway: RTU-Geräte abfragen und Registerwerte veröffentlichen

Machen Sie aus einem ESP32 und einem 3,3-V-RS485-Transceiver einen kleinen Modbus-RTU-Master, der Zähler und Sensoren abfragt, Registerwerte skaliert und sie als JSON per MQTT veröffentlicht.

ESP32-Modbus-RS485-MQTT-Gateway, verdrahtet mit einem Energiezähler, das Registerwerte an einen Cloud-Broker veröffentlicht
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Mit einem ESP32 als Modbus-RS485-MQTT-Gateway holen Sie Daten aus Energiezählern, Frequenzumrichtern, Temperaturtransmittern und anderen Modbus-RTU-Geräten und bringen sie ins WLAN, ganz ohne SPS oder Industrie-PC. In diesem Tutorial verdrahten Sie einen RS485-Transceiver mit einem ESP32, fragen mit der ModbusMaster-Bibliothek Register von einem oder mehreren Slaves ab, skalieren die Rohwerte und veröffentlichen sie als JSON an einen MQTT Broker.

Was das Gateway leistet

Modbus RTU ist ein abfragebasiertes Master-Slave-Protokoll: Auf dem Bus passiert nichts, bis ein Master einen bestimmten Slave nach einem bestimmten Registerblock fragt. MQTT ist das Gegenteil, ein ereignisgesteuertes Publish-Subscribe-Protokoll. Der ESP32 sitzt zwischen beiden:

  1. Er arbeitet als Modbus-Master auf dem RS485-Bus und fragt jedes konfigurierte Gerät in einem festen Intervall ab.
  2. Er wandelt rohe 16-Bit-Register anhand einer Registertabelle in physikalische Werte um (Volt, Grad, Kilowattstunden).
  3. Er arbeitet als MQTT-Client und veröffentlicht pro Gerät eine JSON-Nachricht auf einem Topic wie connectmq/demo/mb-4d1e/meter1.

Viele Geräte können sich ein RS485-Adernpaar teilen (jedes mit eigener Slave-Adresse), sodass ein einziger ESP32 einen kleinen Schaltschrank voller Messgeräte bedienen kann.

Im Produktivbetrieb ersetzt eine Topic-Struktur nach ISA-95 das Demo-Präfix: Enterprise, Site, Area, Line und Cell werden zu Ebenen eines Unified Namespace (UNS) auf dem MQTT-Broker, etwa acme/werk-stuttgart/montage/linie-1/zelle-3/energiezaehler. So landen die Daten aus Bestandsanlagen im Mittelstand an derselben Stelle wie die von SPS, OPC-UA-Servern und MES, und jeder Konsument findet sie über den Anlagenpfad statt über Gateway-Namen. Wie das mit Sparkplug B zusammenpasst, erklärt Sparkplug B und der Unified Namespace.

Hardware

Bauteil Hinweise
ESP32 DevKit Nutzt UART2 für RS485
3,3-V-RS485-Transceiver (MAX3485, SP3485) Der klassische MAX485 ist ein 5-V-Baustein; bevorzugen Sie einen 3,3-V-Chip
Modbus-RTU-Gerät Energiezähler, Temperatur-/Feuchtetransmitter, Frequenzumrichter...
120-Ω-Widerstand Abschluss am fernen Ende langer Busse
Twisted-Pair-Kabel Geschirmtes, verdrilltes Kabel für alles jenseits eines Labortests

Manche Transceiver-Boards schalten die Richtung automatisch um und führen nur RX und TX heraus. Auch diese funktionieren: Lassen Sie den DE/RE-Pin im folgenden Code einfach ungenutzt.

Verdrahtung

Transceiver-Pin ESP32-Pin Zweck
VCC 3V3 Versorgung
GND GND Masse, wenn vorhanden auch mit der Signalmasse des Geräts verbinden
RO GPIO 16 (RX2) Daten vom Bus zum ESP32
DI GPIO 17 (TX2) Daten vom ESP32 zum Bus
DE und RE (gebrückt) GPIO 4 High zum Senden, Low zum Empfangen
A / B Gerät A / B Das Busadernpaar

Der häufigste Verdrahtungsfehler sind vertauschte A- und B-Leitungen. Hersteller sind sich nicht einig, welche davon „Plus“ ist. Wenn Sie also nur Timeouts erhalten, tauschen Sie die beiden, bevor Sie irgendetwas anderes debuggen. Abschlusswiderstände gehören an die beiden physischen Enden des Busses, nicht an jedes Gerät.

Kennen Sie Ihre Registertabelle

Jedes Modbus-Gerät dokumentiert seine Register in einer Tabelle: Adresse, Funktionscode, Datentyp und Skalierung. Lesen Sie sie sorgfältig, denn sie beantwortet die Fragen, die die meisten Gateway-Fehler verursachen:

  • Funktionscode: Holding-Register werden mit Funktion 03 gelesen, Input-Register mit Funktion 04. Eine Anfrage mit der falschen Funktion liefert eine Exception oder gar nichts.
  • Adressversatz: Eine Dokumentation, die „30001“ oder „40001“ angibt, meint meist Registeradresse 0 mit Funktion 04 oder 03. Manche Handbücher listen nullbasierte Adressen, andere einsbasierte.
  • Skalierung: Viele Geräte senden 235 und meinen damit 23,5 Grad (Faktor 0,1).
  • Datentyp: 16 Bit vorzeichenlos, 16 Bit vorzeichenbehaftet oder 32-Bit-Werte, die über zwei Register verteilt sind, manchmal mit vertauschter Wortreihenfolge.
  • Serielle Einstellungen: Baudrate, Parität und Stoppbits müssen exakt übereinstimmen. 9600 8N1 ist verbreitet, laut Standard ist jedoch 9600 8E1 die Modbus-Voreinstellung.

Der ESP32-Sketch für Modbus RS485 und MQTT

Installieren Sie ModbusMaster von Doc Walker und PubSubClient von Nick O'Leary über den Bibliotheksverwalter. Die Registertabelle oben ist der einzige Teil, den Sie für Ihre Geräte ändern müssen. Das Beispiel fragt zwei Geräte ab: einen Temperatur-/Feuchtetransmitter an Adresse 1 und einen Energiezähler an Adresse 2.

#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "mb-4d1e-gateway";
const char* TOPIC_PREFIX  = "connectmq/demo/mb-4d1e";

const int RS485_RX = 16;
const int RS485_TX = 17;
const int RS485_DE = 4;
const uint32_t BAUD = 9600;
const uint32_t POLL_INTERVAL_MS = 10000;

struct Point {
  const char* device;    // wird zur Topic-Ebene
  uint8_t     slave;     // Modbus-Adresse
  uint8_t     fc;        // 3 = Holding, 4 = Input
  uint16_t    reg;       // Registeradresse (nullbasiert)
  uint8_t     words;     // 1 = 16 Bit, 2 = 32 Bit (High-Word zuerst)
  bool        isSigned;
  float       scale;
  const char* name;      // JSON-Schlüssel
};

// Diese Tabelle an die Dokumentation Ihrer Geräte anpassen
Point points[] = {
  {"climate1", 1, 4, 0x0001, 1, true,  0.1,  "temperature_c"},
  {"climate1", 1, 4, 0x0002, 1, false, 0.1,  "humidity_pct"},
  {"meter1",   2, 3, 0x0000, 1, false, 0.1,  "voltage_v"},
  {"meter1",   2, 3, 0x0001, 1, false, 0.01, "current_a"},
  {"meter1",   2, 3, 0x0010, 2, false, 0.01, "energy_kwh"},
};
const size_t NUM_POINTS = sizeof(points) / sizeof(points[0]);

ModbusMaster node;
WiFiClient net;
PubSubClient mqtt(net);
uint32_t lastPoll = 0;

void preTx()  { digitalWrite(RS485_DE, HIGH); }
void postTx() { digitalWrite(RS485_DE, LOW); }

bool readPoint(const Point& p, float& out) {
  node.begin(p.slave, Serial2);
  uint8_t rc = (p.fc == 4) ? node.readInputRegisters(p.reg, p.words)
                           : node.readHoldingRegisters(p.reg, p.words);
  if (rc != node.ku8MBSuccess) {
    Serial.printf("slave %u reg 0x%04X error 0x%02X\n", p.slave, p.reg, rc);
    return false;
  }
  if (p.words == 2) {
    uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) | node.getResponseBuffer(1);
    out = p.isSigned ? (int32_t)raw * p.scale : raw * p.scale;
  } else {
    uint16_t raw = node.getResponseBuffer(0);
    out = p.isSigned ? (int16_t)raw * p.scale : raw * p.scale;
  }
  return true;
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(500);
}

void connectMqtt() {
  char statusTopic[96];
  snprintf(statusTopic, sizeof(statusTopic), "%s/status", TOPIC_PREFIX);
  while (!mqtt.connected()) {
    if (mqtt.connect(CLIENT_ID, statusTopic, 1, true, "offline")) {
      mqtt.publish(statusTopic, "online", true);
    } else {
      delay(5000);
    }
  }
}

void pollAndPublish() {
  // Aufeinanderfolgende Punkte desselben Geräts zu einer JSON-Nachricht bündeln
  size_t i = 0;
  while (i < NUM_POINTS) {
    const char* dev = points[i].device;
    String json = "{";
    int ok = 0, failed = 0;
    while (i < NUM_POINTS && strcmp(points[i].device, dev) == 0) {
      float v;
      if (readPoint(points[i], v)) {
        if (ok > 0) json += ",";
        json += "\"" + String(points[i].name) + "\":" + String(v, 2);
        ok++;
      } else {
        failed++;
      }
      delay(20);                      // Pause zwischen Frames, hilft langsamen Geräten
      i++;
    }
    if (ok > 0) json += ",";
    json += "\"errors\":" + String(failed) + "}";

    char topic[96];
    snprintf(topic, sizeof(topic), "%s/%s", TOPIC_PREFIX, dev);
    mqtt.publish(topic, json.c_str(), true);
    Serial.printf("%s %s\n", topic, json.c_str());
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RS485_DE, OUTPUT);
  digitalWrite(RS485_DE, LOW);
  Serial2.begin(BAUD, SERIAL_8N1, RS485_RX, RS485_TX);
  node.preTransmission(preTx);
  node.postTransmission(postTx);

  connectWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
  connectMqtt();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();
  if (millis() - lastPoll >= POLL_INTERVAL_MS) {
    lastPoll = millis();
    pollAndPublish();
  }
}

Einige Details in diesem Code sind wichtiger, als sie aussehen:

  • preTransmission und postTransmission steuern den DE-Pin, sodass der Transceiver nur sendet, während der ESP32 überträgt. Ohne sie gibt der Transceiver den Bus nie frei, und jede Abfrage läuft in einen Timeout.
  • Jede veröffentlichte Nachricht enthält einen errors-Zähler. Ein Dashboard kann dann „Gerät vorhanden, aber einige Register schlagen fehl“ anzeigen, statt stillschweigend auf alten Werten stehen zu bleiben.
  • Die Nachrichten sind retained, sodass ein neu verbundenes Dashboard sofort die letzten Werte sieht.
  • setBufferSize(512) schafft Platz für längeres JSON, wenn Sie weitere Punkte hinzufügen.

Prüfen Sie die Ausgabe:

mosquitto_sub -h broker.connectmq.com -p 1883 -i mb-4d1e-viewer -t "connectmq/demo/mb-4d1e/#" -v

ModbusMaster-Fehlercodes verstehen

Code Bedeutung Typische Ursache
0xE2 Antwort-Timeout Falsche A/B-Polarität, falsche Baudrate oder Parität, falsche Slave-Adresse, DE-Pin schaltet nicht
0xE0 Ungültige Slave-ID in der Antwort Zwei Geräte mit derselben Adresse oder Störungen
0xE3 Ungültige CRC Fehlender Abschluss, langes ungeschirmtes Kabel, Massepotenzialversatz
0x02 Unzulässige Datenadresse Register existiert nicht; nullbasierte vs. einsbasierte Adressierung prüfen
0x01 Unzulässige Funktion Funktion 03 für ein Register verwendet, das nur auf 04 antwortet, oder umgekehrt

Checkliste zur Fehlersuche für ESP32 Modbus RS485 MQTT

  1. Prüfen Sie die seriellen Einstellungen im Gerätemenü oder Handbuch: Baudrate, Parität, Stoppbits, Adresse.
  2. Testen Sie ein Gerät mit einem USB-RS485-Adapter und einem Modbus-Tool am PC, bevor der ESP32 ins Spiel kommt. Wenn der PC es nicht lesen kann, kann es der ESP32 auch nicht.
  3. Tauschen Sie A und B, wenn jede Anfrage in einen Timeout läuft.
  4. Setzen Sie bei Bussen, die länger als ein paar Meter sind, an beiden Enden 120-Ω-Abschlüsse und verbinden Sie die Signalmasse.
  5. Halten Sie das Abfrageintervall realistisch. Ein Dutzend Register bei 9600 Baud benötigen einen spürbaren Bruchteil einer Sekunde; fragen Sie nicht schneller ab, als der Bus antworten kann.
  6. Lesen Sie zusammenhängende Register nach Möglichkeit in einer Anfrage. Eine Anfrage für zehn Register ist deutlich schneller als zehn Anfragen für je eines.

Weniger veröffentlichen, nicht mehr

Alle zehn Sekunden abzufragen und alles zu veröffentlichen, ist das einfachste Design, doch Zähler ändern sich langsam und Cloud-Traffic summiert sich. Zwei Verbesserungen lassen sich leicht in den Sketch einbauen: Veröffentlichen Sie einen Punkt nur, wenn er sich um mehr als ein Totband ändert, und senden Sie alle paar Minuten einen vollständigen Snapshot als Heartbeat. Dieses Muster wird unter Report by Exception erklärt. Eine Gateway-Sicht auf dasselbe Problem, einschließlich PC-basierter Gateways und größerer RTU-Netze, finden Sie unter Modbus RTU zu MQTT.

Wenn MQTT selbst neu für Sie ist, beginnen Sie mit was ein MQTT Broker macht. Um JSON mit einer Bibliothek statt per String-Verkettung zu erzeugen, ist der ArduinoJson-MQTT-Leitfaden eine gute Ergänzung. Das ESP32-IoT-Tutorial behandelt die WLAN- und Reconnect-Grundlagen, die hier verwendet werden.

Härtung für den Produktivbetrieb

Ein Gateway an einer Fabrikwand läuft jahrelang, behandeln Sie es also wie Infrastruktur:

  • TLS und Zugangsdaten. Verwenden Sie Port 8883 mit WiFiClientSecure und Zugangsdaten, die für genau dieses Gateway ausgestellt wurden. Veröffentlichen Sie Anlagendaten niemals auf einem öffentlichen Test-Topic.
  • Watchdog. Aktivieren Sie den Task-Watchdog, damit eine hängende Modbus-Transaktion oder ein hängender WLAN-Stack das Board neu startet.
  • Galvanische Trennung. In elektrisch verrauschten Schaltschränken verhindert ein isolierter RS485-Transceiver, dass Masseschleifen den ESP32 beschädigen.
  • Standardmäßig nur lesend. Das Schreiben von Registern per MQTT kann Motoren starten oder Sollwerte ändern. Wenn Sie Schreibzugriffe hinzufügen, beschränken Sie sie auf eine Allowlist von Registern und validieren Sie die Wertebereiche.
  • Netzsegmentierung. Trennen Sie das Gateway-Netz vom Büro-IT-Netz und lassen Sie nur ausgehende MQTT-Verbindungen zu; das entspricht dem Zonen-und-Conduit-Modell aus IEC 62443.
  • Broker-Wahl. Ein verwalteter MQTT-Broker nimmt Broker-Patches und Zertifikatserneuerung von Ihrer To-do-Liste, und der Topic-Explorer von ConnectMQ zeigt schnell, ob jedes Gerät Daten meldet.

Starten Sie mit ConnectMQ

Entwickeln Sie Ihren Prototyp auf dem öffentlichen MQTT-Broker broker.connectmq.com, der nur für Tests gedacht ist, und geben Sie Ihrem Gateway danach einen privaten Broker. ConnectMQ bietet TLS-Endpunkte, Zugangsdaten pro Broker, einen Live-Topic-Explorer und eine Geräteansicht, die für industrielle Topics nach ISA-95-Ebenen gegliedert ist. Erstellen Sie unter app.connectmq.com einen kostenlosen Serverless-Broker ohne Kreditkarte und richten Sie den Sketch darauf aus.

Häufig gestellte Fragen

Kann ein ESP32 mehrere Modbus-Geräte auslesen?

Ja. Geben Sie jedem Gerät auf demselben RS485-Adernpaar eine eindeutige Slave-Adresse und fügen Sie seine Register der Punkttabelle hinzu. Der ESP32 fragt sie nacheinander ab.

Brauche ich einen MAX485 oder einen MAX3485?

Der ESP32 arbeitet mit 3,3-V-Logik, daher ist ein 3,3-V-Transceiver wie der MAX3485 oder SP3485 die saubere Wahl. Ein 5-V-MAX485-Board kann funktionieren, erfordert aber Sorgfalt beim Pegel auf der RO-Leitung.

Warum erhalte ich nur Timeout-Fehler?

Fast immer liegt es an Verdrahtung oder Einstellungen: vertauschte A/B-Leitungen, falsche Baudrate oder Parität, falsche Slave-Adresse oder ein DE/RE-Pin, der nicht umschaltet. Testen Sie das Gerät zuerst mit einem PC-Tool.

Kann das Gateway per MQTT in Modbus-Register schreiben?

ModbusMaster unterstützt Schreibzugriffe, es ist also möglich. Beschränken Sie Schreibvorgänge jedoch auf bestimmte Register und validieren Sie die Werte. Ungeprüfte Fernschreibzugriffe auf Industrieanlagen sind ein Sicherheitsrisiko.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.