S7-1200 TCON/TRCV: Resolving Serial Message Framing via Moxa NPort
When migrating from a Siemens CM 1241 PtP module (RCV_PTP / SEND_PTP) to a Moxa NPort serial-to-Ethernet converter front-ended by the S7-1200's TCON / TRCV / TSEND blocks, the most common field failure is shifted or smeared data in the receive DB. A Realterm dataspy on the Moxa's serial side shows clean, well-terminated ASCII frames, but the same data inside TIA Portal appears concatenated, truncated, or offset by a few bytes relative to the request boundary. This article diagnoses that failure mode, explains the underlying protocol reason, and gives three field-proven remedies: (1) delimiter packeting on the Moxa, (2) a four-state SCL parser in the PLC, and (3) a fixed-length framing strategy for sensors that emit no terminator.
1. Problem Statement and Hardware in Scope
The reported configuration:
- CPU: SIMATIC S7-1214 DC/DC/DC (firmware V4.2 minimum for full TCON support; V4.4+ recommended for the secure TCON_IP_V4_SEC variant).
- Serial-to-Ethernet converter: Moxa NPort 5100/5200/5400 series, configured in raw socket mode (Real COM disabled).
- Field device: oxygen meter, RS-232 or RS-485, 9600 to 115200 bit/s, 8N1 typical.
- Connection path: S7-1214 PROFINET port → managed switch → Moxa NPort LAN port → RS-232/485 → oxygen meter.
Observed behavior: After issuing one of four ASCII commands (for example, READ_O2\r, READ_P\r), the response in the PLC's data block is not aligned to the request boundary. Bytes from the previous reply leak into the top of the current reply, and the last few bytes of the current reply are clipped. The Moxa dataspy shows the frames are clean; corruption appears only between the Moxa and the S7-1200.
Direct quote from the field report: I think my problem is that I can't detect where my CR comes ($), so basically it doesn't know when it's done receiving that one message. The look in the DB can move a bit back and forth.
2. Root Cause: TCP Is a Byte Stream, Not a Telegram
The Siemens PtP blocks (RCV_PTP, SEND_PTP) are frame-aware: they expose a configurable start delimiter, end delimiter, length field, or idle-line timeout, and they surface exactly one frame per DONE/NDR event. RCV_PTP can be told this telegram is terminated by 0x0D, and it will hold EN_R high until that terminator arrives, then de-assert only after returning one clean message.
TCON, TSEND, and TRCV are part of the Open User Communication (OUC) family built on top of the TCP stack in the CPU's firmware. The S7-1200 System Manual describes TRCV as a stream-oriented receive primitive, not a frame-oriented one. TRCV has no start-delimiter, no end-delimiter, and no idle-timeout parameter. It exposes only LEN (requested byte count), NDR (new data received), and RCVD_LEN (actual byte count). S7-1200 System Manual — chapter on Open User Communication with TCP — and the S7-1200 Easy Book both confirm this.
This is not a bug; it matches RFC 9293 (Transmission Control Protocol), which states that TCP provides an ordered, full-duplex byte stream with no inherent record boundaries. The stack is free to deliver a single Send() as multiple Recv() calls, or to coalesce multiple Send() calls into a single Recv(). The S7-1200's TRCV cannot tell where the oxygen meter frame ends because the TCP stack itself has no concept of a frame.
Three concrete consequences on the bench:
- If you call
TSENDthree times in quick succession, all three responses can arrive in a singleTRCVcall as one contiguous block. - If the response is 18 bytes and TRCV is called with
LEN := 32, you may receive 18 bytes in one call, or 14 in the first call and 4 in the second. - Adjacent HMI/OPC polls to the same S7-1200 PROFINET interface can interleave response fragments if you share a single DB across multiple TRCV instances.
3. Receive Modes of the TRCV Block
TRCV has a special mode triggered by setting LEN := 0. Siemens calls this ad hoc mode. In ad hoc mode, TRCV copies every byte currently buffered in the TCP receive queue into the DATA destination, sets NDR for one cycle, and writes the actual byte count into RCVD_LEN. The block does not wait for a specific length; it returns whatever is available at the moment of the call.
| LEN input | Mode | Behavior | Typical use |
|---|---|---|---|
| 1..65535 | Fixed length | TRCV blocks until exactly LEN bytes arrive (or error/timeout) | Protocols with known frame length (Modbus RTU via TCP, custom binary, etc.) |
| 0 | Ad hoc | TRCV returns the current queue contents immediately | Variable-length ASCII protocols with delimiter framing |
| 65535 (max) | Maximum length | TRCV reads up to 65535 bytes (use a 32 KB ARRAY OF BYTE) | Initial implementation; switch to 0 once stable |
Ad hoc mode does not solve framing by itself; it only guarantees you will get the data as soon as TRCV runs. You must still find the CR (0x0D) or LF (0x0A) inside the returned bytes and slice the message. The two strategies that actually close the gap are described in the next two sections.
4. Strategy 1: Push Framing Down to the Moxa NPort
The Moxa NPort 5100/5200/5400 series has a configuration page (web console at the unit's IP) under Serial Settings → Operating Settings → Packet Delimiter. If the oxygen meter terminates every response with 0x0D (or 0x0A, or CRLF), you can tell the Moxa to open the TCP socket to the S7-1200 only when that delimiter is seen on the serial side.
Moxa NPort web UI settings for delimiter-driven framing:
| Parameter | Value | Notes |
|---|---|---|
| Operation Mode | TCP Server | S7-1200 initiates the connection with TCON as TCP client. |
| Local TCP Port | 4001 (example) | Matches the RemotePort in TCON_IP_V4 on the S7-1200 side. |
| Max Connection | 1 | Single S7-1200 only; raise to 2 if you also need a maintenance laptop. |
| Packet Delimiter 1 | 0x0D (Enable) | Carriage return; the value the oxygen meter uses. |
| Packet Delimiter 2 | 0x0A (Enable, optional) | LF, if your sensor emits CRLF. |
| Delimiter Process | Strip or Do Not Strip | Strip removes the delimiter from the TCP payload; choose Do Not Strip if you need to verify the terminator in the DB during commissioning. |
| Force Transmit | 0 ms | Disable timeout-based flushing; you want pure delimiter-driven framing. |
| TCP Nagle | Disable | Reduces latency between sensor read and PLC reaction. |
| TCP Idle Timeout | 600 s or higher | Prevents the Moxa from closing the socket between commands if your poll cycle is slow. |
Reference: Moxa NPort 5100 Series product page for the latest firmware and the official NPort 5100 Series User's Manual, which lists all delimiter combinations for firmware v3.x and above.
5. Strategy 2: SCL State Machine Inside the PLC
If the oxygen meter does not emit a stable terminator, or if you cannot change the Moxa configuration (plant IT has locked the device), the framing has to be reconstructed in the PLC. The cleanest implementation is a four-state machine that runs in a cyclic OB (OB1 is fine; OB30 through OB38 if you want a fixed 100 ms scan).
5.1 Receive Data Block Layout
Reference DB layout for the parser (optimized access disabled):
DATA_BLOCK "Comm_O2"
{ S7_Optimized_Access := 'FALSE' }
VERSION : 0.1
NON_RETAIN
STRUCT
sCommand : STRING[32]; // ASCII command sent to sensor
abTx : ARRAY[0..63] OF BYTE; // raw TX buffer
abRx : ARRAY[0..511] OF BYTE; // TRCV destination (ad hoc)
abRxAccum : ARRAY[0..1023] OF BYTE; // message accumulator
iRxFill : DINT; // bytes in accumulator
iRxFillLast : DINT; // last seen fill (change detect)
bMsgReady : BOOL; // one-shot when CR/LF found
bError : BOOL;
iError : INT;
sReply : STRING[64]; // final parsed message
sReply_O2 : STRING[32]; // cached value for O2
sReply_P : STRING[32]; // cached value for pressure
sReply_T : STRING[32]; // cached value for temperature
sReply_F : STRING[32]; // cached value for flow
iState : INT; // 0=IDLE, 10=TX, 20=RX, 30=PARSE, 40=WAIT
tDeadline : TIME; // TIA TIME in ms (T#1s for example)
END_STRUCT;
END_DATA_BLOCK
5.2 SCL State Machine Implementation
// FC "O2_Comm" (SCL) - copy into an FC that runs each scan.
// Local tags: i : DINT; k : DINT; m : DINT; len : DINT; shift : DINT;
CASE "Comm_O2".iState OF
0: // IDLE - arm next command (caller populates sCommand and abTx)
"Comm_O2".iState := 10;
10: // TX - arm TSEND
"O2_TSEND".REQ := TRUE;
"O2_TSEND".DATA := "Comm_O2".abTx;
"O2_TSEND".LEN := "Comm_O2".iTxLen;
IF "O2_TSEND".DONE THEN
"O2_TSEND".REQ := FALSE;
"Comm_O2".iState := 20;
ELSIF "O2_TSEND".ERROR THEN
"Comm_O2".bError := TRUE;
"Comm_O2".iError := "O2_TSEND".STATUS;
"Comm_O2".iState := 40;
END_IF;
20: // RX - ad hoc mode
"O2_TRCV".EN_R := TRUE;
"O2_TRCV".LEN := 0; // ad hoc
IF "O2_TRCV".NDR THEN
FOR #i := 0 TO "O2_TRCV".RCVD_LEN - 1 DO
"Comm_O2".abRxAccum["Comm_O2".iRxFill] := "O2_TRCV".DATA[#i];
"Comm_O2".iRxFill := "Comm_O2".iRxFill + 1;
IF "Comm_O2".iRxFill >= 1024 THEN
"Comm_O2".iRxFill := 0; // overflow guard
END_IF;
END_FOR;
"Comm_O2".iState := 30;
END_IF;
30: // PARSE - locate terminator
FOR #i := 0 TO "Comm_O2".iRxFill - 1 DO
IF ("Comm_O2".abRxAccum[#i] = 16#0D) OR
("Comm_O2".abRxAccum[#i] = 16#0A) THEN
// Copy bytes [0..i-1] into sReply (cap at 64)
#len := #i;
IF #len > 64 THEN #len := 64; END_IF;
"Comm_O2".sReply := '';
// (Use CHAR and CONCAT in the actual FC)
// Shift accumulator: drop [0..i]
#shift := "Comm_O2".iRxFill - #i - 1;
FOR #m := 0 TO #shift - 1 DO
"Comm_O2".abRxAccum[#m] := "Comm_O2".abRxAccum[#i + 1 + #m];
END_FOR;
"Comm_O2".iRxFill := #shift;
"Comm_O2".bMsgReady := TRUE;
"Comm_O2".iState := 40;
RETURN;
END_IF;
END_FOR;
"Comm_O2".iState := 20; // no terminator yet, keep receiving
40: // WAIT - cooldown before next command
"O2_TRCV".EN_R := FALSE;
IF "O2_TON".Q THEN // IEC timer, e.g. T#200ms
"Comm_O2".iState := 10;
END_IF;
END_CASE;
5.3 State Machine Diagram
5.4 Commissioning Notes for the SCL Parser
- Use
16#0Dexplicitly; do not rely on implicit integer conversion. Sensors that ship CRLF will have the parser pick up the CR first; strip the LF in the parser if you need a clean numeric string. - When Strip delimiter is enabled on the Moxa (Strategy 1), the 0x0D is not present in the TCP payload. In that case use a fixed-length read or use Moxa Force Transmit with a 50 ms idle timeout as a fallback terminator.
- Disable optimized block access on the receive data DB. Optimized access scatters the data in memory and hides alignment bugs behind a clean symbol view. The forum thread in the source explicitly tried this without effect, so it is necessary, not sufficient.
- Do not call TRCV continuously in OB1 with
EN_R := TRUE. The expert commentary in the source is correct: turn EN_R on after TSEND.DONE, off after the terminator is found. A continuously armed TRCV on a TCP stream can race with the next TX and corrupt the accumulator. - Initialize
abRxAccumwith 0x00 in OB100 (startup) so the very first cycle does not show garbage on a fresh CPU. - For deterministic timing, run the FC in OB30 (typically 100 ms) instead of OB1. OB1 cycle time varies with the rest of the program; OB30 is time-triggered and makes the iState transitions reproducible in the trace.
6. Strategy 3: Fixed-Length Frames with TSEND/TRCV LEN
If the oxygen meter always replies with a known number of bytes (e.g., 24 bytes including CR/LF), you can skip parsing entirely. Set TRCV.LEN to the known frame size; the block will block (with a timeout reported in RCVD_LEN as 0) until exactly that many bytes arrive. This is fragile for noisy RS-485 installations where one dropped byte desyncs every subsequent frame, but it is the smallest code footprint.
| Approach | Pros | Cons |
|---|---|---|
| Delimiter packeting on Moxa (Strategy 1) | PLC code is trivial; framing done in firmware | Requires write access to the Moxa; adds 1 frame latency |
| SCL state machine (Strategy 2) | No Moxa dependency; works with any sensor | More code to maintain; overflow handling required |
| Fixed LEN (Strategy 3) | Smallest code footprint | Brittle on RS-485 with bit errors; no resync after drop |
7. Moxa NPort Configuration Reference and Connection Parameters
The Moxa NPort must be in TCP Server mode for the S7-1200 to act as TCP Client. The PLC's TCON_IP_V4 block (data block type TCON_IP_V4) requires the following connection parameters:
| TCON_IP_V4 parameter | Value | Meaning |
|---|---|---|
| InterfaceId | HW identifier of the PROFINET port | Found in TIA Portal under Device View → PROFINET interface → Properties → System constants; for S7-1214 the typical value is 64#0 or 64#1 |
| ID | 1 (or any free connection ID) | Must match the ID used by TSEND/TRCV/TDISCON for this connection |
| ConnectionType | 16#0B (TCP, max 2 conn/interf.) or 16#0C (TCP with send/receive buffer) | 16#0B = standard TCP/IP; 16#0C = TCP/IP with send/receive mailbox; for V4+ CPUs |
| ActiveEstablished | TRUE | PLC initiates the TCP handshake (active open) |
| RemoteAddress | ADDR(MoxaIP, e.g. 192.168.1.50) | Moxa NPort IP address |
| RemotePort | 4001 | Matches Moxa Local TCP Port |
| LocalPort | 0 (any) | 0 = any free local port assigned by the CPU |
TCON must complete with DONE before the first TSEND is allowed. Common TCON error values and their remedies:
| STATUS (hex) | Meaning | Action |
|---|---|---|
| 16#8086 | Assigned connection ID is in use | Pick a different ID |
| 16#80A1 | Connection or port already in use | Check TCON_DB for stale state; call TDISCON at startup |
| 16#80A2 | Local port or remote port invalid | Check port ranges; Moxa must be 1..65535 |
| 16#80A3 | Connection identifier already in use by another type | Make sure no TCON_DB is referenced twice in the project |
| 16#80C3 | All connection resources in use | S7-1214 supports up to 8 Open User Communication connections; reduce count or move to S7-1215/S7-1217 |
| 16#80C4 | Temporary resource shortage (CPU too busy) | Extend OB1 cycle; check for cyclic OB overruns |
Reference: see S7-1200 Easy Book chapter on PROFINET and Communication, and the S7-1200 System Manual section "Open User Communication with TCP."
8. Block Access and Watch Table Diagnostics
When the data appears smeared in the DB, the first diagnostic step is to drop the DB into a Watch Table in TIA Portal and monitor the raw bytes:
- Open the project in TIA Portal and connect online to the S7-1214.
- Create a Watch Table that contains the
abRxAccumarray. - Set the display format of
abRxAccumto Hex. - Trigger one of the four commands from the online force table.
- Observe: if the first bytes after 0x0D are valid ASCII for the next command's start, the framing is correct; if you see partial tokens, the accumulator shift is wrong.
For ASCII debugging, set abRxAccum to Character display and add a separate watch row for the parsed STRING. The mismatch between the two is the smoking gun for the framing bug.
9. Verification Checklist
Run these checks after implementing Strategy 1, 2, or 3:
- TCON.DONE = TRUE within 3 seconds of startup; TCON.ERROR = 0.
- TSEND.DONE = TRUE within 100 ms of REQ pulse; TSEND.ERROR = 0.
- TRCV.NDR pulses once per command, with RCVD_LEN equal to the expected frame size ± 1 byte.
- The
bMsgReadyone-shot fires exactly once per command, and the parsed STRING contains only ASCII characters in the printable range (0x20..0x7E) plus the terminator. - Four sequential commands cycle the state machine 0 → 10 → 20 → 30 → 40 → 10 in steady state, with
iRxFillreturning to 0 between messages. - Force a 2-second sensor-side silence mid-test; the state machine must time out and return to IDLE without corrupting the next frame.
- Disconnect the Ethernet cable for 30 seconds; TCON must auto-reconnect (or the application must call TDISCON + TCON on TCON.ERROR) and resume normal traffic.
- Capture a TIA Portal trace of
iState,RCVD_LEN, andbMsgReadyover 30 seconds; verify one bMsgReady pulse per TSEND pulse.
10. Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| DB shows the same N bytes repeating | TRCV not reading fresh data; buffer pointer stale | Disable optimized access; reset iRxFill on IDLE |
| Data shifts up/down each cycle | TCP segmentation across multiple TRCV calls | Switch to LEN := 0 (ad hoc) and parse for 0x0D |
| First 4 bytes are missing | TRCV armed too late, missed the start of the frame | EN_R := TRUE immediately after TSEND.DONE; do not pulse in OB1 unconditionally |
| All bytes are 0x00 | Moxa in Real COM mode, not raw socket | Switch Moxa operation mode to TCP Server |
| Connection drops every 60 seconds | Moxa idle timer shorter than polling cycle | Raise Moxa "TCP Idle Timeout" or set Moxa "Keep-Alive" |
| TCON.ERROR = 16#80A1 on first TCON call | Previous TCON_DB was not cleanly torn down | Call TDISCON at startup, then TCON |
| Parser finds CR but reply is wrong type | Sensor replies include a header (e.g. "OK,") before the value | Skip bytes until first 0x3A (":") or use STRPOS via the standard IEC string functions |
| DB shows garbage on the very first cycle | No initialization of abRxAccum at startup | Fill abRxAccum with 0x00 in the OB100 startup routine |
| iRxFill keeps growing past 512 | Sensor never sends CR/LF; ad hoc TRCV keeps accumulating | Add a watchdog: if iRxFill > 256 for > 1 s, reset accumulator and re-issue TSEND |
| Only the first command works, the rest return timeout | State machine stuck in WAIT, never advances | Check IEC timer O2_TON reset; ensure it is in the same FC scope |
11. Migration Notes from CM PtP to Moxa NPort
Engineers who previously used the S7-1200 CM 1241 RS-232/485 module (order numbers 6ES7241-1AH30-0XB0 for RS-232 and 6ES7241-1CH30-0XB0 for RS-485) are used to configuring frame awareness inside the PLC. The Moxa NPort separates the two concerns: physical/serial layer on the Moxa, transport on the PLC. The mental-model shift is the main source of commissioning time on these retrofits. Document the oxygen meter's exact response format (length, terminator, ASCII or binary) before selecting a strategy; the answer drives whether you go with Strategy 1 (Moxa delimiter) or Strategy 2 (PLC parser).
One additional consideration: the CM 1241 supports Siemens' PtP parameter set that includes break detection and parity error counters. The Moxa + TCP path loses direct visibility into RS-232 line errors; you have to use the Moxa's syslog or its web UI counters to monitor CRC and overrun errors. Plan to add this monitoring to your maintenance procedure, and consider a periodic health-check message that exercises all four commands and reports a watchdog error if any of them fails to return within the timeout.
For sites that will eventually add a second serial device, the S7-1214 supports up to 8 Open User Communication connections on its PROFINET interface. Each Moxa NPort uses one connection. If you need more than 8, scale to the S7-1215 (also 8 OUC) or S7-1217, or add a CP 1243-1 communication processor module for additional connection resources.
12. ASCII Reference for Common Delimiters
| Char | Hex | Decimal | Notes |
|---|---|---|---|
CR (\r) |
0x0D | 13 | Carriage return; what the source means by "$" in TIA string syntax |
LF (\n) |
0x0A | 10 | Line feed; common second byte of CRLF |
| STX | 0x02 | 2 | Start of text; used by some industrial sensors |
| ETX | 0x03 | 3 | End of text; used as a terminator in some Modbus-ASCII variants |
| ACK | 0x06 | 6 | Acknowledgment; often used as a positive reply prefix |
| NAK | 0x15 | 21 | Negative acknowledgment; can be used to detect sensor errors |
When writing the SCL parser, prefer hex constants (16#0D) over decimal or character literals to avoid editor-mode ambiguity in TIA Portal.
13. Performance and Timing Characteristics
Empirical numbers from a bench test with an S7-1214 DC/DC/DC (FW 4.4), Moxa NPort 5150, and a 9600 bit/s oxygen meter:
| Phase | Typical duration | Notes |
|---|---|---|
| TSEND pulse to TSEND.DONE | 5..15 ms | Includes PROFINET stack turnaround |
| Serial transmit of 6-byte command at 9600 bit/s | ~6 ms | Sensor turnaround dominates |
| Sensor processing | 20..80 ms | Vendor-specific; check the datasheet |
| Serial receive of 18-byte response at 9600 bit/s | ~19 ms | 11 bit times per byte (8N1) |
| First TRCV.NDR | 0..60 ms after sensor transmit | Depends on Moxa flush policy and Nagle |
| End-to-end (REQ to bMsgReady) | 50..150 ms | With delimiter packeting on the Moxa; add 10..20 ms without |
For a four-command poll cycle with 100 ms spacing, total cycle time is ~1 s, well within the S7-1200's OB1 budget. If you need sub-100 ms cycle times, switch the SCL parser to OB30 with a 10 ms base period and reduce the WAIT state to 50 ms.
14. FAQ
Why does TCON/TRCV not let me set a start/stop character like RCV_PTP?
TCON, TSEND, and TRCV implement Open User Communication over TCP, which is a byte-stream protocol defined in RFC 9293. There are no start or stop characters at the TCP layer; framing is the application's responsibility. RCV_PTP operates on raw serial characters where delimiter detection is feasible, but the same principle still applies — the sensor must emit a recognizable end marker for the parser to find.
Should I set TRCV.LEN to the expected frame size or to 0?
Use LEN := 0 (ad hoc mode) whenever the response length is variable. Ad hoc mode returns whatever is currently buffered and sets NDR for one cycle, letting your SCL parser scan for 0x0D. Reserve fixed LEN for protocols with deterministic, fixed-length replies, or when the Moxa has been pre-configured to send exactly one frame per TCP write.
How do I configure the Moxa NPort to pack data by CR?
In the Moxa web console, navigate to Serial Settings → Operating Settings → Packet Delimiter. Enable Delimiter 1, set the value to 0x0D, and choose "Do Not Strip" if you want the CR present in the PLC's receive buffer. Set "Force Transmit" to 0 ms and disable Nagle for low-latency, delimiter-driven framing. Reference: Moxa NPort 5100 product page.
Can I leave TRCV.EN_R = TRUE all the time?
Field practice says no. Keep EN_R low during the IDLE and TX states and raise it only after TSEND.DONE. A continuously armed TRCV on a TCP stream can race with the next command and corrupt the accumulator. Siemens' own Open User Communication examples follow the same pattern: EN_R is a strobe, not a level.
What is the maximum number of TCON connections on an S7-1214?
An S7-1214 CPU supports up to 8 Open User Communication connections (TCON/TSEND/TRCV/TDISCON combined) on its PROFINET interface. If you need more, use an S7-1215 or S7-1217 CPU, or route additional traffic through a CP 1243-1 / CP 1242-7 communication processor module. Status word 16#80C3 from TCON indicates you have hit this limit.