Communication
PLC Network Communication: Modbus TCP & EtherNet/IP in Structured Text
Connect PLCs to sensors, drives, and SCADA systems over industrial Ethernet — with production-grade Modbus TCP and EtherNet/IP implementations in Structured Text.
Industrial Ethernet: The Backbone of Modern Automation
Gone are the days when every field device needed dedicated point-to-point wiring. Modern plants run on industrial Ethernet — the same physical layer as office IT, but with deterministic protocols designed for real-time control. The two dominant protocols are:
This article covers both protocols from the PLC programmer's perspective: how to structure your data, handle communication errors, and build robust multi-device architectures.
Modbus TCP Fundamentals
Modbus TCP wraps the classic Modbus RTU protocol in a TCP/IP packet. The data model is simple — four register types:
| Register Type | Modbus Address | Access | Data Size | Typical Use | | Coils | 0xxxx | Read/Write | 1 bit | Digital outputs | | Discrete Inputs | 1xxxx | Read Only | 1 bit | Digital inputs | | Input Registers | 3xxxx | Read Only | 16 bit | Analog inputs, status | | Holding Registers | 4xxxx | Read/Write | 16 bit | Setpoints, config |
Function Codes You'll Actually Use
| Code | Name | Purpose | | 01 | Read Coils | Read digital outputs | | 02 | Read Discrete Inputs | Read digital inputs | | 03 | Read Holding Registers | Read 16-bit R/W registers | | 04 | Read Input Registers | Read 16-bit read-only registers | | 05 | Write Single Coil | Write one digital output | | 06 | Write Single Register | Write one 16-bit register | | 15 | Write Multiple Coils | Write multiple digital outputs | | 16 | Write Multiple Registers | Write multiple 16-bit registers |
Modbus TCP Data Mapping in Structured Text
The key challenge is mapping PLC data types to 16-bit Modbus registers. Here's a complete mapping strategy:
TYPE ModbusDeviceConfig :
STRUCT
IPAddress : STRING[15];
Port : UINT := 502; // Standard Modbus TCP port
UnitID : BYTE := 1; // Modbus slave ID
TimeoutMS : UINT := 1000; // Response timeout
RetryCount : UINT := 3;
PollIntervalMS : UINT := 100; // Polling cycle time
END_STRUCT;
END_TYPETYPE ModbusRegisterMap :
STRUCT
// Input registers (read from device)
IR_Temperature : INT; // Register 30001 — scaled ×10
IR_Pressure : INT; // Register 30002 — scaled ×100
IR_FlowRate : INT; // Register 30003
IR_StatusWord : WORD; // Register 30004
// Holding registers (read/write)
HR_TempSetpoint : INT; // Register 40001
HR_SpeedRef : INT; // Register 40002
HR_ControlWord : WORD; // Register 40003
HR_AlarmReset : BOOL; // Register 40004, bit 0
END_STRUCT;
END_TYPE
Handling 32-Bit Values Across Two Registers
REAL (floating point) and DINT (32-bit integer) values span two consecutive 16-bit registers. The byte order matters — and it varies by device manufacturer:
FUNCTION FC_RegsToReal : REAL
VAR_INPUT
HighWord : WORD; // Register N
LowWord : WORD; // Register N+1
BigEndian: BOOL; // TRUE for Modbus standard (big-endian)
END_VAR
VAR
RawDWord : DWORD;
TempReal : REAL;
END_VARIF BigEndian THEN
// Big-endian (AB CD): High word first — Modbus standard
RawDWord := SHL(WORD_TO_DWORD(HighWord), 16) OR WORD_TO_DWORD(LowWord);
ELSE
// Little-endian (CD AB): Low word first — some devices swap
RawDWord := SHL(WORD_TO_DWORD(LowWord), 16) OR WORD_TO_DWORD(HighWord);
END_IF;
// Type-pun DWORD to REAL (platform-specific, may need MEMCPY)
TempReal := DWORD_TO_REAL(RawDWord);
FC_RegsToReal := TempReal;
FUNCTION FC_RealToRegs : BOOL
VAR_INPUT
Value : REAL;
BigEndian: BOOL;
END_VAR
VAR_IN_OUT
HighWord : WORD;
LowWord : WORD;
END_VAR
VAR
RawDWord : DWORD;
END_VARRawDWord := REAL_TO_DWORD(Value);
IF BigEndian THEN
HighWord := DWORD_TO_WORD(SHR(RawDWord, 16));
LowWord := DWORD_TO_WORD(RawDWord AND 16#0000FFFF);
ELSE
LowWord := DWORD_TO_WORD(SHR(RawDWord, 16));
HighWord := DWORD_TO_WORD(RawDWord AND 16#0000FFFF);
END_IF;
FC_RealToRegs := TRUE;
Production tip: Always document your byte order convention in the project. Nothing wastes more commissioning time than swapped registers showing garbage values.
Modbus TCP Communication Manager
A production system polls multiple devices sequentially. Here's a robust polling architecture:
FUNCTION_BLOCK FB_ModbusTCPManager
VAR_INPUT
Enable : BOOL;
DeviceConfig : ModbusDeviceConfig;
END_VAR
VAR_OUTPUT
Connected : BOOL;
CommOK : BOOL;
CommError : BOOL;
ErrorCode : WORD;
ResponseTimeMS : UINT;
FailedPolls : DINT;
TotalPolls : DINT;
CommQuality : REAL; // 0–100%
END_VAR
VAR
State : INT := 0;
RetryCounter : UINT := 0;
tmrTimeout : TON;
tmrPollCycle : TON;
tmrReconnect : TON; // Comm health tracking
SuccessWindow : ARRAY[1..100] OF BOOL;
WindowIndex : INT := 1;
SuccessCount : INT := 0;
END_VAR
IF NOT Enable THEN
State := 0;
Connected := FALSE;
CommOK := FALSE;
RETURN;
END_IF;
CASE State OF
0: // CONNECT
// Platform-specific: open TCP socket to DeviceConfig.IPAddress:502
// Most PLCs have built-in Modbus TCP client FBs
State := 1;
1: // WAIT FOR CONNECTION
tmrTimeout(IN := TRUE, PT := UINT_TO_TIME(DeviceConfig.TimeoutMS));
IF Connected THEN
tmrTimeout(IN := FALSE);
State := 2;
ELSIF tmrTimeout.Q THEN
tmrTimeout(IN := FALSE);
State := 5; // Connection failed
END_IF;
2: // POLL — READ INPUT REGISTERS
// FC03: Read Holding Registers or FC04: Read Input Registers
// Start address, quantity, destination buffer
TotalPolls := TotalPolls + 1;
tmrTimeout(IN := TRUE, PT := UINT_TO_TIME(DeviceConfig.TimeoutMS));
State := 3;
3: // WAIT FOR RESPONSE
IF ( response received ) TRUE THEN
tmrTimeout(IN := FALSE);
CommOK := TRUE;
CommError := FALSE;
RetryCounter := 0;
// Track success
SuccessWindow[WindowIndex] := TRUE;
WindowIndex := WindowIndex + 1;
IF WindowIndex > 100 THEN WindowIndex := 1; END_IF;
State := 4;
ELSIF tmrTimeout.Q THEN
tmrTimeout(IN := FALSE);
FailedPolls := FailedPolls + 1;
SuccessWindow[WindowIndex] := FALSE;
WindowIndex := WindowIndex + 1;
IF WindowIndex > 100 THEN WindowIndex := 1; END_IF;
RetryCounter := RetryCounter + 1;
IF RetryCounter >= DeviceConfig.RetryCount THEN
CommError := TRUE;
CommOK := FALSE;
State := 5;
ELSE
State := 2; // Retry
END_IF;
END_IF;
4: // POLL INTERVAL — wait before next poll
tmrPollCycle(IN := TRUE, PT := UINT_TO_TIME(DeviceConfig.PollIntervalMS));
IF tmrPollCycle.Q THEN
tmrPollCycle(IN := FALSE);
State := 2;
END_IF;
5: // ERROR — attempt reconnection
Connected := FALSE;
tmrReconnect(IN := TRUE, PT := T#5S);
IF tmrReconnect.Q THEN
tmrReconnect(IN := FALSE);
RetryCounter := 0;
State := 0;
END_IF;
END_CASE;
// Calculate communication quality
SuccessCount := 0;
FOR WindowIndex := 1 TO 100 DO
IF SuccessWindow[WindowIndex] THEN
SuccessCount := SuccessCount + 1;
END_IF;
END_FOR;
CommQuality := INT_TO_REAL(SuccessCount);
Multi-Device Polling Architecture
Real systems talk to dozens of devices. A round-robin scheduler ensures fair polling:
PROGRAM ModbusMultiDevice
VAR
Devices : ARRAY[1..16] OF ModbusDeviceConfig;
DeviceData : ARRAY[1..16] OF ModbusRegisterMap;
DeviceComm : ARRAY[1..16] OF FB_ModbusTCPManager;
NumDevices : INT := 8; ActiveDevice : INT := 1;
PriorityDevice : INT := 0; // 0=none, >0=force poll this device next
// Aggregate status
AllDevicesOK : BOOL;
DevicesInError : INT;
END_VAR
// Priority polling: if a critical device needs attention, poll it first
IF PriorityDevice > 0 AND PriorityDevice <= NumDevices THEN
ActiveDevice := PriorityDevice;
PriorityDevice := 0;
END_IF;
// Poll active device
DeviceCommActiveDevice;
// When current poll completes, move to next device
IF DeviceComm[ActiveDevice].CommOK OR DeviceComm[ActiveDevice].CommError THEN
ActiveDevice := ActiveDevice + 1;
IF ActiveDevice > NumDevices THEN
ActiveDevice := 1;
END_IF;
END_IF;
// Aggregate health
DevicesInError := 0;
AllDevicesOK := TRUE;
FOR ActiveDevice := 1 TO NumDevices DO
IF DeviceComm[ActiveDevice].CommError THEN
DevicesInError := DevicesInError + 1;
AllDevicesOK := FALSE;
END_IF;
END_FOR;
EtherNet/IP: CIP Messaging
EtherNet/IP uses the Common Industrial Protocol (CIP) — a more structured approach than Modbus. Two communication types:
| Type | Name | Use Case | | Implicit (I/O) | Cyclic data exchange | Fast, deterministic I/O updates | | Explicit | Request/response messaging | Configuration, diagnostics, non-cyclic data |
Implicit I/O: Assembly Mapping
Implicit messaging maps device data directly to PLC I/O tags — configured in the controller's hardware tree, not in ST code. The PLC sees the data as input/output arrays:
// EtherNet/IP implicit I/O — data appears as mapped I/O
PROGRAM EIP_ImplicitIO
VAR
// These tags are mapped to EtherNet/IP adapter assemblies
// Configuration is done in the hardware config, not code
VFD_Input : ARRAY[0..3] OF DINT; // Input assembly from drive
VFD_Output : ARRAY[0..3] OF DINT; // Output assembly to drive // Decoded values
DriveStatus : WORD;
ActualSpeed : REAL;
ActualCurrent : REAL;
// Commands
DriveControl : WORD;
SpeedReference : REAL;
END_VAR
// Decode input assembly (layout is device-specific)
DriveStatus := DINT_TO_WORD(VFD_Input[0] AND 16#0000FFFF);
ActualSpeed := DINT_TO_REAL(VFD_Input[1]);
ActualCurrent := DINT_TO_REAL(VFD_Input[2]);
// Build output assembly
VFD_Output[0] := WORD_TO_DINT(DriveControl);
VFD_Output[1] := REAL_TO_DINT(SpeedReference);
Explicit Messaging: MSG Instruction Pattern
For non-cyclic data (reading a parameter, writing a configuration), use explicit CIP messages:
TYPE CIP_MessageConfig :
STRUCT
ServiceCode : BYTE; // 0x4C=Read, 0x4D=Write, 0x01=GetAttrAll
ClassID : UINT; // CIP object class
InstanceID : UINT; // Object instance
AttributeID : UINT; // Specific attribute (0 for service-level)
TargetIP : STRING[15];
TargetSlot : UINT; // For backplane routing
DataLength : UINT;
END_STRUCT;
END_TYPEFUNCTION_BLOCK FB_CIP_ExplicitMsg
VAR_INPUT
Execute : BOOL;
Config : CIP_MessageConfig;
END_VAR
VAR_OUTPUT
Done : BOOL;
Error : BOOL;
ErrorCode : DINT;
ResponseData : ARRAY[0..63] OF BYTE;
ResponseLength : UINT;
END_VAR
VAR
State : INT := 0;
tmrTimeout : TON;
END_VAR
CASE State OF
0: // IDLE
Done := FALSE;
Error := FALSE;
IF Execute THEN
// Build CIP request packet
// Route: port 2 (Ethernet), target IP, slot
// Service: Config.ServiceCode
// Path: Class/Instance/Attribute
State := 1;
END_IF;
1: // WAITING FOR RESPONSE
tmrTimeout(IN := TRUE, PT := T#5S);
IF ( response received ) FALSE THEN
tmrTimeout(IN := FALSE);
Done := TRUE;
State := 0;
ELSIF tmrTimeout.Q THEN
tmrTimeout(IN := FALSE);
Error := TRUE;
ErrorCode := 16#0001; // Timeout
State := 0;
END_IF;
END_CASE;
Reading a Drive Parameter via Explicit Message
// Example: Read motor nameplate current from a PowerFlex drive
// CIP Path: Class 0x93 (Drive Object), Instance 1, Attribute 47 (Nameplate Current)
PROGRAM ReadDriveParam
VAR
msgReadCurrent : FB_CIP_ExplicitMsg;
msgConfig : CIP_MessageConfig;
NameplateCurrent: REAL;
ReadDone : BOOL;
END_VARmsgConfig.ServiceCode := 16#0E; // Get Attribute Single
msgConfig.ClassID := 16#93; // Drive object
msgConfig.InstanceID := 1;
msgConfig.AttributeID := 47; // Motor rated current
msgConfig.TargetIP := '192.168.1.10';
msgReadCurrent(
Execute := NOT ReadDone,
Config := msgConfig
);
IF msgReadCurrent.Done THEN
// Parse response — REAL is 4 bytes
// NameplateCurrent := BytesToReal(msgReadCurrent.ResponseData);
ReadDone := TRUE;
END_IF;
Connection Diagnostics and Health Monitoring
Production systems need visibility into communication health:
TYPE NetworkDiagnostics :
STRUCT
DeviceName : STRING[20];
IPAddress : STRING[15];
Protocol : STRING[12]; // 'ModbusTCP' or 'EtherNetIP'
IsConnected : BOOL;
CommQuality : REAL; // 0–100%
AvgResponseMS : REAL;
MaxResponseMS : REAL;
TotalPackets : DINT;
LostPackets : DINT;
LastErrorCode : WORD;
LastErrorTime : STRING[20];
ConsecErrors : INT;
END_STRUCT;
END_TYPEFUNCTION_BLOCK FB_NetworkMonitor
VAR
Devices : ARRAY[1..32] OF NetworkDiagnostics;
NumDevices : INT;
NetworkHealthPct : REAL;
CriticalDevDown : BOOL;
END_VAR
VAR
i : INT;
HealthyCount : INT;
END_VAR
HealthyCount := 0;
CriticalDevDown := FALSE;
FOR i := 1 TO NumDevices DO
// Flag devices with degraded communication
IF Devices[i].CommQuality < 95.0 AND Devices[i].IsConnected THEN
// Log warning: device communication degraded
END_IF;
IF Devices[i].CommQuality < 50.0 THEN
// Device is effectively offline
Devices[i].IsConnected := FALSE;
END_IF;
IF Devices[i].IsConnected THEN
HealthyCount := HealthyCount + 1;
END_IF;
// Check for critical devices (first 4 are critical in this example)
IF i <= 4 AND NOT Devices[i].IsConnected THEN
CriticalDevDown := TRUE;
END_IF;
END_FOR;
IF NumDevices > 0 THEN
NetworkHealthPct := (INT_TO_REAL(HealthyCount) / INT_TO_REAL(NumDevices)) * 100.0;
END_IF;
Protocol Selection Guide
| Criterion | Modbus TCP | EtherNet/IP | | Complexity | Low — register reads/writes | Medium — CIP object model | | Speed | Good (10–50ms typical) | Better (1–10ms with implicit I/O) | | Determinism | Non-deterministic (TCP) | Implicit I/O can be deterministic | | Multivendor | Excellent — nearly universal | Good — ODVA members | | Data Model | Flat registers | Rich object model with attributes | | Cost | Free — no licensing | May require EDS files, conformance testing | | Best For | Sensors, meters, simple I/O | Drives, robots, complex devices |
Pragmatic advice: Use Modbus TCP for simple devices (power meters, temperature transmitters, basic I/O). Use EtherNet/IP for complex devices (VFDs, robots, safety controllers) where the richer data model pays off.
Security Considerations
Industrial Ethernet is increasingly targeted by cyber attacks. Basic security hygiene:
// Network segmentation validation
// Ensure control network devices are on the correct subnetFUNCTION FC_ValidateIPRange : BOOL
VAR_INPUT
DeviceIP : STRING[15];
SubnetBase : STRING[15]; // e.g., '192.168.10.'
END_VAR
// Simple prefix check — production systems use proper subnet masking
FC_ValidateIPRange := (FIND(DeviceIP, SubnetBase) = 1);
Summary
| Topic | Key Takeaway | | Modbus TCP | Simple register model — 4 register types, 8 function codes | | 32-bit values | Span two registers — document byte order per device | | Polling architecture | Round-robin with priority override for critical devices | | EtherNet/IP implicit | Fast cyclic I/O — configured in hardware, not code | | EtherNet/IP explicit | CIP messaging for parameters and diagnostics | | Health monitoring | Track comm quality, response time, consecutive errors | | Security | Network segmentation, firewalls, traffic monitoring |
The communication layer is the nervous system of your automation architecture. Build it with the same engineering rigor you apply to your control logic.