Receiving Variable-Length Telegrams via FC60 AG_LRECV

David Krause19 min read
S7-300SiemensTutorial / How-to
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

Receiving Variable-Length Telegrams via FC60 AG_LRECV on S7-300/400

FC60 (AG_LRECV) is the SIMATIC NET block for receiving data from a configured Industrial Ethernet connection on a CP 343-1, CP 343-1 Advanced, CP 443-1, or CP 443-1 Advanced. Unlike AG_SEND, AG_LRECV returns the actual number of bytes received via its LEN output, but the destination area is always the slice described by the RECV ANY pointer. This article covers the patterns required to receive variable-length telegrams without the next frame bleeding into the destination DB, and explains the dynamic-ANY-pointer technique that lets you read the header in one OB cycle and the payload in the next.

1. Problem Statement

Many user-defined protocols over ISO-on-TCP, TCP, or UDP carry their length information inside the frame itself. A common shape is a static STX header, a 4-byte data-point count P, P data records of 4 bytes each, and a 1-byte ETX footer. The frame length therefore varies with the data, and the receiver must parse the header before it knows how many more bytes to fetch.

Offset (decimal) Size (bytes) Field Notes
0-15 16 STX header Static prefix, always present
16-23 8 Source / reserved Static
24-27 4 Data-point count P (DWORD) Variable; drives frame length
28 + 4*(N-1) ... 28 + 4*N - 1 4 Data set N: value 1 (2 bytes) + value 2 (2 bytes) P data sets of 4 bytes
28 + 4*P 1 ETX footer End marker, fixed 1 byte

Total frame length L = 28 + 4*P + 1 bytes. If the destination DB is sized for L_max and the actual frame is shorter, FC60 reports the real length through LEN, but the next call begins a fresh frame at the byte offset supplied in the ANY pointer. The challenge is that the frame length is not known until the header is read.

2. Prerequisites

  • STEP 7 V5.4 or V5.5 (or STEP 7 Professional in TIA Portal with the S7-300/400 compatible blocks)
  • SIMATIC NET NCM S7 V6.x or later installed. AG_LRECV/AG_SEND are shipped in the library SIMATIC_NET_CP -> CP 300/400 -> Blocks as FC50 (AG_SEND) and FC60 (AG_LRECV). Newer libraries provide FB12/FB13/FB14/FB15 equivalents with instance DBs.
  • CP 343-1, CP 343-1 Lean, CP 343-1 Advanced, CP 443-1, or CP 443-1 Advanced on Industrial Ethernet. The PROFINET interface of an S7-300 CPU 31x PN/DP is supported only via FB81/FB82, not FC60.
  • A configured connection in NetPro (or in TIA Portal under Devices & Networks): ISO-on-TCP, TCP, or UDP, with a unique connection ID and the partner configured for unsolicited or solicited reception as required.
  • Connection ID and the CP's logical base address (LADDR) known.
Hardware address convention: The LADDR is the I/O start address of the CP in the S7 I/O area. For CP 343-1 in a typical default slot, the diagnostic/status LADDR is W#16#3FFD, but the connection itself uses the connection ID. The ID parameter of AG_LRECV and the connection ID in NetPro must match exactly. Mismatched IDs cause STATUS = 16#80A2 on every call.

3. FC60 AG_LRECV Parameter Reference

Parameter Declaration Type Description
ID INPUT WORD Connection ID (1-16 for CP 343-1, 1-64 for CP 443-1)
LADDR INPUT WORD Logical base address of the CP
RECV IN_OUT ANY Destination area (DB, M, Q, I, L). The length encoded in the ANY defines the maximum receive buffer; the actual length received is returned in LEN.
NDR OUTPUT BOOL TRUE for one cycle when new data is accepted and placed in RECV
ERROR OUTPUT BOOL TRUE if an error occurred during the call
STATUS OUTPUT WORD Status code (see Section 9)
LEN OUTPUT WORD Number of bytes actually received

For the complete status code list, refer to the SIMATIC NET - NCM S7 function blocks (FC) manual on Siemens Industry Online Support. The same block interface and status codes apply whether the underlying connection is ISO-on-TCP (RFC 1006), TCP native, or UDP.

4. The Two-Call Anti-Pattern in One OB

A natural first attempt is to call FC60 twice inside one OB (OB1, OB35, OB36) - once for the header and once for the remaining payload - using two statically declared ANY pointers. This pattern fails because the underlying CP call is a single state-machine step: the CP returns the oldest pending frame on the connection to the first AG_LRECV that asks for it, and the second call within the same OB cycle finds no frame and reports NDR=0 / STATUS=16#8180. The first call's data is correctly placed, but the second call cannot retrieve the remainder of the same frame in the same cycle - the CP does not partition a frame across multiple AG_LRECV calls in one OB.

Field rule: In one OB cycle, AG_LRECV is a state-machine call on the CP. Place at most one AG_LRECV (and at most one AG_SEND) per connection per OB. If you need to read a frame in two pieces, split across two consecutive OB cycles.

This is also why the first call in the original two-call code returned LEN equal to the full frame length: the CP delivered the whole frame to the first valid call, not in slices. The CP does not split; it delivers the whole frame on the first call, and the second call is left looking at an empty buffer.

5. Recommended Pattern: One FC60 per Cycle, Dynamic ANY

The robust solution is to call FC60 exactly once per connection per OB cycle, but to build the RECV ANY pointer dynamically from the length parsed in the previous cycle.

  1. On the first call (when a "header-pending" flag is set), build an ANY that covers only the static header (bytes 0..27, length = 28). Call FC60. On NDR=1, read the data-point count P from offset 24..27.
  2. Compute the payload length: payload_len = 4 * P + 1 (the +1 is the ETX footer). Total frame length: total_len = 28 + payload_len.
  3. Store payload_len in a static word (e.g. MW 100). Keep the "header-pending" flag set.
  4. On the next OB cycle, build an ANY that starts at offset 28 of the same DB with length = payload_len. Call FC60 - it returns the remaining bytes of the same frame.
  5. When NDR=1 on the second call, clear the "header-pending" flag. The cycle is now back to step 1, ready for the next frame.

This is the canonical pattern documented in the SIMATIC NET function block manual: read the fixed prefix, evaluate the length, then continue with a tailored pointer. The same pattern works for any protocol where the length information is contained in the first N bytes of the frame (Modbus TCP PDU length, RFC 1006 header, OPC UA hello, etc.).

6. Building the Dynamic ANY Pointer

6.1 SCL Implementation

In SCL, the ANY data type is a first-class citizen. Declare a TEMP variable of type ANY and assign each field directly:

FUNCTION_BLOCK FB_BuildRecvAny
VAR_INPUT
    i_Len  : INT;      // length in bytes
    i_DB   : INT;      // target DB number
    i_Off  : DINT;     // byte offset within the DB
END_VAR
VAR_OUTPUT
    o_Any  : ANY;      // populated ANY pointer
END_VAR
BEGIN
    o_Any.SYNTAX_ID    := 16#10;          // S7-300/400 complete ANY
    o_Any.INTERNAL_USE := 0;
    o_Any.DATA_TYPE    := 16#02;          // BYTE
    o_Any.NUMBER       := i_Len;
    o_Any.DB_NUMBER    := i_DB;
    o_Any.MEMORY_AREA  := 16#84;          // DB area
    o_Any.BYTE_OFFSET  := i_Off;
END_FUNCTION_BLOCK

Call from the cyclic OB:

IF b_HeaderPhase THEN
    i_BuildLen := 28;
    i_BuildOff := 0;
ELSE
    i_BuildLen := INT_TO_WORD(w_PayloadLen);
    i_BuildOff := 28;
END_IF;

"i_DB700" := 700;
"FB_BuildRecvAny"(i_Len := i_BuildLen, i_DB := "i_DB700", i_Off := i_BuildOff, o_Any => t_RecvPtr);

CALL "AG_LRECV" / FC60
     ID     := 3
     LADDR  := W#16#3FFD
     RECV   := t_RecvPtr
     NDR    := b_NDR
     ERROR  := b_ERR
     STATUS := w_STS
     LEN    := w_LEN;

6.2 AWL / STL Implementation

In AWL, build the ANY manually using pointer arithmetic on a TEMP ANY. The ANY layout for S7-400 is 12 bytes (S7-300 uses 8 bytes for short ANY pointers):

ANY byte Field Value for this protocol
0 Syntax ID 16#10 (complete ANY)
1-2 Data type 16#0002 (BYTE)
3-4 Length / count payload length in bytes
5-6 DB number 700
7 Memory area 16#84 (DB area)
8-11 Byte.bit address offset expressed in bit format (offset * 8 = bit address)

An AWL snippet to mutate the length field and offset of an ANY stored in a TEMP variable:

// t_RecvPtr is a TEMP ANY (10 bytes in S7-400, 8 bytes in S7-300)
LAR1  P##t_RecvPtr              // AR1 -> ANY base
L     #i_Len
T     W [AR1,P#3.0]             // length in bytes at ANY offset 3..4
L     #i_DB
T     W [AR1,P#5.0]             // DB number at ANY offset 5..6
L     16#84
T     B [AR1,P#7.0]             // area = DB at ANY offset 7
// build the byte.bit address (bit offset in lower 3 bits)
L     #i_Off
SLD   3                          // *8 -> bit address
T     D [AR1,P#8.0]             // byte.bit address at ANY offset 8..11

For S7-300, the byte offset is stored at ANY bytes 6..7 (the ANY is only 8 bytes long). For S7-400 the layout above is correct. Always cross-check against the STEP 7 programming manual ANY pointer reference on Siemens Industry Online Support before relying on the layout in production code.

7. End-to-End AWL Example (OB35 / OB36 cycle)

// Static flags / words in the DB or M area
// "b_HeaderPhase"   BOOL  - TRUE = next call reads header (28 bytes)
// "w_PayloadLen"    WORD  - bytes after the header for the current frame
// "w_P"             WORD  - data-point count from header bytes 24..27
// "i_DB700"         INT   - 700

NETWORK 1 - Build the ANY for this cycle
A     "b_HeaderPhase"
JCN   PAY
L     28
T     #t_RecvLen
L     0
T     #t_RecvOff
JU    BUILD
PAY:  L     "w_PayloadLen"
      T     #t_RecvLen
      L     28
      T     #t_RecvOff
BUILD:LAR1  P##t_RecvPtr
      L     #t_RecvLen
      T     W [AR1,P#3.0]
      L     "i_DB700"
      T     W [AR1,P#5.0]
      L     16#84
      T     B [AR1,P#7.0]
      L     #t_RecvOff
      SLD   3
      T     D [AR1,P#8.0]

NETWORK 2 - Single AG_LRECV per cycle
CALL  "AG_LRECV" / FC60
      ID     := 3
      LADDR  := W#16#3FFD
      RECV   := #t_RecvPtr
      NDR    := "c_NEW_DATA"
      ERROR  := "c_ERROR"
      STATUS := "c_STATUS"
      LEN    := "c_LENGTH"
      NOP 0

NETWORK 3 - Interpret NDR
A     "c_NEW_DATA"
A     "b_HeaderPhase"
JCN   NO_HEADER
// First call succeeded - parse P and compute remaining length
L     DB700.DBD 24              // P as DWORD, big-endian on the wire
T     "w_P"
L     "w_P"
SLD   2                          // *4 (each data set is 4 bytes)
T     "w_PayloadLen"
L     "w_PayloadLen"
+     1                          // + ETX footer
T     "w_PayloadLen"
// b_HeaderPhase stays set; next cycle will read payload
JU    END_CALL
NO_HEADER:
A     "c_NEW_DATA"
R     "b_HeaderPhase"            // Payload call succeeded - ready for next frame
END_CALL: NOP 0

The next OB cycle, the first network re-evaluates b_HeaderPhase and selects the payload path; the rest of the frame is delivered into DB700 starting at byte 28. When the frame's payload has been read, NDR fires on the second call, the flag is cleared, and the next frame's header is read on the cycle after that.

8. Choosing the Cyclic OB

OB1 is acceptable if the frame rate is low (one frame per second or slower). For deterministic cycle times, use OB35 (default 100 ms; configurable in HW Config under the CPU properties -> Cyclic Interrupts) or OB36 (default 200 ms). The frame must arrive in full within one OB cycle - if the partner sends data at 10 Hz and you poll at 5 Hz, the CP buffers the data; the next call returns the oldest pending frame, not the newest. The newer the application logic, the higher the OB rate must be relative to the partner's send rate.

For TIA Portal users, the equivalent cyclic interrupt OB is the same OB35/OB36 of the S7-300/400 program; S7-1200/1500 uses OB30..OB38 with the same numbering convention. S7-1200/1500 do not use FC60 - they use the TCON / TSEND / TRCV / TURCV instructions (IEC 61131-3 standard).

Ordering: When the application contains both AG_SEND and AG_LRECV for the same connection, do not call SEND and RECV back-to-back in the same OB. The CP may report STATUS = 16#80A1 or 16#80A2 if you collide with the local send/receive handshake. Split SEND and RECV across two OBs, or insert a one-cycle delay between SEND and the corresponding RECV.

9. Status Codes and Troubleshooting

STATUS (hex) Meaning Remedy
0000 OK (no error) Continue
8180 Frame not yet received Normal; NDR will be set on the next call
8181 RECV length too small for received data Increase ANY length; check that LEN output is read before overwriting the DB
80A1 Connection / resource error on CP Check connection status in NetPro / diagnostic buffer of the CP
80A2 Parameter error - LADDR or ID Verify LADDR matches the CP base address; ID matches the NetPro connection
80A4 TCP / ISO-on-TCP error Check partner availability; verify port and TSAP
80A7 Receive buffer overflow on CP Increase OB poll rate, reduce partner send rate, or enlarge the CP receive buffer
80C0 Frame length 0 - partner closed connection Implement graceful close handling; re-establish the connection on the next cycle
80C1 Connection lost Reset the connection; implement a reconnect state machine

Always clear NDR / ERROR with a falling-edge evaluation to avoid acting on a stale flag. A common pattern is to copy the FC60 outputs into edge-detection flag words (e.g. M41.0 for NDR, M41.1 for ERROR) and trigger application logic only on the positive edge. Reading NDR / ERROR as a static bit will fire the same frame's logic on every OB cycle until the partner sends a new frame.

9.1 Troubleshooting Matrix

Symptom Likely cause Fix
Second FC60 in the same OB never sees NDR=1 Two-call anti-pattern: the CP drained the frame on the first call Move the second call to the next OB cycle; build a dynamic ANY pointer
First call returns LEN = full frame length even though the ANY says 28 Same as above - the CP delivers the whole frame on the first call, not the slice the ANY requested Build the dynamic ANY pointer as described in Section 5; ensure the ANY length field is correct
NDR = 1, but DB contains garbage / wrong byte order Endianness mismatch on the data-point count P Swap bytes with TAH/TAW/CAL as required; P is typically big-endian on the wire, S7 is little-endian internally
Residue of the previous (longer) frame is still in the DB DB is not zeroed between frames; new frame is placed at offset 0, but the previous frame's tail persists Clear the DB, or write only the bytes that arrived (use the LEN output), or use a ring buffer
STATUS 80A2 on every call ID / LADDR mismatch Verify NetPro connection ID matches the ID parameter; verify the CP LADDR
STATUS 8181 even though the ANY is sized for the full frame The ANY pointer is being overwritten by a second call; or the S7-300/400 ANY width is wrong (8 vs 12 bytes) Confirm the ANY structure matches the CPU type; rebuild in SCL to avoid format errors
STATUS 80A7 / lost frames under load OB cycle too slow for the partner's send rate Decrease OB35 interval, or send a frame-end semaphore from the partner

10. Verification and Commissioning

  1. With a static-length test frame (e.g. P=0, total length 29), confirm that DB700 bytes 0..28 contain the expected STX, P, and ETX values. Use a VAT table on the online view of DB700.
  2. Connect a raw TCP test tool to the partner port. Send a frame with P=2 (length 37 bytes) and confirm DB700 bytes 28..36 contain the data records.
  3. Send P=10 (length 69) and P=1 (length 33) interleaved; verify that the next frame's STX overwrites byte 0 of DB700 (i.e. no residue from the prior frame's longer tail). The simplest check is to write a known 16-byte STX pattern and confirm the first 16 bytes always match after a frame is fully received.
  4. Disconnect the partner; STATUS should transition to 16#80C1 and the application should suppress DB writes until the connection re-establishes.
  5. Force a cycle overrun by sending a frame larger than the OB35 interval can complete; confirm that the second-call path also fires its NDR within two OB cycles and the application returns to the header-phase flag.
  6. Stress-test: send 1000 frames of varying length and verify the application has consumed all of them with no loss. A missing NDR will show as a frame count gap in your test harness.

11. Edge Cases and Field-Proven Caveats

11.1 Maximum ANY length per call

AG_LRECV has a per-call limit. On CP 343-1 and CP 343-1 Advanced, the maximum is 8192 bytes per call. For larger frames, the partner must respect the negotiated receive window, or you must read the frame in multiple segments (still one AG_LRECV per OB, with a moving offset and the buffer length split into chunks of <= 8192 bytes). CP 443-1 has a larger limit, but the per-call ceiling is documented per CP firmware version - always check the relevant CP manual on Siemens Industry Online Support before commissioning.

11.2 Endianness

The data-point count P (bytes 24..27) is encoded in the partner's byte order. S7 is little-endian internally; on the wire, the ISO-on-TCP layer preserves the partner's byte order. If the partner is a PC sending big-endian, swap bytes with TAH/TAW/CAL as required when reading the DWORD, or use the SFC swaps in a standardized helper FB.

11.3 Multiple connections on one CP

With multiple connections on one CP, give each its own pair of header/payload state words and its own TEMP ANY. Sharing a TEMP ANY across two connections is a classic source of cross-talk - the second connection's call sees the first connection's pointer and writes to the wrong DB.

11.4 S7-400 ANY width

S7-400 expects a 12-byte ANY (including the byte.bit address in the bottom three bits). Building an S7-300-style 8-byte ANY on an S7-400 CPU will be rejected with STATUS 16#80A2. If you are migrating an S7-300 program to S7-400, audit every dynamic-ANY construction site.

11.5 Library version

Pin the SIMATIC NET NCM S7 library version. On older library versions (V5.x and earlier), AG_LRECV and AG_SEND are FC60/FC50. On newer libraries (V6.0 and later, especially as of NCM S7 V5.5 + SPx), Siemens ships them as FB13/FB12/FB14/FB15 with a paired instance DB. The patterns above still apply, but the call interface moves from FC to FB with IDB management - and you must call each FB with its own instance DB per connection.

11.6 Reconnect handling

When the partner drops the connection, AG_LRECV returns STATUS = 16#80C1. On reconnect, the CP does not replay the buffered frames - the new connection is logically fresh. Implement a "discard partial frame" rule: if the connection drops while b_HeaderPhase is set, clear the flag on the next NDR; otherwise the next frame will be misinterpreted because the leftover header state is stale.

11.7 Migration to TIA Portal

For TIA Portal, AG_LRECV is still available in the SIMATIC NET program blocks catalog (as AG_LRECV / FB12 or the FB113/FB114/FB115 variants). The SCL implementation of Section 6.1 works unchanged. For S7-1200/1500, use the TCON / TSEND / TRCV / TURCV instructions (IEC 61131-3 standard) - AG_LRECV is not available on S7-1200/1500 CPUs.

12. Connection Configuration in NetPro

The connection must be configured in NetPro (or in TIA Portal under Devices & Networks -> Connections) with the correct attributes. The relevant fields are:

Field Value for this protocol
Connection type ISO-on-TCP (recommended), TCP, or UDP
Connection ID 1-16 (CP 343-1) or 1-64 (CP 443-1); must match the ID parameter of AG_LRECV
Local TSAP / port Any free TSAP or TCP port; document in the project's network plan
Partner TSAP / port The TSAP / port of the sending device
Active connection establishment Yes, if the S7 is the TCP client; No, if the S7 is the server (partner is the client)
Operating mode Full duplex for ISO-on-TCP; specify Send/receive email only if the connection is for SMTP/email

For ISO-on-TCP connections, the TSAP is a string of 2-16 ASCII characters encoded as hex (each character is one byte of the TSAP). For TCP and UDP, the port is a 16-bit number. Mismatched TSAPs are the most common reason for STATUS 16#80A4 in the field.

13. Diagnostic Aids

  • Online -> Accessible Nodes: Confirm the CP is reachable and its firmware version is current. Out-of-date CP firmware can change the behavior of AG_LRECV - always update to the latest service pack listed in the CP firmware download area on Siemens Industry Online Support.
  • CP diagnostic buffer: Read with the CP's online diagnostics in STEP 7. Look for connection-establishment errors, resource warnings, and any frame-too-large events.
  • SFC51 (RDSYSST): Read the CP's partial system state to confirm the connection state machine. SFC51 subsystem ID 1, ID 1 gives the CPU's module status; subsystem ID 0x0131 / ID 0x0000 gives the CP's connection list (where supported).
  • SFC87 (C_DIAG): Read the diagnostic data record of the CP for the connection in question. On older STEP 7 versions, SFC59 (RD_REC) is used instead.
  • SFB52 (RDREC) / SFB53 (WRREC) / SFB54 (RALRM): Available on S7-400 and S7-300 PN/DP CPUs, used to read/write diagnostic records and receive interrupts from the CP. Confirm the CP supports the record number you intend to use.

14. Sample Partner-Side Framing Pseudocode

For reference, the partner (e.g. a C# / Python application) typically emits the frame as:

// Pseudocode for the partner (big-endian PC) sending a frame with P=2
byte[] stx   = new byte[16];                       // 16-byte STX, filled with 0x02
byte[] info  = new byte[8];                        // 16..23 reserved
uint   p     = 2;                                   // 2 data sets
byte[] data  = new byte[4 * p];                    // 4 bytes per data set
byte   etx   = 0x03;                                // ETX footer

byte[] frame = new byte[28 + 4 * p + 1];
Array.Copy(stx,  0, frame, 0,  16);
Array.Copy(info, 0, frame, 16, 8);
frame[24] = (byte)(p >> 24);                       // big-endian P at offset 24..27
frame[25] = (byte)(p >> 16);
frame[26] = (byte)(p >> 8);
frame[27] = (byte)(p & 0xFF);
Array.Copy(data, 0, frame, 28, 4 * p);
frame[28 + 4 * p] = etx;
socket.send(frame);

The S7 reads bytes 24..27 of the received frame as a DWORD. Because the partner is big-endian, the S7 must either swap the bytes (TAH + TAW + CAL sequence) or treat the field as a 4-byte array and assemble it manually. The cleanest fix is to define the protocol as little-endian at both ends so the S7 can read the DWORD directly.

15. FAQ

Why does my second FC60 call never see NDR=1?

Because AG_LRECV is a single state-machine step per connection per OB cycle. A second call within the same cycle does not re-read the same frame; the CP has already drained it to the first call's RECV area. Always split header and payload across two consecutive OB cycles, with a single AG_LRECV per cycle.

How do I declare a dynamic ANY pointer in SCL?

Declare a TEMP variable of type ANY and assign its fields directly: SYNTAX_ID:=16#10, DATA_TYPE:=16#02 (BYTE), NUMBER:=length_in_bytes, DB_NUMBER:=target_DB, MEMORY_AREA:=16#84 (DB), BYTE_OFFSET:=target_byte_offset. Pass the variable as the RECV parameter of FC60.

My data from a short frame leaks into the next frame's DB area - why?

You are most likely writing into a fixed-size DB but not zeroing the area above the current frame. The new frame is placed at offset 0 of the RECV area, but the tail of the previous (longer) frame is still there. Clear the DB after each frame, or use a different DB per frame, or shift the write offset to a ring buffer.

What is the difference between FC60 and FB13/FB14/FB15?

FC60 (legacy) and the newer FB13/FB14/FB15 (with instance DBs) implement the same receive function. FB variants are needed for hot-restart resilience and for parallel multi-instance calls. The ANY-pointer pattern described in this article is identical for both call styles.

Can I use AG_LRECV over PROFINET without a configured connection?

No. AG_LRECV requires a configured ISO-on-TCP, TCP, or UDP connection in NetPro (or the equivalent TIA Portal connection configuration) with a unique ID. For ad-hoc UDP broadcast, use FB12/FB13 with the UDP broadcast connection configuration, or fall back to the CP's open IE communication via the SEND/RECV primitives.

Back to blog