Inhaltsverzeichnis
Die bisherigen Beispiele basieren auf fertigen Modbus-Slaves für Relais und Sensorik. Im nächsten Schritt wird gezeigt, wie sich ein ESP32 als eigener Modbus-RTU-Slave realisieren lässt. Der ESP32 übernimmt dabei die Modbus-Kommunikation zum Master, während auf der Peripherieseite kostengünstige Standard-Hardware wie Relaismodule, analoge Sensoren oder digitale Sensoren angeschlossen werden können. Dadurch entsteht ein flexibles I/O-Modul, das sich an die jeweiligen Anforderungen anpassen lässt und viele Anwendungsfälle ohne teure Modbus-fähige Feldgeräte ermöglicht.
Um die Kompatibilität zur bisherigen Software weitgehend sicherzustellen, wird der ESP32 zunächst die Funktionalität der bereits verwendeten Modbus-Slaves nachbilden. Die vorhandene WPF-Anwendung kann aber nicht ohne weiteres übernommen werden, da wir statt zwei nur noch einen Slave haben. Zusätzlich vereinheitliche ich die Kommunkation mit den Registern und Coils, indem ich sie alles als 0-basiert definiere.
Die bisher verwendeten Modbus-Geräte für Relais- und Sensorfunktionen werden dabei durch einfache Relaismodule sowie analoge oder digitale Sensoren, wie z.B. dem DHT22 Temperatur- und Luftfeuchtigkeitssensor, ersetzt. Die Modbus-Kommunikation übernimmt ausschließlich der ESP32, während die angeschlossene Peripherie selbst kein Modbus unterstützt.
Ausgangssituation
In Modbus RTU mit ESP32 wurde bereits gezeigt, wie der ESP32 als Modbus-Slave konfiguriert und verwendet werden kann.
In ESP32 Modbus Grundlagen und ESP32 Modbus Grundlagen – Praxis mit Relais und Sensor wurden die Grundlagen erläutert, wie im allgemeinen, Modbus-Slaves über einen Modbus-Master angesteuert werden können.
Dann haben wir in Modbus-RTU-Master in C# auch schon eine Kleine Modbus-Master-Anwendung in C# als WPF realisiert, mit der wir mit Modbus-Slaves kommunizieren können.
Jetzt wollen wir den Kreis schließen und die Modbus-Slaves durch herkömmliche, digitale Bausteine ersetzen, die an den ESP32-Slave angeschlossen werden. Um den vorherigen Beispielen treu zu bleiben, verwende ich jetzt ein herkömmliches TTL-Relais-Modul, welches an die Coils des ESP32-Slave angeschlossen wird, und den DHT22-Sensor, der an zwei Input-Register des ESP32-Slave angeschlossen ist.
Dafür müssen wir das proprietäre Protokoll des DHT22 in den ESP32-Slave als Bibliothek integrieren. Zunächst werden wir den Programm-Code, den wir zum Auslesen des DHT22 in DHT22-Sensor mit ESP32 und ESP-IDF auslesen – ohne Bibliothek entwickelt haben, zu einer robusten Bibliothek weiterentwickeln. Denn nur die ausgewerteten Daten des DHT22 übergeben wir dann den Input-Registern, damit der Master dann direkt verwertbare Daten erhält.
Auf diese Weise, kann ein Modbus-Master beliebige Peripherie steuern und Auslesen, indem er auschließlich mit dem ESP32-Slave über Modbus kommuniziert.
Am Ende verfügen wir über einen eigenen Modbus-Slave auf Basis eines ESP32, der mit dem zuvor entwickelten Modbus-RTU-Master in C# kommuniziert.
Der einzige Unterschied ist, dass wir jetzt nur noch eine Modbus-Slave-Adresse haben, da wir ja nur noch einen Slave haben, nämlich den ESP32.
Steuerung eines TTL-Relaismoduls mit dem ESP32-Modbus-Slave
In den folgenden Abbildungen sehen wir die beiden Relaismodule. Einmal das Modbus-Relaismodul in Abb. 2 und das TTL-Relaismodul in Abb. 1.
Wir werden hier mit dem TTL-Relaismodul weiter arbeiten, da die Modbus Kommunikation ausschließlich über den ESP32 erfolgt. Das TTL-Relaismodul wird direkt über die GPIO-Ausgänge des ESP32 angesteuert.
- TTL-High 5V: Relais aus
- TTL-Low 0V: Relais an
Das bedeutet, dass wir das TTL-Relaismodul direkt an zwei Coils unseres ESP32-Slaves anbinden können, da Coils digitale Ausgänge repräsentieren.
Wir verwenden hier, wie auch schon beim Modbus-Relaismodul in ESP32 Modbus Grundlagen – Praxis mit Relais und Sensor, die gleichen Parameter:
Verwendete Coils, bzw. Modbus-Adressen:
- 2 Coils (1-bit Output Register)
- Adresse 0x00: Relais 1
- Adresse 0x01: Relais 2
- Weitere Register für Konfiguration
- Adressierung: 0-Basiert (Deswegen Offset 1 in Simply Modbus bzw. 00001)
Für den Master wird es am Ende genau so aussehen, als wenn wir das Modbus-Relaismodul anschließen würden.
Integration des TTL-Relaismoduls in den ESP32-Slave
In Modbus RTU mit ESP32, hatten wir bereits 8 Coils für 8 digitale Ausgänge definiert. Das habe ich einfach so festgelegt, man kann auch mehr oder weniger digitale Ausgänge definieren. Für die Coils 0 und 1, verwende ich die GPIO-Pins GPIO25 und GPIO26.
// Coils,
// 00000
typedef struct
{
uint8_t coils_port0;
} coil_reg_params_t;
extern coil_reg_params_t coil_reg_params;
Diese Coils können wir ohne weitere Änderung direkt für unser Relaisboard verwenden. Aufgrund der 0-basierten Adressierung, wählen wir Coil 0 und Coil 1.
Die Ausgaben des ESP32 verknüpfen wir dann einfach in einer Task mit den Modbus-Coils. Wir verknüpfen vorerst auch nur die Coils, die tatsächlich verwendet werden.
void TaskReadWriteMBRegs(void *pvParameters)
{
// Bitmasken
uint8_t BIT_0 = 0x01;
uint8_t BIT_1 = 0x02;
uint8_t BIT_2 = 0x04;
uint8_t BIT_3 = 0x08;
while(1)
{
// Coils
// 00000
if(coil_reg_params.coils_port0 & BIT_0)
gpio_set_level(LED2, true);
else
gpio_set_level(LED2, false);
if(coil_reg_params.coils_port0 & BIT_1)
gpio_set_level(LED3, true);
else
gpio_set_level(LED3, false);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
Funktionstest
Die Funktion des Relaismoduls lässt sich mit Simply-Modbus-Master vorab testen. An den GPIO-Pins 25 und 26, habe ich noch LEDs angeschlossen. So kann man das invertierte Verhalten des Relaisboards gut erkennen.
Die 0-basierte Adressierung kann man gut in Simply-Modbus daran erkennen, dass vom ersten Coil 1 ein Offset von 1 substrahiert wird.
Auslesen eines DHT22 Sensors mit dem ESP32-Modbus-Slave
In den folgenden Abbildungen sehen wir die beiden Temperatur- und Luftfeuchtigkeitssensensoren DHT22. Einmal den Modbus-DHT22 in Abb. 4 und den TTL-DHT22 in Abb. 3.
Hier übernehme ich nicht die gleichen parameter des Sensors, wie in ESP32 Modbus Grundlagen – Praxis mit Relais und Sensor beschrieben, da die Register jetzt 0-basiert sind.
Register:
- 2 Input-Register
- Adresse 0x00: Temperatur, 2 Bytes, Skalierung/10 (Wert im Register/10 = Temperatur)
- Adresse 0x01: Luftfeuchtigkeit, 2 Bytes, Skalierung/10 (Wert im Register/10 = Luftfeuchtigkeit)
- Weitere Register für Abgleich und Konfiguration
- Adressierung: 0-Basiert (Deswegen Offset 1 in Simply Modbus bzw. 30001)
Integration des DHT22 in den ESP32-Slave
Beim DHT22-Sensor gehen wir ähnlich vor, wie beim TTL-Relaismodul. Der Unterschied besteht darin, dass wir nicht auf Coils schreiben, sondern die Messwerte des Sensors in Modbus-Input-Register übertragen.
Der DHT22 liefert nur alle zwei Sekunden neue Messwerte. Deshalb kann er nicht wie ein Relais ereignisgesteuert behandelt werden. In diesem Abschnitt zeige ich, wie der Sensor mit einer eigenen FreeRTOS-Task in den Modbus-Slave integriert wird.
Dafür verwenden wir eine eigene FreeRTOS-Task void TaskReadDHT22(void *pvParameters), welche den Sensor zyklisch ausliest. Der DHT22 darf nur in Abständen von mindestens etwa 2 Sekunden ausgelesen werden. Wird der Sensor früher erneut abgefragt, liefert er keine gültigen Messdaten.
Die ermittelten Temperatur- und Luftfeuchtigkeitswerte werden anschließend in die Input-Register des ESP32-Slaves geschrieben und können dort vom Modbus-Master ausgelesen werden.
Die Funktion zum Auslesen des DHT22 wurde in die dht.c und dht.h ausgelagert und gibt jetzt einen struct mit den Sensordaten Temperatur und Luftfeuchtigkeit, sowie einen Status zurück.
Der vollständige Code zum Auslesen des DHT22 wurde bereits im Artikel DHT22-Sensor mit ESP32 und ESP-IDF auslesen – ohne Bibliothek, ausführlich besprochen. Daher werden hier nur die für dieses Projekt relevanten Codeausschnitte gezeigt.
/* dht.h */
#ifndef DHT_H_
#define DHT_H_
typedef enum
{
DHT_OK,
DHT_ERROR_TIMEOUT_LOW,
DHT_ERROR_TIMEOUT_HIGH,
DHT_ERROR_CHECKSUM
} DhtStatus;
struct DhtData{
uint16_t temperature;
uint16_t humidity;
DhtStatus status;
};
bool wait_for_level(int level, uint32_t timeout_us);
struct DhtData DhtReadSensor(void);
#endif // DHT_H_
/* dht.c */
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "dht.h"
#define DTH22Data 4 // Datenpin DHT
#define HIGH 1
#define LOW 0
static const char *TAG = "DHT22";
// Wartet, bis der Pegel "level" erreicht ist.
bool wait_for_level(int level, uint32_t timeout_us)
{
...
}
struct DhtData DhtReadSensor()
{
struct DhtData dhtData{
.temperature = 0.0f,
.humidity = 0.0f,
.status = DHT_OK
};
gpio_set_pull_mode(DTH22Data, GPIO_PULLUP_ONLY);
while(1)
{
...
return dhtData;
}
}
In der main.c wird die Funktion DhtReadSensor() schließlich zyklisch mit etwas Puffer, also alle 2,5 Sekunden aufgerufen.
void DHT22Task(void *pvParameters)
{
while(1)
{
struct DhtData dhtData = DhtReadSensor();
// Input Register
// 30000
input_reg_params.input_data0 = dhtData.temperature;
input_reg_params.input_data1 = dhtData.humidity;
vTaskDelay(2500 / portTICK_PERIOD_MS);
}
}
So werden die Daten in den Input-Registern, alle 2,5 Sekunden aktualisiert und können jeder Zeit vom Master abgerufen werden.
Test des ESP32-Modbus-Slaves mit Simply Modbus
Nachdem wir das TTL-Relaismodul und den DHT22-Sensor in den ESP32-Modbus-Slave integriert haben, steht nun ein vollständiger Modbus-Slave mit der Slave-ID 1 für die weiteren Tests zur Verfügung, der jederzeit um weitere nicht modbusfähige Peripherie erweitert werden kann.
Die Modbus-Parameter des ESP32-Slave sind nach Bedarf frei konfigurierbar. Da wir hier aber den gegeben Parametern aus vorgeheneden Kapitel nachkommen, haben wir folgende Parameter:
ESP32-Slave Modbus-Parameter:
- Slave-Adresse: 1
- 2 Input-Register (0-basiert)
- Adresse 0x00: Temperatur, 2 Bytes, Skalierung/10 (Wert im Register/10 = Temperatur)
- Adresse 0x01: Luftfeuchtigkeit, 2 Bytes, Skalierung/10 (Wert im Register/10 = Luftfeuchtigkeit)
- 2 Coils (0-basiert)
- Adresse 0x00: Relais 1
- Adresse 0x01: Relais 2
- Baud Rate: 19200 (8 Datenbits, 1 Stopbit, keine Parität)
In dem folgenden Video wird gezeigt, wie sich das Relais über einen Modbus-Master schalten lässt und gleichzeitig der Sensor zyklisch ausgelesen wird.
Am Anfang lasse ich den Sensor bewusst im 2 Sekunden Takt vom Modbus-Master auslesen, um zu zeigen, wie sich das Senden und Empfangen von Daten immer wieder in die Quere kommen können, wenn man keine Maßnahmen trifft. Das zyklische Auslesen des Sensors blockiert im Moment des Zugriffs alle anderen Lese- oder Schreibzugriffe auf Coils und Register.
Für einen reibungslosen Ablauf setze ich das Auslesen des Sensors deswegen auf 10 Sekunden.
Mit einem geeigneten Modbus-Master können Lese- und Schreibzugriffe unmittelbar nacheinander ausgeführt werden, ohne dass sich diese gegenseitig blockieren. Während Simply-Modbus auf die Sensorabfrage wartet und dadurch keine weiteren Requests sendet, können andere Master die Requests unabhängig von der Sensorabfrage zeitnah nacheinander absetzen.
Das Problem liegt nicht im Modbus-Protokoll, sondern darin, dass Simply-Modbus während einer laufenden Sensorabfrage keine weiteren Requests absetzt.
Das stellt jedoch kein Problem dar, denn Simply-Modbus ist in erster Linie ein sehr gutes und bezahlbares Testwerkzeug für Modbus-Kommunikation. Für den späteren produktiven Einsatz kommt stattdessen unser selbst entwickelter Modbus-RTU-Master in C# zum Einsatz, der Lese- und Schreibzugriffe effizient verwaltet.
Abb. 5 zeigt die vom DHT22 gelieferten Messwerte in den Input-Registern von Simply-Modbus. Oben die Temperatur und dadrunter die Luftfeuchtigkeit. Da die Daten aus unsigned 16-Bit-Registern kommen und mit Nachkommastellen eingelesen werden, müssen die Registerwerte durch 10 dividiert werden, um die tatsächlichen Temperatur- und Luftfeuchtigkeitswerte zu erhalten.

Test des ESP32-Modbus-Slaves mit der C#-WPF-Anwendung
Eine kleine Änderung muss bei der C#-WPF-Anwendung aus C# WPF-Master und ESP32-Slave angepasst werden. Im Gegensatz zu der bisherigen Anwendung mit zwei Slaves, nämlich dem Modbus Relaismodul und dem Modbus DHT22, haben wir jetzt nur noch einen Slave mit der Slave ID 1, nämlich den ESP32.
Desweiteren definiere ich die Adressen alle 0-basiert. Der aktuelle Stand ist wie folgt:
- Es gibt nur noch einen Modbus-Slave, den ESP32, mit Slave-ID 1.
- Die Registeradressen sind jetzt durchgängig 0-basiert.
- Die alte Sonderbehandlung „Sensor 1-basiert / Relais 0-basiert“ ist weg.
- In der WPF gibt es nur noch die Klasse ESP32Slave.
- Die Klassen für SensorTempHum und Relaisboard sind entfernt.
- Die WPF ist damit nicht mehr für mehrere unterschiedliche Modbus-Geräte aufgebaut, sondern gezielt für deinen eigenen ESP32-Slave.
Weiterführende Links
- Installation ESP-IDF unter Windows 10/11
- Erstes Programmbeispiel, die blinkende LED
- Visual Studio Code und ESP-IDF Extension
- ESP32 – Das Entwicklungsboard
- ESP32 JTAG-Debugger
- FreeRTOS am ESP32 – Grundlagen, Tasks und Scheduling
- ESP-IDF Menuconfig – Überblick und wichtigste Optionen



