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:
- Er arbeitet als Modbus-Master auf dem RS485-Bus und fragt jedes konfigurierte Gerät in einem festen Intervall ab.
- Er wandelt rohe 16-Bit-Register anhand einer Registertabelle in physikalische Werte um (Volt, Grad, Kilowattstunden).
- 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
235und 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:
preTransmissionundpostTransmissionsteuern 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
- Prüfen Sie die seriellen Einstellungen im Gerätemenü oder Handbuch: Baudrate, Parität, Stoppbits, Adresse.
- 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.
- Tauschen Sie A und B, wenn jede Anfrage in einen Timeout läuft.
- Setzen Sie bei Bussen, die länger als ein paar Meter sind, an beiden Enden 120-Ω-Abschlüsse und verbinden Sie die Signalmasse.
- 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.
- 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
WiFiClientSecureund 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.



