The Siemens S7 Communication (S7Comm) protocol is a proprietary mechanism designed for data exchange between Siemens S7-series PLCs. It operates above the transport layer, specifically interacting via TCP/IP networks through an encapsulation strategy. The protocol stack relies on ISO-on-TCP (RFC 1006), which nests the S7 payload within ISO-COTP (Connection Oriented Transport Protocol) and TPKT (Transport Packet) layers. This structure allows the Protocol Data Unit (PDU) to be transmitted reliably over standard Ethernet networks.
Protocol Frame Architecture
To communicate effectively, data must be packaged into a specific frame format consisting of three distinct layers:
- TPKT (Transport Packet): This layer handles the overall packet length and protocol version information, acting as the outermost wrapper.
- ISO-COTP (Session Layer): This section defines the PDU type and manages the logical connection parameters between the client and the PLC.
- S7 Protocol (Application Layer): The core payload containing the S7 header, which dictates the function (read/write), and the actual data parameters or payload.
Establishing a Connection
Unlike standard TCP communication, connecting to an S7 device requires a multi-stage handshake process. This ensures the session is properly registered with the PLC's processor before data exchange occurs. The sequence is as follows:
- TCP Handshake: Standard socket connection to port 102.
- COTP Connection Request: The first application-level handshake to negotiate transport parameters.
- S7 Setup Communication: The second application-level handshake to negotiate S7 specific parameters (like PDU length).
Once these steps are completed, the channel is open for reading and writing memory areas.
Implementing the S7 Client in C#
The following implementation demonstrates how to establish this connection and perform read/write operations using raw TCP sockets. The code has been refactored into a utility class to separate protocol logic from UI components.
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
public class S7PlcClient
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private readonly string _ipAddress;
private readonly int _port = 102;
public S7PlcClient(string ip)
{
_ipAddress = ip;
}
public async Task ConnectAsync()
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_ipAddress, _port);
_stream = _tcpClient.GetStream();
// 1. Send COTP Connection Request
byte[] cotpConnectFrame = new byte[]
{
// TPKT Header
0x03, 0x00, // Version, Reserved
0x00, 0x16, // Length (22 bytes)
// COTP Header
0x11, // Header length
0xE0, // PDU Type (Connect Request)
0x00, 0x00, // Destination Reference
0x00, 0x01, // Source Reference
0x00, // Class/Options
// Parameters
0xC1, 0x02, // Called TSAP
0x10, 0x00, // Called TSAP Value ( Rack/Slot )
0xC2, 0x02, // Calling TSAP
0x03, 0x01, // Calling TSAP Value
0xC0, 0x01, // TPDU Size
0x0A // Size code
};
await _stream.WriteAsync(cotpConnectFrame, 0, cotpConnectFrame.Length);
await ReadResponseAsync(); // Acknowledge COTP response
// 2. Send S7 Setup Communication Request
byte[] s7SetupFrame = new byte[]
{
// TPKT Header
0x03, 0x00,
0x00, 0x19, // Length (25 bytes)
// COTP Header (Data Transfer)
0x02, // Header length
0xF0, // PDU Type (Data)
0x80, // EOT flag
// S7 Header
0x32, // Protocol ID
0x01, // Job Type
0x00, 0x00, // Redundancy
0x00, 0x00, // TPDU Reference
0x00, 0x08, // Parameter length
0x00, 0x00, // Data length
// S7 Parameters (Setup)
0xF0, // Function Code (Setup)
0x00, // Reserved
0x00, 0x03, // Max AMQ Caller
0x00, 0x03, // Max AMQ Callee
0x03, 0xC0 // PDU Length (480 bytes)
};
await _stream.WriteAsync(s7SetupFrame, 0, s7SetupFrame.Length);
await ReadResponseAsync(); // Acknowledge S7 Setup response
Console.WriteLine("S7 Connection Established Successfully.");
}
public async Task ReadMemoryAsync()
{
// Example: Reading 4 bytes from Memory (M) area starting at M200.0
byte[] readRequest = new byte[]
{
// TPKT
0x03, 0x00,
0x00, 0x1F, // Length 31
// COTP
0x02, 0xF0, 0x80,
// S7 Header
0x32, 0x01,
0x00, 0x00,
0x00, 0x01, // Sequence ID
0x00, 0x0E, // Param Length
0x00, 0x00, // Data Length (Read req has 0 data)
// S7 Params (Read Var)
0x04, // Function Code (Read)
0x01, // Item Count
0x12, // Var Spec
0x0A, // Length of spec
0x10, // Syntax ID
0x02, // Transport Size (Byte)
0x00, 0x04, // Length (4 bytes)
0x00, 0x00, // DB Number (0 for M-area)
0x83, // Area Code (M-Memory)
// Address Calculation: 200 * 8 = 1600 = 0x640
// Bytes are stored as: 0x00, 0x06, 0x40
0x00, 0x06, 0x40
};
await _stream.WriteAsync(readRequest, 0, readRequest.Length);
byte[] response = await ReadResponseAsync();
// Parse data (payload starts after headers)
if (response != null && response.Length > 25)
{
Console.WriteLine($"Read Data: {response[25]}");
}
}
public async Task WriteMemoryAsync(int value)
{
byte[] dataBytes = BitConverter.GetBytes(value);
// Example: Writing 4 bytes to M200.0
byte[] writeRequest = new byte[]
{
// TPKT
0x03, 0x00,
0x00, 0x27, // Length 39
// COTP
0x02, 0xF0, 0x80,
// S7 Header
0x32, 0x01,
0x00, 0x00,
0x00, 0x01, // Sequence ID
0x00, 0x0E, // Param Length
0x00, 0x08, // Data Length (4 bytes value + transport info)
// S7 Params (Write Var)
0x05, // Function Code (Write)
0x01, // Item Count
0x12, 0x0A, // Var Spec
0x10, // Syntax ID
0x02, // Transport Size
0x00, 0x04, // Write Length (4 bytes)
0x00, 0x00, // DB Number (0)
0x83, // Area Code (M-Memory)
// Address 200.0 -> 0x00, 0x06, 0x40
0x00, 0x06, 0x40,
// Data Payload
0x00, // Fill byte
0x04, // Data Length indicator
// Actual Data (Little Endian)
dataBytes[3], dataBytes[2], dataBytes[1], dataBytes[0]
};
await _stream.WriteAsync(writeRequest, 0, writeRequest.Length);
await ReadResponseAsync();
Console.WriteLine("Write Command Sent.");
}
private async Task<byte> ReadResponseAsync()
{
byte[] buffer = new byte[1024];
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
byte[] received = new byte[bytesRead];
Array.Copy(buffer, received, bytesRead);
string hex = BitConverter.ToString(received);
Console.WriteLine($"RX: {hex}");
return received;
}
return null;
}
}
</byte>
Addressing Logic Explained
When constructing the request frames, addressing memory areas requires specific calculations. For Memory (M) areas or Data Blocks (DBs), the protocol uses a 3-byte addressing scheme.
- DB Number: If accessing a DB, the number is placed in the first 2 bytes. For flags or inputs (M, I, Q), these are typically
0x00 0x00. - Area Code: Identifies the memory region (e.g.,
0x83for Merkers/Memory,0x84for DBs). - Byte Offset: The specific byte index is calculated as
Address * 8to account for bit-level addressing, then encoded into the remaining 3 bytes in big-endian format.
For example, to access DB1.DBB100, the DB number is 1, and the byte offset is 100. For M200, the area code is 0x83, and the offset is 200.