In diesem Beitrag zeige ich, wie ein Arduino als I2C‑Master arbeitet und gleichzeitig als Modbus‑TCP‑Server fungiert. Damit lassen sich Daten von I2C‑Modulen einfach auslesen und über das Netzwerk an eine SPS oder andere Steuerungen weitergeben.
Ein praktisches Beispielprogramm zeigt, wie die Messwerte übertragen werden, sodass du direkt loslegen kannst – ideal für Projekte, bei denen I2C‑Sensoren in industrielle Netzwerke integriert werden sollen.
Hardware
Für diese Applikation wird das I2C-Master Modul verwendet.
– Frei programmierbar mit Arduino-IDE
– Status-LED für SDA, SCL und INT
– Jumper zum aktivieren der Terminierung
– Blaue LED frei programmierbar. Z.B. für Status der LAN-Verbindung
– Steckplatz für WizNET W5500 LAN-Modul
– BUS-Stecker zum Anschluss weiterer I2C-Module
– Steckplatz für USB-RS232 Programmieradapter
Aufbau
Für unser Beispiel ist als Kopf der Arduino-I2C-Master mit gestecktem WizNET W5500 LAN-Modul. Daneben folgende Module
– Digitale I2C-Eingangskarte I2HE (Slave-Adresse 112)
– Digitale I2C-Ausgangskarte I2HA (Slave-Adresse 64)
– Analoge I2C-Eingangskarte I2HAE (Slave Adresse 16)
– Analoge I2C-Ausgangskarte I2HAA (Slave Adresse 176)
Software Modbus-TCP-Server
Die Modbus-TCP-Bibliothek für den Arduino kommt von André Sarmento Barbosa
http://github.com/andresarmento/modbus-arduino
Ich habe in paar verschiedene Bibliotheken ausprobiert und diese hat auf Anhieb super funktioniert.
Diese Bibliotheken werden benötigt:
#include <Wire.h> // I2C-Lib #include <avr/wdt.h> // Watchdog #include <Modbus.h> // ArduinoRS485 library #include <ModbusIP.h> // Modbus
//ModbusIP object
ModbusIP mb;
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte ip[] = { 192, 168, 1, 7};
Dann definieren wir die Variablen für die verwendeten Raspberry-SPS-Baugruppen.
Sollen mehrere Eingangskarten verwendet werden, einfach die drei Zeilen eines Blocks kopieren und aus der 1 eine 2 machen u.s.w.
// I2C-Baugruppen definieren und globale Variablen anlegen #define I2E1_ADDR 112 >> 1 // I2C-Digital INPUT Addresse als 7 Bit int I2E1_WERT=0; bool I2E1_OK; #define I2A1_ADDR 64 >> 1 // I2C-Digital OUTPUT Addresse als 7 Bit int I2A1_WERT=0; bool I2A1_OK; #define I2AE1_ADDR 16 >> 1 // I2C-Analog INPUT Addresse als 7 Bit int I2AE1_WERT[5]; bool I2AE1_OK; #define I2AA1_ADDR 176 >> 1 // I2C-Analog OUTPUT Addresse als 7 Bit int I2AA1_WERT[4]; bool I2AA1_OK;
Im Bereich Setup werden die Pin-Modes und die serielle Schnittstelle eingestellt.
Weiter unten kommt der Start und die Konfiguration vom Modbus-Server.
Die input-Register können von der SPS nur gelesen werden. In die Holding-Register kann die SPS Werte schreiben, die dann vom Modbus-TCP-Server gelesen und verarbeitet werden können.
// Modbus-TCP Server starten // ------------------------- Serial.println("Modbus TCP Server"); mb.config(mac, ip); // Input-Register anlegen (Server -> SPS) mb.addIreg(0); // Input-Register 0 = 30001 in der SPS mb.addIreg(1); // Input-Register 1 = 30002 in der SPS mb.addIreg(2); // Input-Register 2 = 30003 in der SPS mb.addIreg(3); // Input-Register 3 = 30004 in der SPS mb.addIreg(4); // Input-Register 4 = 30005 in der SPS mb.addIreg(5); // Input-Register 5 = 30006 in der SPS // Holding Register anlegen (SPS -> Server) mb.addHreg(0); // Holding-Register 0 = 40001 in der SPS mb.addHreg(1); // Holding-Register 1 = 40002 in der SPS mb.addHreg(2); // Holding-Register 2 = 40003 in der SPS mb.addHreg(3); // Holding-Register 3 = 40004 in der SPS mb.addHreg(4); // Holding-Register 4 = 40005 in der SPS
Im Loop-Bereich wird als erstes die Eingangskarte über den I2C-Bus eingelesen. Die Funktion I2E ist im Arduino-Programm ganz unten im Bereich „Globale Funktionen“ programmiert. Die Funktion kann für mehrere Karten immer wieder aufgerufen werden.
Der eingelesene Wert wird dann in das Modbus-Register o eingetragen. Der Wert erscheint dann in der SPS im Register 30001
// Einlesen der Bits von der I2C-INPUT Karte // ------------------------------------------ // 8-Bit von der Eingangkarte lesen I2E(I2E1_ADDR, I2E1_WERT, I2E1_OK); // Ein Byte von der DI-Karte Adr. 112 lesen if (I2E1_OK == 0) { Serial.print("Keine Antwort vom Slave "); Serial.println (I2E1_ADDR); } // Wert zur SPS mb.Ireg(0, I2E1_WERT) ; // Input-Register 0 = 30001 in der SPS
Der nächste Programm-Block schreibt den Wert aus dem Holding-Register 40001 zur digitalen Ausgangskarte
// Ausgeben der Bits an die I2C-OUTPUT Karte // ------------------------------------------ // Wert von der SPS umrangieren I2A1_WERT = mb.Hreg(0); // Holding-Register 0 = 40001 in der SPS // 8-Bit zur Ausgangskarte schicken I2A(I2A1_ADDR, I2A1_WERT, I2A1_OK);
Anschließend werden die analogen Eingänge mit der Funktion
I2AE(I2AE1_ADDR, I2AE1_WERT, I2AE1_OK);
eingelesen und in die Input-Register 1-5 einsortiert.
Die erscheinen dann in der SPS in den Registern 30002 bis 30006)
// Analogwerte einlesen und berechnen // ---------------------------------- // Werte von Analogkarte lesen I2AE(I2AE1_ADDR, I2AE1_WERT, I2AE1_OK); // Werte zur SPS mb.Ireg(1, I2AE1_WERT[0]); // Input-Register 1 = 30002 in der SPS mb.Ireg(2, I2AE1_WERT[1]); // Input-Register 2 = 30003 in der SPS mb.Ireg(3, I2AE1_WERT[2]); // Input-Register 3 = 30004 in der SPS mb.Ireg(4, I2AE1_WERT[3]); // Input-Register 3 = 30005 in der SPS mb.Ireg(5, I2AE1_WERT[4]); // Input-Register 3 = 30006 in der SPS
Die Analogen Ausgänge werden mit der Funktion
I2AA(I2AA1_ADDR, I2AA1_WERT, I2AA1_OK);
zur Analogkarte gesendet. Vorhermüssen aber die Werte von den Holding-Registern 4002 bis 4005 umrangiert werden
// Analogwerte ausgeben // -------------------- // Wert von der SPS I2AA1_WERT[0] = mb.Hreg(1); // Holding-Register 1 = 40002 in der SPS I2AA1_WERT[1] = mb.Hreg(2); // Holding-Register 2 = 40003 in der SPS I2AA1_WERT[2] = mb.Hreg(3); // Holding-Register 3 = 40004 in der SPS I2AA1_WERT[3] = mb.Hreg(4); // Holding-Register 4 = 40005 in der SPS // Werte zur Analogkarte schicken I2AA(I2AA1_ADDR, I2AA1_WERT, I2AA1_OK);
Software SIEMENS TIA-Portal
Als SPS habe ich eine SIEMENS SIMATIC S7-1511 CPU auf meinem Teststand.
Die soll jetzt als Modbus-TCP-Client die Werte vom Server lesen und schreiben.
Eine Ausführliche Beschreibung der Modbus-Kommunikation gibt es als PDF-Dokument auf der SIEMENS Seite support.industry.siemens.com
Modbus/TCP mit den Anweisungen MB_CLIENT und …
Im Beispiel verwende ich drei Datenbausteine
DB3 = DataWrite
Darin ist ein Array mit 5 WORD Variablen für das Holding-Register[0] .. [4] angelegt.
DB3 = DataRead
Darin ist ein Array mit 6 WORD Variablen für die Reading-Register[0] .. [5] angelegt.
DB100 = ModbusData
Hier sind die Verbindungsdaten und ein paar Variablen für den Status vom Funktionsbaustein MB_CLIENT definiert
es müssen zwei Strukturen mit dem UDT TCON_IP_V4 angelegt werden. Eine Struktur zum Lesen, eine Struktur zum Schreiben
InterfaceID ist die Hardware-Kennung der lokalen Schnittstelle.
Die Hardware-Kennung finden Sie in der Gerätekonfiguration der CPU. Markieren Sie die PROFINET-Schnittstelle – Eigenschaften – Systemkonstanten „Local_Profinet-Schnittstelle_1
ID: Über den Parameter wird eine Verbindung innerhalb der CPU eindeutig identifiziert. Jede einzelne Instanz der Anweisung „MB_CLIENT“ sowie „MB_SERVER“ muss eine eindeutige ID verwenden.
ConnectionType Verbindungstyp Wählen Sie 11 (dezimal) für TCP. Andere Verbindungstypen sind nicht zulässig.
ActiveEstablished Kennung für die Art des Verbindungsaufbaus. True: aktiver Verbindungsaufbau False: passiver Verbindungsaufbau
RemoteAddress IP-Adresse des entfernten Verbindungspartners
RemotePort Verwenden Sie die IP-Port-Nummer des Servers, zu dem der Client die Verbindung herstellt und über das TCP/IP-Protokoll kommuniziert
LocalPort Port-Nummer des lokalen Verbindungspartners 0= Beliebiger Port
FB1 ModbusTCP
// Input-Register vom Server lesen #MB_READ(REQ := TRUE, DISCONNECT :=FALSE, // Verbindung halten MB_MODE :=0, // 0=Lesen MB_DATA_ADDR := 30001, // Start bei Register 30001 MB_DATA_LEN :=6, // 6 Register auslesen DONE => "ModbusData".clientData.done_read, BUSY => "ModbusData".clientData.busy_read, ERROR => "ModbusData".clientData.error_read, STATUS => "ModbusData".clientData.status_read, MB_DATA_PTR := "Date_Read".InputRegister, CONNECT := "ModbusData".connectParamClient_read); // Holding-Register zum Server schreiben #MB_WRITE(:= TRUE, DISCONNECT:=FALSE, // Verbindung halten MB_MODE:=1, // 1=Schreiben MB_DATA_ADDR:=40001, // Start bei Register 40001 MB_DATA_LEN:=5, // 5 Register auslesen DONE=>"ModbusData".clientData.done_write, BUSY=>"ModbusData".clientData.busy_write, ERROR=>"ModbusData".clientData.error_write, STATUS=>"ModbusData".clientData.status_write, MB_DATA_PTR:="Data_Write".holdingRegister, CONNECT:="ModbusData".connectParamClient_write);
| Hier kann eine TIA-V17 Bibliothek mit den Bausteinen aus der obigen Beschreibung heruntergeladen werden Modbus-TCP TIA-V17 Library (1074 Downloads ) |
Und hier das komplette Arduino-Programm
/*
==============================================
Test I2C-Modbus TCP Server
==============================================
Board Arduino UNO
Version 1.0
Quelle:
Modbus-Arduino Example (Modbus IP)
Copyright by André Sarmento Barbosa
http://github.com/andresarmento/modbus-arduino
==============================================
*/
String Version = "V1.0"; // Version
#include <Wire.h> // I2C-Lib
#include <avr/wdt.h> // Watchdog
#include <Modbus.h> // ArduinoRS485 library
#include <ModbusIP.h> // Modbus
//ModbusIP object
ModbusIP mb;
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte ip[] = { 192, 168, 1, 7};
// Pins definieren
int I2C_SCL = 19; // PC5 = I2C-Bus SCL
int I2C_SDA = 18; // PC4 = I2C-Bus SDA
int I2C_INT = 17; // PC3 = I2C-Bus INT
int LED_BL = 8; // PB0 = LED Blau
// I2C-Baugruppen definieren und globale Variablen anlegen
#define I2E1_ADDR 112 >> 1 // I2C-Digital INPUT Addresse als 7 Bit
int I2E1_WERT=0;
bool I2E1_OK;
#define I2A1_ADDR 64 >> 1 // I2C-Digital OUTPUT Addresse als 7 Bit
int I2A1_WERT=0;
bool I2A1_OK;
#define I2AE1_ADDR 16 >> 1 // I2C-Analog INPUT Addresse als 7 Bit
int I2AE1_WERT[5];
bool I2AE1_OK;
#define I2AA1_ADDR 176 >> 1 // I2C-Analog OUTPUT Addresse als 7 Bit
int I2AA1_WERT[4];
bool I2AA1_OK;
// Variablen deklarieren
byte ALTWERT;
long ts;
void setup() {
// Pin-Modes einstellen
// --------------------
pinMode(I2C_INT, INPUT);
pinMode(LED_BL, OUTPUT);
Serial.begin(9600); // Serielle Schnittstelle konfigurieren
Wire.begin(); // I2C-Pins definieren
Serial.println("Version " + Version); // Version ausgeben
// setzten aller Bits der Eingabekarte auf 1
// -----------------------------------------
Wire.beginTransmission(I2E1_ADDR); // Start Übertragung zum PCF8574
Wire.write(0xFF); // Alle Bits sind Eingänge
Wire.endTransmission(); // Ende
// Blaue LED abschalten
// --------------------
digitalWrite(LED_BL, LOW); // Blaue LED AUS
// Modbus-TCP Server starten
// -------------------------
Serial.println("Modbus TCP Server");
mb.config(mac, ip);
// Input-Register anlegen (Server -> SPS)
mb.addIreg(0); // Input-Register 0 = 30001 in der SPS
mb.addIreg(1); // Input-Register 1 = 30002 in der SPS
mb.addIreg(2); // Input-Register 2 = 30003 in der SPS
mb.addIreg(3); // Input-Register 3 = 30004 in der SPS
mb.addIreg(4); // Input-Register 4 = 30005 in der SPS
mb.addIreg(5); // Input-Register 5 = 30006 in der SPS
// Holding Register anlegen (SPS -> Server)
mb.addHreg(0); // Holding-Register 0 = 40001 in der SPS
mb.addHreg(1); // Holding-Register 1 = 40002 in der SPS
mb.addHreg(2); // Holding-Register 2 = 40003 in der SPS
mb.addHreg(3); // Holding-Register 3 = 40004 in der SPS
mb.addHreg(4); // Holding-Register 4 = 40005 in der SPS
}
void loop() {
//Call once inside loop() - all magic here
mb.task(); // Modbus-Task
// Einlesen der Bits von der I2C-INPUT Karte
// ------------------------------------------
// 8-Bit von der Eingangkarte lesen
I2E(I2E1_ADDR, I2E1_WERT, I2E1_OK); // Ein Byte von der DI-Karte Adresse 112 lesen
if (I2E1_OK == 0) {
Serial.print("Keine Antwort vom Slave ");
Serial.println (I2E1_ADDR);
}
// Wert zur SPS
mb.Ireg(0, I2E1_WERT) ; // Input-Register 0 = 30001 in der SPS
// Ausgeben der Bits an die I2C-OUTPUT Karte
// ------------------------------------------
// Wert von der SPS umrangieren
I2A1_WERT = mb.Hreg(0); // Holding-Register 0 = 40001 in der SPS
// 8-Bit zur Ausgangskarte schicken
I2A(I2A1_ADDR, I2A1_WERT, I2A1_OK);
// Analogwerte einlesen und berechnen
// ----------------------------------
// Werte von Analogkarte lesen
I2AE(I2AE1_ADDR, I2AE1_WERT, I2AE1_OK);
// Werte zur SPS
mb.Ireg(1, I2AE1_WERT[0]); // Input-Register 1 = 30002 in der SPS
mb.Ireg(2, I2AE1_WERT[1]); // Input-Register 2 = 30003 in der SPS
mb.Ireg(3, I2AE1_WERT[2]); // Input-Register 3 = 30004 in der SPS
mb.Ireg(4, I2AE1_WERT[3]); // Input-Register 3 = 30005 in der SPS
mb.Ireg(5, I2AE1_WERT[4]); // Input-Register 3 = 30006 in der SPS
// Analogwerte ausgeben
// --------------------
// Wert von der SPS
I2AA1_WERT[0] = mb.Hreg(1); // Holding-Register 1 = 40002 in der SPS
I2AA1_WERT[1] = mb.Hreg(2); // Holding-Register 2 = 40003 in der SPS
I2AA1_WERT[2] = mb.Hreg(3); // Holding-Register 3 = 40004 in der SPS
I2AA1_WERT[3] = mb.Hreg(4); // Holding-Register 4 = 40005 in der SPS
// Werte zur Analogkarte schicken
I2AA(I2AA1_ADDR, I2AA1_WERT, I2AA1_OK);
// Link-Status auf blaue LED
// -------------------------
bool takt_1hz = (millis() / 500) & 1; // Blinktakt 1 Hz
auto link = Ethernet.linkStatus();
switch (link) {
case Unknown:
digitalWrite(LED_BL, takt_1hz); break;
case LinkON:
digitalWrite(LED_BL, HIGH); break;
case LinkOFF:
digitalWrite(LED_BL, LOW); break;
}
//alle 5 Sekunden lesen für Test
if (millis() > ts + 1000) {
ts = millis();
// Werte von der SPS
Serial.print(mb.Hreg(0)); // Holding-Register 0 = 40001 in der SPS
Serial.print("\t ");
Serial.print(mb.Hreg(1)); // Holding-Register 1 = 40002 in der SPS
Serial.print("\t ");
Serial.print(mb.Hreg(2)); // Holding-Register 2 = 40003 in der SPS
Serial.print("\t ");
Serial.print(mb.Hreg(3)); // Holding-Register 3 = 40004 in der SPS
Serial.print("\t ");
Serial.print(mb.Hreg(4)); // Holding-Register 4 = 40005 in der SPS
Serial.println ();
}
}
// =============================================================================================================
// Globale Funktionen
// =============================================================================================================
// ---------------------------------------
// 8-Bit von digitale Eingangskarte lesen
// ---------------------------------------
void I2E(int I2C_Adresse, int &Wert, bool &Slave_OK) {
Wire.requestFrom(I2C_Adresse, 1); // Anfrage an den I2C-Slave
if (Wire.available()) {
Wert = 255 - Wire.read(); // Ein Byte vom PCF8574 lesen und in invertierte Eingabe wandlen
Slave_OK = 1;
} else {Slave_OK = 0;}
}
// ------------------------------------------
// 8-Bit zur digitale Ausgangskarte schreiben
// ------------------------------------------
void I2A(int I2C_Adresse, int &Wert, bool &Slave_OK) {
Wire.beginTransmission(I2C_Adresse); // Start Übertragung zum PCF8574
Wire.write(255 - Wert); // Wert schreiben
byte error = Wire.endTransmission();
if (error == 0) { Slave_OK = 1; } else {Slave_OK = 0;} // Fehlerauswertung
}
// ---------------------------------------
// 5 Analogwerte von der I2HAE-Karte lesen
// ---------------------------------------
void I2AE(int I2C_Adresse, int (&Wert)[5], bool &Slave_OK) {
int Array[11]; // Lokales Array zur Speicherung der gelesenen Daten
Wire.requestFrom(I2C_Adresse, 11); // Anfrage an den I2C-Slave
if (Wire.available()) { // 11 Bytes von Analogkarte lesen und in das array kopieren
Slave_OK = 1;
for(int i=0;i<11;i++){
int c = Wire.read();
Array[i]=c;
}
// Werte berechnen. Analogwert = Highbyte * 256 + Lowbyte
Wert[0] = Array[ 2] * 256 + Array[1];
Wert[1] = Array[ 4] * 256 + Array[3];
Wert[2] = Array[ 6] * 256 + Array[5];
Wert[3] = Array[ 8] * 256 + Array[7];
Wert[4] = Array[10] * 256 + Array[9];
} else {Slave_OK = 0;}
}
// ---------------------------------------
// 4 Analogwerte zur I2HAA-Karte schreiben
// ---------------------------------------
void I2AA(int I2C_Adresse, int (&Wert)[4], bool &Slave_OK) {
Wire.beginTransmission(I2C_Adresse); // Start Übertragung zur ANALOG-OUT Karte
Wire.write(0); // Kanal auf 0 setzen
for(int i=0;i<4;i++){
byte HBy = Wert[i] / 256; // HIGH-Byte berechnen
Wire.write(Wert[i] - HBy * 256); // LOW-Byte schreiben
Wire.write(HBy); // HIGH-Byte schreiben
}
byte error = Wire.endTransmission();
if (error == 0) { Slave_OK = 1; } else {Slave_OK = 0;} // Fehlerauswertung
}






