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.
1. Problem Description and Symptoms
The reported failure exhibits a deterministic, repeatable signature on every cycle of the user program:
- The C# client opens a
Socketagainst the S7-1200 server IP (e.g. 192.168.0.1) on port 2000 and successfully completes the three-way handshake. - The client transmits its first message; the S7-1200 acknowledges receipt and the TSEND reply of
"helloworld"returnsDONE=1on the S7 side. - The client attempts to send a second message on the same socket. The WinSock stack returns
0x80004005with a "socket access permissions" error and the socket descriptor is invalidated. - On the S7-1200, the TRCV instance remains in
BUSY=1, never transitions toDONE, andNDRnever sets — meaning the second client message never reaches the user program. - 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 theTIME_WAITtimer 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. |
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 mode —
LEN > 0. The block accumulates bytes until exactlyLENbytes have arrived, then setsNDR=1. If the partner sends fewer or more bytes, the block stays inBUSYindefinitely and never releasesNDR. -
AdHoc mode —
LEN = 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 theDATAreceive area.NDRsets 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. |
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; keepREQ = 1. -
ERROR = 1— readSTATUSfor the diagnostic code. Common values are16#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.
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;
}
}
}
- A single
Socketis created inConnect()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-sideBindto a fixed port is the single most common cause of0x80004005on Windows desktop builds. -
SendandReceivecalls are wrapped inlockto 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
- Online → CPU → Online & Diagnostics → General: confirm firmware
V4.1. - In the project tree, expand Program blocks → System blocks → Communication. Right-click TCON, TRCV, TSEND → Properties → Information → Version: each must show
3.0. - 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
- Open the TCON instance and click the wrench icon on the
CONNECTinput. - Select Partner: unspecified → TCP → Passive connection.
- Set Local port = 2000, leave Remote address = 0.0.0.0, Remote port = 0.
- Accept the wizard → a new
TCON_DB_1is created with the structure from the table in section 5.
Step 3 — Configure TRCV in AdHoc mode
- Set the
LENinput of TRCV to a constant0. - Wire
EN_Rto a tag that latches TRUE onTCON.DONEand resets only onTDISCON.DONEor a deliberate stop command. - Create a dedicated ReceiveDB with
ARRAY[0..1023] OF BYTEand point theDATAinput 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
- In the PLC, after
TCON.DONE = 0for more than 5 s, callTDISCON(REQ, then wait for DONE), then re-triggerTCON.REQ. - In the C# client, on any
SocketExceptionwithSocketErrorin{ConnectionReset, NetworkUnreachable, ConnectionAborted, NotConnected}, close the socket, wait 1 s, and callConnect()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, butSTATUScodes for length errors areW#16#8086_0001in V3.0 andW#16#8086_0000in V2.0. - For S7-1500 with block V4.x, additional parameters
BUSY_TYPEandEXT_STATUSare available; the framing on the wire is unchanged. - For UDP, use
TUSENDandTURCV; 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.