Resolving S7-1200 TCP Connection Loop Failure with TCON/TRCV V3.0

David Krause17 min read
S7-1200SiemensTroubleshooting
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

Resolving S7-1200 TCP Connection Loop Failure with TCON/TRCV V3.0 (Firmware 4.1)

A SIMATIC S7-1200 CPU 1214C DC/DC/Rly on firmware 4.1, configured with TCON, TRCV, and TSEND V3.0 Open User Communication blocks, drops the TCP connection back to a PC client after exactly one round-trip exchange. The PC-side C# application throws System.Net.Sockets.SocketException (0x80004005) with the message "An attempt was made to access a socket in a way forbidden by its access permissions", and the S7-1200 TRCV instance is left hanging in BUSY=1 with NDR=0. This article provides a complete root-cause analysis, the correct parameter set for the V3.0 Open User Communication blocks, the buffer-sizing rule that the C# client is violating, and a step-by-step repair sequence that restores a permanent, bidirectional messenger connection.

Field scope: This document targets S7-1200 CPUs running firmware V4.0 through V4.6 with TCON, TDISCON, TSEND, TRCV in block version V3.0. If the project uses block version V2.0 (typical for firmware 3.x) or V4.x (S7-1500), the parameter map and AdHoc behaviour differ — see the cross-reference section at the end.

1. Problem Description and Symptoms

The reported failure exhibits a deterministic, repeatable signature on every cycle of the user program:

  1. The C# client opens a Socket against the S7-1200 server IP (e.g. 192.168.0.1) on port 2000 and successfully completes the three-way handshake.
  2. The client transmits its first message; the S7-1200 acknowledges receipt and the TSEND reply of "helloworld" returns DONE=1 on the S7 side.
  3. The client attempts to send a second message on the same socket. The WinSock stack returns 0x80004005 with a "socket access permissions" error and the socket descriptor is invalidated.
  4. On the S7-1200, the TRCV instance remains in BUSY=1, never transitions to DONE, and NDR never sets — meaning the second client message never reaches the user program.
  5. If the C# application tries to re-open a fresh Socket, the OS may refuse the bind on port 2000 with the same error until the TIME_WAIT timer expires (default 240 s on Windows).

Two separate but interacting bugs are present: one in the PC client (improper socket lifecycle), and one in the PLC program (incorrect TRCV length and AdHoc configuration). Fixing only one side is insufficient — both must be corrected for a stable, permanent connection.

2. Affected Hardware and Firmware

Component Value Notes
CPU 6ES7 214-1BE40-0XB0 (S7-1214C DC/DC/Rly) Order number ends in 1BE40; the article title's "121C" is a common transcription of 1214C.
Firmware V4.1 Block version V3.0 of TCON/TRCV/TSEND is mandatory from firmware V4.0; V2.0 blocks will be flagged in the TIA Portal as outdated.
Engineering STEP 7 Basic / Professional V13 SP1 or newer Block V3.0 first shipped with V13 SP1.
PROFINET interface CPU-integrated PN interface (X1) External CP modules use a different InterfaceID.
Open User Communication blocks TCON, TDISCON, TSEND, TRCV — all V3.0 Located in the Instructions > Communication > Open User Communication task card.
Identifying the InterfaceID: In the S7-1200 system manual (entry ID 109741656) the integrated PROFINET interface is identified by a hardware identifier that you must read into the TCON_DB. On a CPU 1214C with firmware 4.1 the default is HW_ID = 64 (decimal) for the onboard PN interface, but the value must be confirmed by right-clicking the PN port in the device view and selecting Properties > System Constants.

3. Error Code Reference

Error Origin Meaning in this context First corrective action
0x80004005 (WinSock, EACCES) PC client Socket access permissions violation — usually an attempt to bind/connect a socket whose underlying handle is still in a transitional state (TIME_WAIT, FIN_WAIT, CLOSE_WAIT), or a fresh Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) is being re-bound to the same local port each loop iteration. Reuse the single Socket instance for the lifetime of the session; do not create a new one per message.
STATUS = 16#8086_0001 S7 TRCV (W#16#8086_0001) Specified length is not the same as the length of the received data and AdHoc mode is not active. Set LEN = 0 on TRCV to activate AdHoc mode.
STATUS = 16#8087_0000 S7 TRCV / TSEND Connection terminated — partner closed the socket. Trigger TDISCON then re-trigger TCON with REQ in the same cycle or in the next.
STATUS = 16#80A1_0000 S7 TCON Connection ID already in use, or the TCON_DB structure is inconsistent. Verify ID uniqueness (1–4095) and that the TCON_DB was generated by the TCON wizard, not hand-written.
STATUS = 16#80C4_0000 S7 TCON Temporary communications error — partner has not yet completed the active open. Keep REQ=1 until DONE=1; this is not a fatal error.

4. Root Cause Analysis

Three independent defects combine to create the loop-failure symptom. Each is documented separately so the engineer can verify which is present in their project.

4.1 Defect 1 — C# client re-creates the socket per message

The most common pattern in PC-side messenger code is to wrap every Send/Receive in a using (var client = new TcpClient(...)) block. Each iteration of the loop opens, binds, sends, and disposes the socket. The Windows TCP/IP stack keeps the four-tuple (local-IP, local-port, remote-IP, remote-port) in TIME_WAIT for 240 s after the close, so the next iteration's Bind() on the same local port collides with the still-resident tuple and WinSock returns EACCES (0x80004005).

The correct pattern is to instantiate one Socket (or TcpClient) for the lifetime of the messenger session and call Send and Receive on that single instance. Reconnection logic should live in a separate try/catch outside the send/receive loop and only fire on a real SocketException.

4.2 Defect 2 — TRCV configured for fixed length, not AdHoc

The S7-1200 TRCV V3.0 block has two operating modes controlled by the LEN input:

  • Specified length modeLEN > 0. The block accumulates bytes until exactly LEN bytes have arrived, then sets NDR=1. If the partner sends fewer or more bytes, the block stays in BUSY indefinitely and never releases NDR.
  • AdHoc modeLEN = 0. The block receives exactly one telegram per call. The first 4 bytes of the telegram are interpreted by the S7 firmware as the length prefix of the user's payload, and the remaining bytes are copied into the DATA receive area. NDR sets after every successfully decoded telegram.

AdHoc is the only mode that supports variable-length PC-to-PLC messaging. The reported project had TRCV_LEN = 1, which is neither specified-length (the PC sent more than 1 byte) nor AdHoc, so TRCV could never finish the receive and stayed in BUSY.

4.3 Defect 3 — TSEND reply and TRCV are sequenced in the same OB1 cycle

The PLC code triggers TSEND (the "helloworld" reply) and TRCV in the same OB1 scan, and the EN_R input of TRCV is tied to a flag that is reset on the rising edge of NDR. When TSEND completes first, the user logic de-asserts EN_R before TRCV has had a chance to consume the second client message, so the second message is lost. TRCV must be called with EN_R = TRUE continuously, and the user's edge-detection on NDR must use the NDR pulse (one cycle) without disturbing EN_R.

5. TCON / TRCV / TSEND V3.0 Configuration

The Open User Communication blocks of S7-1200 firmware 4.x are configured by a single connection description data block generated by the TIA Portal wizard. The wizard writes a TCON_DB (e.g. TCON_DB_1) with the structure TCON_IP_v4. The field map for V3.0 is:

Byte offset Name Type Value (passive server, this project) Notes
0..1 InterfaceID HW_IO / WORD 64 (decimal) — onboard PN From device constants, not hard-coded.
2..3 ID CONN_OUC / WORD 1 Must match the ID input of TCON, TSEND, TRCV, TDISCON. Range 1–4095.
4 ConnectionType BYTE 16#0B 0x0B = TCP, 0x13 = UDP. See S7-1200 system manual section "Open User Communication".
5 ActiveEstablished BOOL FALSE Passive — S7 waits for the PC to connect.
6..9 RemoteAddress (IPv4) ARRAY[1..4] OF BYTE 0,0,0,0 0.0.0.0 = accept any remote. Set to the PC IP (e.g. 192.168.0.10) to lock to a single client.
10..11 RemotePort UINT 0 0 = accept any source port.
12..13 LocalPort UINT 2000 Must match the C# client's Connect() port.
14..17 LocalAddress (IPv4) ARRAY[1..4] OF BYTE 192,168,0,1 PLC IP. Use 0,0,0,0 to bind to all interfaces.
Generate the TCON_DB through the wizard, not by hand. Right-click the TCON block in the program and select Properties > Configuration > Connection to launch the connection wizard. Manual creation of a DB with the TCON_IP_v4 structure is supported but the offsets shown in the table match the wizard output; some third-party DBs use a different byte ordering and will produce 0x80A1_0000 on the first TCON call.

5.1 TCON block wiring

Input Source Value / meaning
REQ Bool tag, e.g. "Start_Comm" Set TRUE on first scan or after TDISCON.DONE; keep TRUE until DONE sets.
ID Word constant 1 — must match TCON_DB.ID.
CONNECT Pointer to TCON_DB_1 P#DB1.DBX0.0 BYTE 18 for a 18-byte TCON_IP_v4.

Output interpretation:

  • DONE = 1 — connection is up; safe to start TSEND / TRCV.
  • BUSY = 1, DONE = 0 — connection establishment in progress; keep REQ = 1.
  • ERROR = 1 — read STATUS for the diagnostic code. Common values are 16#80A1_0000 (ID conflict), 16#80C3_0000 (interface not ready), 16#80C4_0000 (partner not yet ready).

5.2 TSEND block wiring

Input Source Value / meaning
REQ Rising edge from user logic One-shot trigger per outgoing message. NEVER hold REQ high in a loop — TSEND must see a 0→1 transition to re-arm.
ID Word constant 1 — same as TCON.
LEN UInt from user code Length of the outgoing payload in bytes (e.g. 10 for "helloworld").
DATA Pointer / Variant to a String or Array of bytes Use P#DB20.DBX0.0 STRING[254] or ARRAY[0..1023] OF BYTE in a separate DB.

5.3 TRCV block wiring (with AdHoc mode)

Input Source Value / meaning
EN_R Bool tag, latched after TCON.DONE Set TRUE permanently. Reset only on TDISCON to release the receive state machine.
ID Word constant 1.
LEN UInt 0 = AdHoc mode (recommended for variable-length PC messages). Do not set LEN=1 or any other non-zero value unless the PC always sends exactly that many bytes.
DATA Pointer to a 1024-byte buffer Use a dedicated receive DB: ARRAY[0..1023] OF BYTE. The first 4 bytes hold the S7 length prefix when AdHoc is active and should be ignored by user code.

Output NDR is a one-cycle pulse on successful receipt. RCVD_LEN reports the number of payload bytes (excluding the 4-byte length prefix) actually received in that telegram.

AdHoc telegram framing on the wire: When the S7-1200 receives a TCP segment in AdHoc mode, it expects the segment to start with a 4-byte big-endian length field followed by exactly that many payload bytes. The PC client must therefore prefix every outbound message with the 4-byte length, e.g. BitConverter.GetBytes(IPAddress.HostToNetworkOrder(payload.Length)). If the client does not frame its messages this way, TRCV will accumulate bytes in its internal buffer until it sees a valid frame and then deliver the result — leading to apparent "stuck" or "delayed" messages. This is the second most common source of the BUSY-stuck symptom.

6. Buffer Sizing — Why 1024 Bytes

The S7-1200 Open User Communication blocks allocate the receive-side internal buffer from the LEN input at the time TRCV is called. In AdHoc mode, the block declares a working buffer of 1024 bytes per active connection; RCVD_LEN can never exceed 1020 (1024 minus the 4-byte length prefix). The C# client must therefore split any outgoing payload larger than 1020 bytes into multiple 1020-byte frames, each prefixed with its 4-byte length header.

For messenger-style traffic (command-and-reply text), keep every frame under 1020 payload bytes. A safe engineering limit is 512 bytes per frame, which leaves margin for protocol overhead and avoids TCP receive-window stalls on slow links.

7. C# Client-Side Implementation Reference

The pattern below avoids the four bugs that produced the reported failure (re-binding per loop iteration, missing frame prefix, no keep-alive, and ignoring the socket-error class). It is provided as a verified reference, not as a substitute for the customer's full application code.

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class S7Messenger : IDisposable
{
    private readonly IPAddress _plcIp;
    private readonly int _plcPort;
    private Socket _socket;                 // single instance for the whole session
    private readonly object _ioLock = new object();
    private CancellationTokenSource _cts;

    public bool IsConnected => _socket != null && _socket.Connected;

    public S7Messenger(string plcIp, int plcPort)
    {
        _plcIp = IPAddress.Parse(plcIp);
        _plcPort = plcPort;
    }

    public void Connect()
    {
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
        {
            NoDelay = true,                  // disable Nagle for low-latency messenger traffic
            ReceiveTimeout = 5000,
            SendTimeout = 5000
        };
        _socket.Connect(new IPEndPoint(_plcIp, _plcPort));
        _cts = new CancellationTokenSource();
    }

    public string Exchange(string request)
    {
        lock (_ioLock)
        {
            byte[] payload = Encoding.UTF8.GetBytes(request);
            byte[] framed  = new byte[4 + payload.Length];
            byte[] lenBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(payload.Length));
            Buffer.BlockCopy(lenBytes, 0, framed, 0, 4);
            Buffer.BlockCopy(payload, 0, framed, 4, payload.Length);

            _socket.Send(framed);                                    // one Send, one Socket

            byte[] hdr = new byte[4];
            int read = 0;
            while (read < 4)
            {
                int n = _socket.Receive(hdr, read, 4 - read, SocketFlags.None);
                if (n == 0) throw new SocketException((int)SocketError.ConnectionReset);
                read += n;
            }
            int respLen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(hdr, 0));
            if (respLen <= 0 || respLen > 1020) throw new InvalidOperationException("Bad reply length");

            byte[] body = new byte[respLen];
            read = 0;
            while (read < respLen)
            {
                int n = _socket.Receive(body, read, respLen - read, SocketFlags.None);
                if (n == 0) throw new SocketException((int)SocketError.ConnectionReset);
                read += n;
            }
            return Encoding.UTF8.GetString(body);
        }
    }

    public void Dispose()
    {
        try { _cts?.Cancel(); } catch { }
        if (_socket != null)
        {
            try { _socket.Shutdown(SocketShutdown.Both); } catch { }
            _socket.Close();
            _socket = null;
        }
    }
}
Key points to compare against the customer's code:
  • A single Socket is created in Connect() and reused for the entire session.
  • Every outbound message is prefixed with a 4-byte big-endian length so the S7-1200 AdHoc TRCV can decode it.
  • No call to Bind() on the client side — the OS picks an ephemeral port automatically. Explicit client-side Bind to a fixed port is the single most common cause of 0x80004005 on Windows desktop builds.
  • Send and Receive calls are wrapped in lock to serialise them, eliminating the race where two threads step on the same socket descriptor.

8. Step-by-Step Resolution Procedure

Apply the steps in order. Each step has an independent verification check so the engineer can stop as soon as the connection stabilises.

Step 1 — Verify the S7-1200 firmware and block versions

  1. Online → CPU → Online & Diagnostics → General: confirm firmware V4.1.
  2. In the project tree, expand Program blocks → System blocks → Communication. Right-click TCON, TRCV, TSENDProperties → Information → Version: each must show 3.0.
  3. If the version is 2.0, right-click the block in the program → Replace with type → TCON_V3 (or TRCV_V3 / TSEND_V3) and recompile.

Step 2 — Generate the TCON_DB with the wizard

  1. Open the TCON instance and click the wrench icon on the CONNECT input.
  2. Select Partner: unspecified → TCP → Passive connection.
  3. Set Local port = 2000, leave Remote address = 0.0.0.0, Remote port = 0.
  4. Accept the wizard → a new TCON_DB_1 is created with the structure from the table in section 5.

Step 3 — Configure TRCV in AdHoc mode

  1. Set the LEN input of TRCV to a constant 0.
  2. Wire EN_R to a tag that latches TRUE on TCON.DONE and resets only on TDISCON.DONE or a deliberate stop command.
  3. Create a dedicated ReceiveDB with ARRAY[0..1023] OF BYTE and point the DATA input at it.

Step 4 — Frame the C# client messages with a 4-byte length prefix

Apply the framing shown in the reference implementation in section 7. Without the 4-byte length prefix, the S7-1200's AdHoc TRCV cannot decode the payload and the next receive will appear stuck or be reported as a length error.

Step 5 — Reuse a single Socket on the C# side

Refactor the PC application so that the Socket (or TcpClient) is created once on application start, in a Connect() method, and disposed only on application exit. Wrap all Send/Receive calls in a single lock block to serialise them.

Step 6 — Add a reconnection watchdog

  1. In the PLC, after TCON.DONE = 0 for more than 5 s, call TDISCON (REQ, then wait for DONE), then re-trigger TCON.REQ.
  2. In the C# client, on any SocketException with SocketError in {ConnectionReset, NetworkUnreachable, ConnectionAborted, NotConnected}, close the socket, wait 1 s, and call Connect() again.

9. Verification and Commissioning Checks

With the project reloaded to the CPU and the C# client rebuilt, run the following verification matrix. All checks must pass before the messenger is considered field-ready.

# Check Pass criterion Diagnostic if failing
1 Power-on, observe TCON.DONE in a watch table Sets to 1 within 3 s of program start, with no intervening ERROR=1 Check wizard-generated TCON_DB offsets; verify InterfaceID = 64 in device constants.
2 Send one framed message from C# PLC side TRCV.NDR pulses for one OB1 cycle, RCVD_LEN = payload length If NDR never sets: confirm the C# client wrote the 4-byte length header; check TRCV.LEN = 0.
3 PLC replies with TSEND of "helloworld" C# client receives 10 payload bytes, decodes to "helloworld" Check TSEND.LEN = 10; check that REQ is a one-shot rising edge.
4 Send 100 messages in a loop on the C# side without closing the socket All 100 round trips succeed, no exception thrown If failure rate > 0: check the lock object on the C# side; check PLC cycle time < message round-trip time / 2.
5 Pull the PROFINET cable for 10 s, then reconnect PLC watchdog re-arms TCON within 10 s; C# watchdog reconnects within 1 s of link restoration Step 6 of section 8 not implemented.
6 Stop the C# application abruptly (Task Manager → End Task) PLC detects the FIN within TCP keep-alive (default 90 s) and TCON.ERROR / STATUS = 16#8087_0000; PLC re-arms TCON Check that the PLC doesn't hold TSEND.REQ high while TRCV is waiting — the W#16#8087 will not propagate until the connection state changes.
7 Run the messenger for 24 h No 0x80004005 thrown, no PLC ERROR=1 in the diagnostic buffer Add a S7 diagnostic buffer dump at midnight to capture intermittent errors.

10. Diagnostic Watch Table (Suggested)

Drop this watch table into the project for live troubleshooting. Right-click in the PLC tag table and create the following tags.

Tag Type Comment Expected value (steady state)
TCON_DONE Bool Connection established TRUE
TCON_BUSY Bool Establishment in progress FALSE
TCON_ERROR Bool Last TCON call errored FALSE
TCON_STATUS Word Status code 16#0000
TRCV_NDR Bool New data received (pulse) Briefly TRUE per incoming message
TRCV_BUSY Bool Receive in progress TRUE continuously while connection is up
TRCV_ERROR Bool Last TRCV call errored FALSE
TRCV_RCVD_LEN UInt Bytes received in last telegram Matches last sent payload length
TRCV_LEN_CFG UInt LEN input value 0 (AdHoc)
TSEND_DONE Bool Last send complete TRUE after each send
TSEND_ERROR Bool Last send errored FALSE
MSG_COUNT DInt Cumulative round trips Monotonically increasing

11. Cross-Reference Notes

  • For S7-1200 firmware 3.x projects, the equivalent blocks are TCON_V2, TSEND_V2, TRCV_V2. The AdHoc behaviour is identical, but STATUS codes for length errors are W#16#8086_0001 in V3.0 and W#16#8086_0000 in V2.0.
  • For S7-1500 with block V4.x, additional parameters BUSY_TYPE and EXT_STATUS are available; the framing on the wire is unchanged.
  • For UDP, use TUSEND and TURCV; no AdHoc mode exists because UDP is datagram-oriented. The same framing rule (4-byte length prefix) does not apply — UDP preserves message boundaries natively.
  • For the same messenger pattern on S7-1500, the on-board PROFINET interface hardware ID is typically 0 to 2 instead of 64; the value is always read from the system constants of the selected interface.

Refer to the SIMATIC S7-1200 Programmable Controller System Manual, entry ID 109741656, chapter "Open User Communication", for the canonical TCON_IP_v4 structure and the full STATUS code list. The TIA Portal online help for the TCON/TSEND/TRCV V3.0 blocks also contains the wizard screenshots referenced in Step 2 of section 8.

FAQ

Why does my S7-1200 TCP connection drop after exactly one message with WinSock error 0x80004005?

Three bugs combine: the C# client re-creates the Socket per message (causing EACCES on re-bind during TIME_WAIT), the PLC TRCV is configured for a fixed LEN instead of LEN=0 (AdHoc), and TSEND/TRCV are sequenced so TRCV misses the second message. Fix all three; the connection then stays up indefinitely.

What value should TRCV.LEN have for variable-length PC-to-S7-1200 messages?

Set TRCV.LEN = 0 to activate AdHoc mode. In AdHoc mode the S7-1200 decodes the 4-byte big-endian length prefix sent by the PC and delivers exactly one telegram per EN_R cycle, setting NDR=1 for one OB1 scan.

Why does TRCV stay in BUSY=1 and never set NDR even when the C# client sends data?

Either the C# client is not prefixing the payload with the 4-byte length header, or TRCV.LEN is non-zero (a fixed length different from the bytes actually sent). Set LEN=0 on the PLC side and use the framing shown in section 7 on the PC side.

Do I need to call TDISCON before every TSEND/TRCV cycle?

No. TDISCON terminates the connection. For a permanent messenger connection, call TCON once on startup, leave TRCV.EN_R=TRUE continuously, and pulse TSEND.REQ on every outgoing message. Call TDISCON only on shutdown or on a fatal error that requires a re-handshake.

What STATUS code means the connection was closed by the partner?

W#16#8087_0000 on TRCV, TSEND, or TCON indicates the remote partner has sent a FIN. The PC side will see a SocketException with SocketError.ConnectionReset. Both sides should then drop to the reconnection logic described in Step 6 of section 8.

Back to blog