S7-1200 TCON/TRCV Resolving Serial Message Framing via Moxa NPort

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

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.

Scope of this article: Open User Communication (OUC) on the S7-1200 with TCP. S7-1500 uses a different block set (TSEND_C, TRCV_C) with integrated connection management; the framing principle is identical but the block parameters differ.

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:

  1. If you call TSEND three times in quick succession, all three responses can arrive in a single TRCV call as one contiguous block.
  2. 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.
  3. 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.

Why this works: When the Moxa is configured for delimiter-based packeting, the Moxa buffers the serial bytes until the delimiter arrives, then sends the whole assembled frame in a single TCP write. The S7-1200's TRCV will then typically receive the entire message in one call (Nagle's algorithm on the Moxa side should be disabled for low latency). The visible data in the DB will be one frame per NDR pulse, matching the Realterm dataspy output exactly.

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

IDLE (0) TX (10) RX (20) PARSE (30) WAIT (40) ERROR REQ DONE NDR ERROR TERM FOUND TIMER NO TERM FAULT

5.4 Commissioning Notes for the SCL Parser

  • Use 16#0D explicitly; 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 abRxAccum with 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:

  1. Open the project in TIA Portal and connect online to the S7-1214.
  2. Create a Watch Table that contains the abRxAccum array.
  3. Set the display format of abRxAccum to Hex.
  4. Trigger one of the four commands from the online force table.
  5. 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.

Optimized block access: Disable on the receive DB. Watch tables and HMI connections can read optimized blocks but cannot decode SCL strings as raw byte arrays. The S7-1200 Easy Book explicitly recommends standard access for any data block that will be referenced by both SCL and external HMI/OPC clients. Watch tables also show slice offsets in the "Offset" column only for standard-access blocks.

9. Verification Checklist

Run these checks after implementing Strategy 1, 2, or 3:

  1. TCON.DONE = TRUE within 3 seconds of startup; TCON.ERROR = 0.
  2. TSEND.DONE = TRUE within 100 ms of REQ pulse; TSEND.ERROR = 0.
  3. TRCV.NDR pulses once per command, with RCVD_LEN equal to the expected frame size ± 1 byte.
  4. The bMsgReady one-shot fires exactly once per command, and the parsed STRING contains only ASCII characters in the printable range (0x20..0x7E) plus the terminator.
  5. Four sequential commands cycle the state machine 0 → 10 → 20 → 30 → 40 → 10 in steady state, with iRxFill returning to 0 between messages.
  6. Force a 2-second sensor-side silence mid-test; the state machine must time out and return to IDLE without corrupting the next frame.
  7. 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.
  8. Capture a TIA Portal trace of iState, RCVD_LEN, and bMsgReady over 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.

Back to blog