S7-1200 TCP Communication with PC Using TCON/TSEND (No OPC)

David Krause11 min read
S7-1200SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The S7-1200 supports native TCP/IP communication on its PROFINET port through the Open User Communication instructions TCON, TSEND, TRCV, and TDISCON available in the TIA Portal instruction palette. These instructions allow a PC application written in C# (or any other language that exposes the Winsock API) to talk directly to the PLC over Ethernet without requiring Siemens SIMATIC NET OPC, S7-PCT, PC Access, or any third-party OPC server on the control PC.

This configuration is commonly used when:

  • The control PC cannot install vendor middleware (locked image, anti-virus exclusions, or air-gapped machine).
  • Data is read/written from a single DB (Data Block) with a fixed amount of bytes.
  • The PC is running Windows XP SP3, Windows 7, or Windows 10 with .NET Framework 2.0+ and the application is a service or HMI replacement written in C#.
Note: The original S7-1200 (order numbers 6ES721x-1xx30-0XB0) shipped with firmware 1.0 and did not expose all four open-communication blocks. TCON/TSEND/TRCV/TDISCON are fully available on firmware 4.0 and later. On firmware 2.x and 3.x, only TSEND_C / TRCV_C (compact versions) are reliable for partner connections. Always check the instruction's "Restrictions" tab in TIA Portal before commissioning.

Prerequisites

Item Required Value / Version
CPU S7-1200 (any DC/DC/DC or AC/DC/RLY variant with PROFINET port)
Firmware V4.0 or later recommended (V2.2 minimum for TSEND_C / TRCV_C)
TIA Portal V13 SP1 or later for full instruction support
PC Windows XP SP3 / Windows 7 / Windows 10 with .NET Framework 2.0+
IDE Visual Studio 2008 / 2010 / 2015+ with C# language
Network Direct Ethernet cable or managed switch, both on the same subnet (e.g. 192.168.0.x / 255.255.255.0)
DB One optimized-access DB or standard DB that holds the payload (see Data Layout below)

Network and IP Configuration

Set a static IP on the S7-1200 from Devices & Networks > PROFINET interface > Ethernet addresses. Pick an address that is not served by a corporate DHCP server to avoid address conflicts. A typical engineering setup uses:

  • PLC IP: 192.168.0.1 (Subnet: 255.255.255.0)
  • PC IP: 192.168.0.100 (Subnet: 255.255.255.0)

Disable the Windows firewall on the control PC for the engineering phase, or open the TCP port you intend to use (the example below uses 2500). Siemens documentation consistently recommends unallocated ports in the IANA dynamic/private range 49152–65535 for production systems to avoid clashes with services like HTTP (80) or Siemens S7 protocol (102).

Step 1 - Create the Payload Data Block

Create a standard (non-optimized) DB. Optimized access must be disabled for direct byte-level TCP transfer, because byte offsets must be deterministic. Example for a 100-byte payload:


DATA_BLOCK "DB_CommPayload"
{ S7_Optimized_Access := 'FALSE' }
AUTHOR : EngTeam
FAMILY : Comm
VERSION : 0.1
  STRUCT
    Header  : BYTE;       // 0xAA start marker
    Cmd     : BYTE;       // 0=read, 1=write, 2=ack
    Length  : WORD;       // payload length excluding header/trailer
    Data    : ARRAY[0..91] OF BYTE; // 92-byte payload
    Crc16   : WORD;       // CRC-16/CCITT over Cmd..Data
    Trailer : BYTE;       // 0x55 end marker
  END_STRUCT;
END_DATA_BLOCK

Total size: 1 + 1 + 2 + 92 + 2 + 1 = 99 bytes (rounded to 100 for transfer). The header / trailer markers and CRC make the protocol tolerant of half-received packets on noisy links.

Step 2 - Configure the TCON Connection

Drag TCON from Instructions > Communication > Open User Communication into OB1 (or a cyclic OB). TIA Portal will prompt you to create a Connection Description DB automatically. Use these parameters:

Parameter Value Notes
ID 1 Connection ID (W#16#0001). Must be unique per CPU.
CONNECT "TCON_Config_DB".Config Auto-generated connection DB.
REQ TRUE (first scan) Use a rising edge of a startup flag to trigger once.
DONE / BUSY / ERROR monitor only ERROR = 0x80xx style extended error codes.

Open the generated connection DB and edit the Interface / Configuration UDT:


InterfaceId  := 64;        // TCP/IP (0x40)
Id          := 1;         // matches TCON ID
ConnectionType := 16#0B;  // TCP (0x0B = 11 decimal)
ActiveEstablished := TRUE; // PLC acts as the *active* connection establisher
RemoteAddress := '192.168.0.100'; // PC
RemotePort    := 2500;
LocalPort     := 0;        // 0 = any free local port
Field note: When the PC is the server (typical for C# TcpListener), the S7-1200 must be the active peer, otherwise the PLC will not initiate the TCP three-way handshake and TCON will sit in BUSY = 1 forever. Set ActiveEstablished = TRUE on the PLC side and use TcpListener.AcceptTcpClient() on the PC side.

Step 3 - Send and Receive with TSEND / TRCV

With the connection established, drop TSEND and TRCV into OB1.

Block REQ / EN_R ID LEN / DATA Trigger
TSEND Rising edge 1 LEN = 100, DATA = P#DB_CommPayload.DBX0.0 BYTE 100 Operator button or scheduled task
TRCV TRUE (enable) 1 LEN = 100, DATA = P#DB_CommPayload.DBX0.0 BYTE 100 Always enabled; ADHOC mode if LEN = 0

TSEND/C send the entire DB region over TCP. TRCV supports ad-hoc mode by setting LEN = 0, which copies whatever bytes are currently buffered into the destination area. Use a length-prefixed protocol (as in the DB layout above) to handle partial frames.

Step 4 - TDISCON for Clean Shutdown

Add TDISCON in the startup OB (OB100) to close the connection when the CPU goes to STOP, or call it from a shutdown bit so the PC TcpListener does not throw IOException: An existing connection was forcibly closed on every restart.


"TDISCON_DB".REQ := TRUE;
"TDISCON_DB".ID  := 1;

Step 5 - C# PC Application

The PC side is a standard System.Net.Sockets.TcpListener. The example below opens a listener on port 2500, accepts the S7-1200, and pumps the 100-byte DB back and forth at 100 ms intervals. This is the minimal viable C# program for a Windows XP SP3 workstation with .NET 2.0+.


using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;

class PlcLink
{
    static TcpListener listener = new TcpListener(IPAddress.Any, 2500);
    static NetworkStream ns;
    static readonly object sync = new object();
    static byte[] rx = new byte[100];
    static byte[] tx = new byte[100];

    static void Main()
    {
        listener.Start();
        Console.WriteLine("Waiting for S7-1200 on 0.0.0.0:2500 ...");
        using (TcpClient client = listener.AcceptTcpClient())
        {
            Console.WriteLine("PLC connected from " + client.Client.RemoteEndPoint);
            ns = client.GetStream();
            // Worker thread: 100 ms poll
            new Thread(Pump) { IsBackground = true }.Start();
            // Main thread: read raw TCP frames
            while (true)
            {
                int n = ns.Read(rx, 0, rx.Length);
                if (n == 0) break;
                ParseFrame(rx, n);
            }
        }
    }

    static void Pump()
    {
        while (true)
        {
            lock (sync) BuildFrame(tx);  // populate tx from PC state
            ns.Write(tx, 0, tx.Length);
            Thread.Sleep(100);
        }
    }

    static void ParseFrame(byte[] b, int n)
    {
        if (b[0] != 0xAA || b[n - 1] != 0x55) { Console.WriteLine("Framing error"); return; }
        byte cmd = b[1];
        ushort len = BitConverter.ToUInt16(b, 2);
        ushort crc = BitConverter.ToUInt16(b, 96);
        if (crc != CrcCcitt(b, 1, 95)) { Console.WriteLine("CRC error"); return; }
        switch (cmd)
        {
            case 0: /* read request */ break;
            case 1: HandleWrite(b, len); break;
            case 2: /* ack */ break;
        }
    }

    static void BuildFrame(byte[] b)
    {
        b[0] = 0xAA; b[1] = 0x02; b[2] = 0; b[3] = 92;
        // Fill b[4..95] from local model
        ushort crc = CrcCcitt(b, 1, 95);
        b[96] = (byte)(crc & 0xFF); b[97] = (byte)(crc >> 8);
        b[98] = 0x55;
    }

    static void HandleWrite(byte[] b, int len) { /* update PC state */ }

    static ushort CrcCcitt(byte[] data, int offset, int count)
    {
        ushort crc = 0xFFFF;
        for (int i = 0; i < count; i++)
        {
            crc ^= (ushort)(data[offset + i] << 8);
            for (int j = 0; j < 8; j++)
                crc = (ushort)(((crc & 0x8000) != 0) ? (crc << 1) ^ 0x1021 : crc << 1);
        }
        return crc;
    }
}
Threading: NetworkStream.Read and Write are not thread-safe. Use a single producer/consumer pattern or a SemaphoreSlim around the stream. The example uses a coarse lock on sync for clarity; in production, prefer a dedicated send and receive thread plus a ConcurrentQueue<byte[]>.

Alternative - libnodave (No TIA Block Configuration)

For projects that need to read/write multiple DBs and I/O areas without writing a custom TCP protocol, the open-source libnodave library implements the S7 protocol (ISO-on-TCP port 102) directly from C, with .NET bindings used by many C# applications. libnodave works against the S7-1200 out of the box because the PLC keeps the S7 comm port open even when you do not configure TCON blocks.

Attribute Value
Project libnodave (SourceForge mirror, maintained fork on GitHub)
Protocol S7 communication (port 102, ISO-on-TCP / RFC 1006)
CPU side No TIA blocks required. Default S7 server is enabled when "Permit access with PUT/GET" is set in CPU Properties > Protection > Connection mechanisms.
Throughput ~30–50 DB reads/s from C# wrapper, depending on PC
Tested on S7-1200 firmware 4.2, Windows XP SP3, .NET 2.0/4.0

Sample wrapper call (C# P/Invoke into libnodave.net.dll):


// Connect
daveConnection dc = dave.NewConnection(2, "192.168.0.1", 0, 2); // 2=S7ONLINE_TCP
dave.Connect(dc);
// Read 100 bytes from DB1 starting at byte 0
dave.ReadBytes(dc, dave.DaveDB, 1, 0, 100, out buf);
// Write 4 bytes (REAL) to DB1.DBD10
dave.WriteFloat(dc, dave.DaveDB, 1, 10, 12.34f);
dave.Disconnect(dc);
Production caveat: libnodave is a community project without formal vendor support. Field reports from 2014–2016 deployments against S7-1200 firmware 4.x show stable operation for non-safety, non-silenced polling. For SIL/PLT-classified machines, validate under your own change-management process and consider the commercial libnodave.NET successor libnodave.NET or Snap7 as alternatives.

Step 6 - Enable PUT/GET on the S7-1200 (Required for libnodave)

  1. Open the CPU device configuration in TIA Portal.
  2. Select Properties > Protection > Connection mechanisms.
  3. Check Permit access with PUT/GET communication from remote partner.
  4. Download the hardware configuration to the CPU. The PLC will now answer on TCP/102 to any PC that knows the connection password (or has no password set, which is the default in protected projects).

Without this tick, libnodave (and any other third-party S7 client) will silently fail with daveResNoPeriphery or simply timeout.

Verification

Use this checklist after the program is loaded and the cable is connected:

  1. Link lights: Both PROFINET port LEDs on the CPU and the PC NIC are solid green. A blinking orange light indicates a speed/duplex mismatch.
  2. Ping test: From a CMD window, ping 192.168.0.1 -t. Replies must be <1 ms on a direct cable. Any value >5 ms suggests a switch loop or a duplex issue.
  3. TCON status: In TIA Portal's Online & Diagnostics > Status & Error, the connection ID 1 must show ESTABLISHED. A BUSY state that never clears means the PC is not listening; check Windows firewall and that listener.Start() was reached.
  4. TSEND done: After a rising edge on TSEND.REQ, monitor DONE for a single cycle. ERROR = 0x80C4 means "connection terminated by remote" - the PC closed the socket.
  5. TRCV data: Watch "DB_CommPayload".Header in the watch table. It should change to 16#AA within one cycle of the PC sending.
  6. Wireshark trace (optional): Capture on the PC interface and filter tcp.port == 2500. You should see a SYN/SYN-ACK/ACK handshake, then bidirectional PSH/ACK segments of 100 bytes each.

Troubleshooting Matrix

Symptom Likely Cause Fix
TCON stays BUSY forever PC not listening or wrong port Verify netstat -an | findstr 2500 on PC shows LISTENING
TSEND ERROR = 0x80C4 Partner closed socket PC threw exception; wrap Read in try/catch and call listener.Stop(); listener.Start();
TRCV.NDR but data is stale Ad-hoc mode LEN mismatch Use fixed LEN or implement length-prefix in payload
libnodave timeout on connect PUT/GET not enabled Enable as in Step 6
CRC errors on every frame Byte-order assumption wrong Confirm little-endian on PC matches S7-1200 byte layout
PLC goes to SF on download Optimized DB and TCON conflict Disable optimized access on payload DB
Only first cycle works TSEND REQ held high; S7 only sends on rising edge Use a 1-Hz clock bit or pulse on DONE

Open Communication Wizard (S7-300 Reference Still Valid for S7-1200)

Siemens' Open Communication Wizard (entry ID 25209116) was originally written for S7-300/400, but the generated function blocks (FB100 through FB105 for TCP/UDP, ISO-on-TCP) are source-compatible with the S7-1200 once you re-import them into TIA Portal. The companion entry 40556214 documents the variant where an S7-300 acts as the partner to an S7-1200, which is useful as a stepping-stone to migrate older machines without rewriting the PC side.

Field-Engineering Checklist Before Going Live

  1. Lock the PC's IP with DHCP reservations or static config to avoid silent address drift after a router reboot.
  2. Set the PC's Windows power profile to High Performance and disable NIC power saving, which can drop TCP sockets after long idle periods.
  3. Implement a heartbeat in the payload (e.g. incrementing WORD at byte 4) and alarm the SCADA if it freezes for > 3 s.
  4. Run a 72-hour soak test with Wireshark capturing in rolling mode; review for retransmissions, which indicate buffer overruns on the PLC side - the fix is usually to lower the TRCV frequency or increase the receive buffer (where supported on the firmware version).

FAQ

Which S7-1200 firmware versions support TCON, TSEND, TRCV, and TDISCON natively?

Full TCON/TSEND/TRCV/TDISCON support begins at firmware V4.0. Firmware 2.x and 3.x only expose the compact variants TSEND_C and TRCV_C, which combine connection setup, send, and receive in a single block and are usually sufficient for a C# TcpListener peer.

Do I need to open TCP port 102 on the Windows firewall for libnodave?

Yes. libnodave uses the S7 protocol on ISO-on-TCP port 102, so the firewall rule must allow inbound TCP/102 from the PLC's IP. The custom TCP/2500 example above requires inbound TCP/2500 instead. Restrict the rule to the PLC's source IP for production hardening.

Can the S7-1200 act as a TCP server instead of the PC?

Yes, by setting ActiveEstablished = FALSE on the connection DB. The PC's TcpClient.Connect() then triggers the handshake. This is the more common arrangement for SCADA software and is also the topology assumed by libnodave's dave.NewConnection() with connection type TCP.

Why does TSEND report ERROR = 0x80C4 after a few hours of stable operation?

0x80C4 means the partner closed the TCP socket cleanly. The most common cause is the PC's NetworkStream being disposed by an unhandled exception in the C# code. Wrap the read loop in try/catch and re-create the TcpListener on every disconnect; also enable SO_KEEPALIVE on the socket to detect broken cables within minutes rather than hours.

Is libnodave safe to use on a production machine in 2025?

libnodave is stable for non-safety, diagnostic-class polling against S7-1200 firmware 4.x. For new projects requiring vendor support, evaluate the commercial libnodave.NET fork or Snap7. For SIL/PLT applications, treat libnodave as untrusted middleware and validate under your own functional-safety change-management process.

Back to blog