Implementing Siemens S7 Protocol Communication in C#

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:

  1. TPKT (Transport Packet): This layer handles the overall packet length and protocol version information, acting as the outermost wrapper.
  2. ISO-COTP (Session Layer): This section defines the PDU type and manages the logical connection parameters between the client and the PLC.
  3. 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:

  1. TCP Handshake: Standard socket connection to port 102.
  2. COTP Connection Request: The first application-level handshake to negotiate transport parameters.
  3. 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., 0x83 for Merkers/Memory, 0x84 for DBs).
  • Byte Offset: The specific byte index is calculated as Address * 8 to 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.

Posted on Wed, 15 Jul 2026 16:53:02 +0000 by mrdonrule