S7-1200 TCP TSEND Stuck After TRCV: Root Cause and Fix

David Krause12 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

Problem Overview

An S7-1200 CPU 1214C using the TCON, TSEND, and TRCV instructions to exchange HEX frames with a third-party device over a raw TCP connection (192.168.255.2 ↔ 192.168.255.1, port 10001) executes the first transaction successfully. After the first TSEND/TRCV cycle, the second TSEND request never completes: the DONE bit does not latch and BUSY remains cleared while ERROR momentarily pulses high. The same scenario works flawlessly when tested against a PC running a terminal emulator (e.g., HyperTerminal, PuTTY) on the same port.

This is one of the most common support cases on the S7-1200 platform because the failure is silent and only appears after the first transaction. The root cause is almost always missing handshake sequencing between the receive and send instructions, not a fault in the physical connection or the partner device.

Affected Hardware and Firmware

Component Value
CPU SIMATIC S7-1200, CPU 1214C DC/DC/DC or DC/DC/RLY
Order numbers 6ES7214-1AG40-0XB0 (FW 4.x), 6ES7214-1BG40-0XB0 (FW 4.x), 6ES7214-1HG40-0XB0 (FW 4.x)
Supported firmware range V4.0 and later (open user communication added in V4.0)
Software STEP 7 / TIA Portal V13 SP1 or later
Communication blocks TCON, TSEND, TRCV, TDISCON, T_RESET (from "Communication" palette)
Protocol TCP/IP (RFC 793), ISO-on-TCP optional, UDP optional

Reference: SIMATIC S7-1200 Programmable Controller System Manual (entry ID 109478121).

How TCP Behaves on a Single S7-1200 Connection ID

The Transmission Control Protocol is connection-oriented and full-duplex. A single established connection allows independent, simultaneous data flow in both directions, governed by the TCP state machine (LISTEN, SYN-SENT, ESTABLISHED, FIN-WAIT, CLOSE-WAIT, etc.). See RFC 793 and the IANA assignment database for protocol numbers.

On the S7-1200, a single TCON instance with one connection ID is therefore technically sufficient to send and receive on the same socket. The TSEND and TRCV blocks reference that ID and the same active connection descriptor. The constraint is not the wire protocol; it is the way the firmware schedules the TSEND/TRCV job buffer.

Important: The same connection descriptor is bidirectional. The partner device does not need to open a new socket to reply. The S7-1200 can call TSEND and TRCV against the same ID, provided the application honors the job-state transitions of each block.

Root Cause: Overlapping Job State on a Single Connection

Each TSEND and TRCV call is a stateful job. Internally, the firmware uses two job slots per connection ID: one for transmit, one for receive. The blocks expose the following status outputs that must be polled every cycle:

Output Type Meaning
DONE / NDR BOOL One-cycle TRUE on successful completion (NDR = "New Data Received" on TRCV)
BUSY BOOL TRUE while the job is in progress
ERROR BOOL One-cycle TRUE if the job aborted
STATUS WORD Detailed error or progress code (W#16#...)

The failure mode in the reported case is created when the application triggers the second TSEND while the previous TRCV job is still flagged as active, or when the rising edge of REQ is masked by an already-latched internal request. The firmware rejects the new transmit request silently, returns STATUS = W#16#80A1 ("Connection or channel is currently busy processing a job") or W#16#8085 ("No new job was started because a job is already active"), and clears BUSY before DONE is set.

Three concrete variants produce the symptom:

  1. Latched REQ with no edge: REQ of TSEND is wired to a tag that is already TRUE. TSEND only accepts a rising edge of REQ; a level-driven REQ does not restart a job.
  2. Overlapping jobs: A second TSEND REQ is issued before TRCV has signalled NDR (or before DONE from the previous TSEND has been acknowledged).
  3. Connection descriptor corruption: The same connection ID is reused by two independent TCON DBs, causing the firmware to swap descriptors on the second call. This is the only variant that requires separate TCON instances.

TSEND / TRCV Parameter Reference

All values are taken from the Siemens function-block help in TIA Portal. The exact same interface applies to TSEND_C / TRCV_C (the "Compact" variants that bundle the TCON setup).

Block Parameter Direction Type Description
TSEND REQ IN BOOL Rising edge starts the send job
TSEND ID IN CONN_OUC Connection ID from TCON (WORD in older projects)
TSEND LEN IN UINT Number of bytes to send (0 = use DATA length)
TSEND DATA IN_OUT VARIANT Source area (DB, M, P, or tag of BYTE/CHAR/INT/WORD/DWORD/REAL/STRING)
TSEND DONE OUT BOOL Job finished without error
TSEND BUSY OUT BOOL Job in progress
TSEND ERROR OUT BOOL Job aborted
TSEND STATUS OUT WORD Error / progress code
TRCV EN_R IN BOOL Enable receive (must be level-driven TRUE to accept data)
TRCV ID IN CONN_OUC Connection ID from TCON
TRCV LEN IN UINT 0 = length is part of the frame (advisory length on TCP)
TRCV DATA IN_OUT VARIANT Receive buffer (use a sufficiently large DB area)
TRCV NDR OUT BOOL New data has been received
TRCV BUSY OUT BOOL Job in progress
TRCV ERROR OUT BOOL Job aborted
TRCV STATUS OUT WORD Error / progress code
TRCV RCVD_LEN OUT UINT Actual number of bytes received

Common STATUS Codes (W#16#...)

Full list in the TIA Portal online help, "Status codes for TSEND / TRCV / TCON". Most relevant values:

STATUS Meaning Remediation
0000 No error, job complete (DONE/NDR) None
7000 Job idle, no operation in progress None
7001 Job running, first call Wait, keep REQ/EN_R set
7002 Job running, follow-up call Wait, keep REQ/EN_R set
80A1 Connection or channel busy De-assert REQ, wait for BUSY=FALSE, then re-trigger on edge
8085 No new job started (REQ already active) Generate a clean rising edge of REQ
80A3 Connection being terminated (RST received) Call T_RESET, then re-establish via TCON
80A4 Connection not yet established Verify TCON state (DONE), check partner
80A7 TCP connection refused / no listener on partner Verify partner IP/port, firewall
80B3 Error in connection parameters (length / structure) Re-check TCON DB ("TCON_Config")
80C3 Local resource shortage Reduce number of active connections, check memory
80C4 Temporary local resource error Retry; if persistent, restart connection

Reference: Siemens KB entry 67196808 — "Why does TSEND/TRCV not work the way I expect?" and the TIA Portal integrated block help.

Solution: Implement a Strict TX/RX State Machine

The reliable pattern on a single TCON ID is to drive the application from explicit states and to gate each new request on the successful completion of the previous one. The sequence required by the original poster is:

  1. Establish the connection once with TCON. Latch TCON.DONE into a static tag (iConnEstablished).
  2. Enable TRCV continuously with EN_R = TRUE. On NDR, copy the buffer to the application DB and raise iRxComplete.
  3. Compare transmitted and received data. If they match, raise iTxPending and generate a single rising edge of TSEND.REQ.
  4. On TSEND.DONE, clear iTxPending. Do not re-trigger REQ until the next TRCV.NDR has been consumed.

In SCL this is implemented as a small CASE-of-INT state machine. The same logic in ladder is a chain of mutually exclusive rungs gated by an EQ on the state tag.

SCL Skeleton (TIA Portal V16+)

// Persistent tags (retain not required)
iState         : INT  := 0;   // 0=Idle, 10=WaitTcon, 20=WaitRx, 30=Compare, 40=SendAck, 50=WaitSendDone
iConnEstablished : BOOL;
iRxComplete    : BOOL;
iTxPending     : BOOL;
iCompareOk     : BOOL;

CASE iState OF
  0:  // Idle: start connection
      "dbTCON".REQ := TRUE;            // call TCON in DB instance
      iState := 10;

 10:  // Wait for TCON done
      IF "dbTCON".DONE THEN
         iConnEstablished := TRUE;
         "dbTCON".REQ := FALSE;
         iState := 20;
      ELSIF "dbTCON".ERROR THEN
         iState := 90;                  // error handling
      END_IF;

 20:  // Enable TRCV, wait for NDR
      "dbTRCV".EN_R := TRUE;
      IF "dbTRCV".NDR THEN
         iRxComplete := TRUE;
         iState := 30;
      ELSIF "dbTRCV".ERROR THEN
         iState := 90;
      END_IF;

 30:  // Compare rx data with tx data
      iCompareOk := memcmp("dbRx".RxBuf, "dbTx".TxBuf, "dbTx".TxLen) = 0;
      IF iCompareOk THEN
         iState := 40;
      ELSE
         iState := 80;                  // mismatch path
      END_IF;

 40:  // Send ACK on the same connection
      "dbTSEND".REQ := FALSE;           // ensure clean edge
      iTxPending := TRUE;
      iState := 50;

 50:  // Rising edge of REQ, wait for DONE
      "dbTSEND".REQ := iTxPending AND NOT iTxPendingEdge;
      iTxPendingEdge := iTxPending;
      IF "dbTSEND".DONE THEN
         iTxPending := FALSE;
         "dbTSEND".REQ := FALSE;
         iRxComplete := FALSE;
         iState := 20;                  // back to waiting for next reply
      ELSIF "dbTSEND".ERROR THEN
         "dbTSEND".REQ := FALSE;
         iState := 90;
      END_IF;

 80:  // Mismatch handling (re-send, log, etc.)
      ;

 90:  // Error handling: capture STATUS, optionally call T_RESET / TDISCON
      iLastStatus := "dbTSEND".STATUS;
      ;

END_CASE;

The key invariants are:

  • TSEND.REQ is a single rising edge, never a sustained level. Reset REQ to FALSE in the same cycle that observes DONE.
  • A new transmit is never armed until TRCV.NDR has been observed for the current cycle and the data has been processed.
  • The status from the first cycle's TSEND.DONE is latched into iTxPending := FALSE only after the application has consumed the receive buffer.

Alternative: Use Multiple Connection IDs for Pipelining

If throughput is more important than simplicity, the application can open two (or more) connections to the partner on different local port numbers and use one TSEND / TRCV pair per connection ID. Each ID has its own job slot, so a second transaction on ID 2 can start while ID 1 is still mid-receive. This requires:

  • One TCON DB per connection, with distinct ID values and distinct LocalPort or RemotePort values.
  • Matching TSEND/TRCV instances wired to the correct ID.
  • Application-level multiplexing (sequence number) in the payload so that the partner can correlate replies.

This approach is described in the Siemens application example "Open User Communication with S7-1200 / S7-1500" (entry ID 109744691), available on the Siemens support portal.

Verification Procedure

  1. Watch table: monitor dbTCON.DONE, dbTSEND.DONE, dbTSEND.BUSY, dbTSEND.ERROR, dbTSEND.STATUS, and dbTRCV.NDR. Confirm that DONE pulses for one cycle, that BUSY returns to FALSE between jobs, and that STATUS = 16#0000 on completion.
  2. Online & Diagnostics: open the CPU's connection diagnostics. The connection should be in state ESTABLISHED, not LISTEN or CLOSED.
  3. Wireshark: capture on the PC side. Filter on tcp.port == 10001. Verify that each application cycle produces exactly one PSH/ACK frame in each direction, and that there are no RSTs (which indicate the firmware dropped the connection).
  4. Partner echo test: have the third-party device run a loop-back. Trigger two sends in succession from the PLC; both should appear as outgoing PSH/ACK frames.
  5. Stress test: drive 1000 transactions from the PLC at the maximum cycle rate. All DONE pulses must be observed and STATUS must never be non-zero.

Diagnostic Flow When the Symptom Returns

Symptom Likely STATUS Likely cause Action
First TSEND OK, second never completes 16#80A1 Overlapping job, REQ driven by level Add rising-edge generation on REQ
Done bit never set, BUSY always FALSE 16#8085 REQ was already TRUE when block was called Clear REQ in the cycle after DONE
One TSEND OK then status jumps to 16#80A3 16#80A3 Connection reset by partner Check partner timeout, call T_RESET then re-TCON
Intermittent timeouts on long frames 16#80C4 / 16#80C3 Local resource exhaustion Reduce number of simultaneous connections, increase cycle time
No data ever received 16#80A7 Partner not listening, firewall Verify partner server, disable Windows firewall for test
Truncated payload RCVD_LEN < expected TRCV.LEN too small or LEN=0 with TCP stream Pre-allocate full DB and set LEN = max expected

Best Practices for S7-1200 Open User Communication

  • Always use TSEND_C/TRCV_C for new projects. They include the connection setup and status query inside a single block, removing most sequencing errors.
  • Generate REQ with a rising-edge helper tag (REQ := Trigger AND NOT TriggerOld; TriggerOld := Trigger;) instead of latching the trigger.
  • Keep EN_R of TRCV permanently TRUE. Let the firmware handle the receive window; toggling EN_R will lose data.
  • Reserve a static receive DB of the maximum expected frame size. Re-using the same buffer for every cycle prevents stale-data faults.
  • Always latch the first non-zero STATUS observed in error state into a separate tag for commissioning. The error pulse is one cycle long and is easy to miss in online watch tables.
  • For raw TCP where the partner application expects a single transaction per cycle, do not pipeline by default. Add pipelining only after the deterministic path is proven.
  • On FW 4.2 and earlier, ensure that the partner IP is in the same subnet as the CPU's IP. Routing across gateways on S7-1200 requires the CPU to be configured as a router in TIA Portal (only available on newer FW and with security settings adjusted).

HyperTerminal Cross-Check (Why It Works on the PC)

When the same scenario is tested with a PC running HyperTerminal, the PC terminates the TCP connection as a generic socket: the application is free to call send() and recv() in any order, and the OS scheduler handles buffering. The PLC firmware, by contrast, exposes a strictly stateful job interface. The fact that HyperTerminal works is therefore evidence that the link layer is healthy, not that the PLC's state machine is correct.

Summary

A single TCON connection ID on the S7-1200 is fully bidirectional at the TCP level, but the user program must sequence each TSEND and TRCV explicitly. A single TSEND that is followed by a TRCV that is not awaited will leave the connection in a state where the next TSEND request is rejected with STATUS = 16#80A1 or 16#8085, and the DONE bit is never set. Drive REQ with a clean rising edge, gate each new transmit on the previous receive having produced NDR, and the connection remains stable for unlimited transactions.

Why does my S7-1200 TSEND not finish after the first TRCV cycle?

The TSEND REQ is being driven by a sustained level, or a new transmit is being armed before the previous TRCV has produced NDR. The firmware rejects the request with STATUS 16#80A1 ("Connection or channel is busy") or 16#8085 ("No new job was started"). Generate a single rising edge of REQ and gate the next send on TRCV.NDR.

Can I use the same TCON connection ID for both TSEND and TRCV?

Yes. A single TCON instance with one ID supports full-duplex TCP traffic, because TCP itself is full-duplex. As long as the application honours the job-state transitions of TSEND and TRCV, both can share the same ID. See the S7-1200 system manual (entry ID 109478121) for the official description.

What STATUS code means "connection busy" on TSEND/TRCV?

W#16#80A1 indicates that the connection or channel is currently processing another job. W#16#8085 means no new job was started because a job is already active. In both cases, lower REQ, wait for BUSY to return to FALSE, and re-trigger with a rising edge.

Should I use TSEND/TRCV or TSEND_C/TRCV_C on S7-1200?

For new projects use TSEND_C and TRCV_C. They encapsulate the TCON setup, the connection ID, and the data buffer in a single instance DB, which removes most of the state-machine mistakes that produce the stuck-TSEND symptom. TSEND/TRCV with a separate TCON DB is still supported and useful when the connection parameters are managed in a user-defined data block.

How can I see why TSEND is failing if DONE never pulses?

The ERROR bit is also a one-cycle pulse. Add a latch that stores STATUS into a static tag the cycle ERROR rises: IF "dbTSEND".ERROR THEN iLastStatus := "dbTSEND".STATUS; END_IF. The captured value is one of the codes in the STATUS table above and points directly to the cause (overlap, RST, refused, resource).

Back to blog