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.
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:
-
Latched REQ with no edge:
REQofTSENDis wired to a tag that is already TRUE.TSENDonly accepts a rising edge ofREQ; a level-driven REQ does not restart a job. -
Overlapping jobs: A second
TSENDREQ is issued beforeTRCVhas signalledNDR(or beforeDONEfrom the previousTSENDhas been acknowledged). -
Connection descriptor corruption: The same connection ID is reused by two independent
TCONDBs, causing the firmware to swap descriptors on the second call. This is the only variant that requires separateTCONinstances.
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:
- Establish the connection once with
TCON. LatchTCON.DONEinto a static tag (iConnEstablished). - Enable
TRCVcontinuously withEN_R = TRUE. OnNDR, copy the buffer to the application DB and raiseiRxComplete. - Compare transmitted and received data. If they match, raise
iTxPendingand generate a single rising edge ofTSEND.REQ. - On
TSEND.DONE, cleariTxPending. Do not re-triggerREQuntil the nextTRCV.NDRhas 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.REQis a single rising edge, never a sustained level. ResetREQto FALSE in the same cycle that observesDONE. - A new transmit is never armed until
TRCV.NDRhas been observed for the current cycle and the data has been processed. - The status from the first cycle's
TSEND.DONEis latched intoiTxPending := FALSEonly 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
TCONDB per connection, with distinctIDvalues and distinctLocalPortorRemotePortvalues. - Matching
TSEND/TRCVinstances 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
- Watch table: monitor
dbTCON.DONE,dbTSEND.DONE,dbTSEND.BUSY,dbTSEND.ERROR,dbTSEND.STATUS, anddbTRCV.NDR. Confirm thatDONEpulses for one cycle, thatBUSYreturns to FALSE between jobs, and thatSTATUS=16#0000on completion. - Online & Diagnostics: open the CPU's connection diagnostics. The connection should be in state ESTABLISHED, not LISTEN or CLOSED.
- 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). - 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.
- Stress test: drive 1000 transactions from the PLC at the maximum cycle rate. All
DONEpulses must be observed andSTATUSmust 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_Cfor new projects. They include the connection setup and status query inside a single block, removing most sequencing errors. - Generate
REQwith a rising-edge helper tag (REQ := Trigger AND NOT TriggerOld; TriggerOld := Trigger;) instead of latching the trigger. - Keep
EN_RofTRCVpermanently TRUE. Let the firmware handle the receive window; togglingEN_Rwill 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
STATUSobserved 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).