S7-1200 PUT/GET Variable ANY Pointer Parameter via AT Overlay

David Krause14 min read
S7-1200SiemensTechnical Reference
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

The PUT and GET instructions on a Siemens S7-1200 (firmware V4.0 and later, including the S7-1200 G2) accept two parameter families for data routing:

  • ADDR_1 ... ADDR_4 — declared as data type REMOTE, which internally maps to the classic ANY pointer (10-byte structured pointer: ID + type + count + DB + offset).
  • SD_1 ... SD_4 — declared as data type VARIANT, which is a typed wrapper around an ANY pointer plus runtime type information.

Both parameter types behave like constants in TIA Portal. When the block that calls PUT or GET has the Optimized block access attribute enabled (the default for new FB/FC in TIA V13 and newer), the compiler refuses to write to a REMOTE or VARIANT tag at runtime. The user cannot pass a variable pointer (e.g., an offset that increments per scan) into ADDR_1, and cannot programmatically build a source pointer for SD_1 either.

The practical effect is that every different source/destination slice requires a separate, hard-coded PUT call. For a payload larger than the 212-byte internal limit, the engineer is forced to write long CASE ladders or duplicate FB instances — exactly the workaround shown in the case study below.

Why the Limit Exists

The PUT / GET family is implemented as an asynchronous TSAP-based service running over the ISO-on-TCP / S7 communication channel. The 212-byte boundary is not arbitrary — it matches the maximum net payload of a single PUT/GET request frame after deducting the S7 protocol header overhead for an S7-1200 server. Exceeding it causes STATUS = 0x8001 ("Data length error") on ERROR = TRUE.

PUT/GET payload boundaries on S7-1200
Parameter Minimum Maximum Notes
Bytes per SD_i / ADDR_i 1 212 Net user payload after S7 header
Number of ADDR_i regions 1 4 Used when memory is fragmented
Total request frame (worst case) 1 B 4 × 212 B = 848 B Up to four slices per call
S7 connections per CPU (S7-1200) 1 8 (V4.x), 16 (V5.x), up to 32 on S7-1200 G2 Configured in Properties > Communication
Active PUT/GET partners 1 CPU-dependent Per partner-resource table

For the 800-byte case discussed here, four 200-byte slices satisfy the limit while leaving margin for the S7 header and any partner-side offset adjustments.

Reference Implementation — Constant-Pointer CASE Ladder

The engineer's original SCL uses a CASE state machine to fan out four PUT calls, each pinned to a fixed slice in DB16. Because ADDR_1 and SD_1 cannot be variables, every slice is a hand-written call:

CASE #put_step OF
  0: // idle — arm transfer
     IF #put_req THEN
       #put_step := 1;
       #final_done := false;
       #error_mem  := false;
       #status_mem := 0;
     END_IF;

  1: // first slice
     "PUT_DB"(REQ    := #put_req,
              ID     := #connection_ID,
              DONE   => #put_done,
              ERROR  => #put_error,
              STATUS => #put_status,
              ADDR_1 := P#DB16.DBX0.0   BYTE 200,
              SD_1   := P#DB16.DBX0.0   BYTE 200);
     IF #put_done THEN #put_req := false; #put_step := 2; END_IF;

  2: // re-trigger second slice (rising edge on REQ)
     "PUT_DB"(REQ    := #put_req,
              ID     := #connection_ID,
              ADDR_1 := P#DB16.DBX200.0 BYTE 200,
              SD_1   := P#DB16.DBX200.0 BYTE 200);
     #put_req := true; #put_step := 3;

  3: // second slice complete
     ...

  7: // last slice complete
     "PUT_DB"(REQ    := #put_req,
              ID     := #connection_ID,
              ADDR_1 := P#DB16.DBX600.0 BYTE 200,
              SD_1   := P#DB16.DBX600.0 BYTE 200);
     IF #put_done THEN
       #put_req := false;
       #final_done := true;
       #put_step := 0;
     END_IF;
ELSE
  #put_step := 0;
END_CASE;

IF #put_error THEN
  #error_mem  := true;
  #status_mem := #put_status;
  #put_step   := 0;
  #put_req    := false;
END_IF;

This works, but every time the payload changes — a new length, a different source DB, a different offset base — the entire ladder has to be edited. The same pattern repeats on the partner station for the GET side.

The AT-Overlay Solution

The fix exploits the fact that REMOTE on S7-1200 is an alias for the legacy 10-byte ANY pointer. On S7-300/400, ANY pointers are routinely manipulated in STL by writing into the byte fields. TIA Portal hides that capability when block access is optimized, but exposes it again the moment the block is created with Optimized block access = false and the variable is declared with an AT overlay.

Critical: Both the FC that builds the pointer and the FB that owns the PUT/GET call must be non-optimized. The InOut that carries the pointer between them must also live in a non-optimized block. Optimized block access reorders symbolic memory and breaks the byte layout of the ANY pointer.

Step 1 — Declare the ANY structure

In a non-optimized FB or DB, declare the canonical ANY pointer layout. Naming and order match the legacy definition used in Step 7 since V5:

TYPE "Udt_Any10"
  STRUCT
    ID      : BYTE;   // 0x10 = ANY
    Typ     : BYTE;   // 0x02 = BYTE, 0x04 = WORD, 0x05 = INT, 0x07 = DINT, 0x08 = REAL, 0x09 = LREAL ...
    Count   : WORD;   // repetition factor or string length
    DB      : WORD;   // DB number (0 for non-DB areas)
    Ptr     : DWORD;  // byte.bit pointer (24-bit byte offset + 8-bit bit offset)
  END_STRUCT;
END_TYPE

Step 2 — Build the FC that constructs the REMOTE pointer

Create an FC with Optimized block access = false. Declare the temporary REMOTE variable and overlay it with the structure above using AT. Populate the fields at runtime and pass the REMOTE variable up to the FB that owns the PUT/GET call.

FUNCTION "fc_build_remote" : VOID
{ S7_Optimize_Access := 'FALSE' }
VAR_TEMP
    t_remote   : REMOTE;          // 10-byte ANY under the hood
    t_any      : AT "Udt_Any10";  // byte-level overlay
    t_byte_off : DWORD;
END_VAR

BEGIN
    // --- encode: P#DB16.DBX200.0 BYTE 200 ---
    t_any.ID    := 16#10;          // ANY ID
    t_any.Typ   := 16#02;          // BYTE data type
    t_any.Count := 200;            // 200 bytes
    t_any.DB    := 16;             // target DB number

    // Byte.Bit pointer: bit offset (low byte) + byte offset (upper 24 bits)
    t_byte_off  := SHL_DWORD(DWORD#200, 3);  // 200 bytes << 3 bits
    t_any.Ptr   := t_byte_off;     // bit offset = 0, byte offset = 200

    // Hand the constructed ANY up to the calling FB
    "fb_put_master".io_remote_1 := t_remote;
END_FUNCTION

Step 3 — Receive the pointer in the calling FB

The master FB must also be non-optimized. Declare an InOut of type REMOTE (which the editor shows as an ANY-style pointer) and forward it directly to PUT:

FUNCTION_BLOCK "fb_put_master"
{ S7_Optimize_Access := 'FALSE' }
VAR_IN_OUT
    io_remote_1 : REMOTE;          // variable pointer, populated by FC
    io_sd_1     : VARIANT;
END_VAR
VAR
    s_done   : BOOL;
    s_error  : BOOL;
    s_status : WORD;
    s_req    : BOOL;
END_VAR

BEGIN
    s_req := s_req XOR TRUE;       // generate rising edge per call

    "PUT_DB"(REQ    := s_req,
             ID     := 1,
             DONE   => s_done,
             ERROR  => s_error,
             STATUS => s_status,
             ADDR_1 := io_remote_1,    // variable pointer — now legal
             SD_1   := io_sd_1);
END_FUNCTION_BLOCK

Step 4 — Loop instead of ladder

Because io_remote_1 is a true variable, the master FB can compute the next slice offset before re-arming REQ. The four-state CASE collapses into a single FOR loop:

FOR #slice := 0 TO 3 DO
    "fc_build_remote"(
        io_offset := #slice * 200,
        io_len    := 200,
        io_remote := #io_remote_1);

    #s_req := NOT #s_req;          // rising edge
    "PUT_DB"(REQ    := #s_req,
             ID     := 1,
             ADDR_1 := #io_remote_1,
             SD_1   := #io_sd_1);

    // wait for DONE or ERROR before issuing next slice
    WHILE NOT "PUT_DB".DONE AND NOT "PUT_DB".ERROR DO
        ;  // single-scan polling; replace with cyclic task delay if needed
    END_WHILE;

    IF "PUT_DB".ERROR THEN
        #err_status := "PUT_DB".STATUS;
        RETURN;
    END_IF;
END_FOR;
#final_done := TRUE;

Why Non-Optimized Access is Mandatory

Optimized blocks in TIA Portal store symbolic tags at addresses the compiler chooses, and those addresses may shift between compilations. The 10-byte layout of an ANY pointer — ID | Typ | Count | DB | Ptr — requires that the absolute byte offset of the Ptr field be fixed relative to the start of the tag. AT overlays are permitted in optimized blocks, but the REMOTE parameter of the PUT/GET instruction itself is internally declared as a fixed-offset ANY; when the surrounding block is optimized, TIA Portal rejects the connection at compile time with the diagnostic "The data type REMOTE is not permitted here".

Trade-off: Disabling optimized block access forces you to manage symbol addresses manually. Do not mix optimized and non-optimized access across the same data flow; the moment a pointer crosses into an optimized block it is silently copied and any later writes to the fields are dropped.

Configuring the S7 Connection

Before the PUT/GET call returns a status other than 0x7000, the S7 connection must be configured and the partner CPU must allow PUT/GET access. On the partner's Properties > Protection & Security > Connection mechanisms, enable Permit access with PUT/GET from remote partner. Without it the partner returns STATUS = 0x8004 on every PUT request.

Connection configuration matrix for S7-1200 PUT/GET
Setting Local CPU Partner CPU
Connection type S7 connection S7 connection
Active connection establishment Check if CPU initiates the PUT Uncheck if passive
Port (TCP/IP) 102 (ISO-on-TCP) 102
TSAP local Any free TSAP, e.g. 01.01 Free TSAP, e.g. 03.01
TSAP partner Partner's TSAP
PUT/GET enabled Must be enabled
Connection resource ID 1..16 (S7-1200), 1..32 (S7-1200 G2) Mirrors local

The compiled S7 connection blocks are stored in the project under Devices & Networks > Networks > S7 connections and downloaded as part of the hardware configuration. The S7-1200 server side stores its own view of the connection; mismatched TSAPs produce STATUS = 0x8005 (resource error) on the first PUT request.

PUT/GET Status Codes — Quick Reference

Selected PUT/GET STATUS values (DONE / ERROR outputs)
STATUS (hex) Meaning Typical cause Corrective action
0x0000 Done, no error Transfer completed
0x7000 No job active, REQ = FALSE Initial state Set REQ TRUE to start
0x7001 Job active (first call) PUT executing Wait for DONE / ERROR
0x7002 Job active (subsequent call) PUT still running Wait
0x8001 Data length error SD_i length > 212 bytes Reduce slice to ≤ 212 B
0x8004 Partner rejected access PUT/GET not enabled on partner Enable in partner protection
0x8005 Resource error Bad TSAP, partner offline Verify connection table, cabling
0x800B Partner in STOP with no OB Partner CPU STOP, no OB 82/86/122 Configure partner, run in RUN
0x8014 DB not loaded on partner SD_i / ADDR_i DB missing Download DB to partner
0x8081 Bad pointer type Typ field invalid Verify AT overlay, use 0x02 for BYTE
0x8090 Pointer / length inconsistent Count = 0 or Ptr misaligned Check Count and Ptr fields
0x80A1 Pointer DB number invalid DB field = 0 with non-zero area pointer Set correct DB number
0x80B1 Length error (variable) SD_i length > partner buffer Match length to partner capacity
0x80C1 Too many active jobs More than CPU allows Limit to configured connections
0x80C3 No free connection resources All connections busy Increase active connection limit
0x80D0 / 0x80D1 / 0x80D2 Connection fault, link down, send timeout Network / partner fault Inspect CPU diagnostics buffer

The complete list, including S7-1500-specific codes, is in the TIA Portal help for the PUT instruction under Status codes. See the official PUT: Write data to a remote CPU (S7-1200, S7-1500, S7-1200 G2) reference.

Verification Procedure

  1. Online compile. After building the FC and FB with non-optimized access, perform Compile > Software (rebuild all). The diagnostic buffer must remain clean; warnings about AT overlay on optimized block indicate that the optimization attribute was not cleared.
  2. Download to PLC. Use Download to device > Extended download so that the S7 connection table is refreshed. Without it, a newly added PUT call returns 0x8005.
  3. Watch table test. Open a watch table on the local DB and force put_req := TRUE. Observe PUT_DB.DONE rising on each slice and verify the partner DB updates byte-for-byte against the source.
  4. Inspect diagnostics buffer. On any non-zero STATUS, read Online & Diagnostics > Diagnostics buffer on both CPUs. The receiving CPU's event ID 0x18A1 / 0x18B1 entries show the exact S7 connection and offset that failed.
  5. Cycle-time check. PUT/GET runs in the cyclic OB and can extend scan time when the payload approaches 200 bytes. Capture OB1 runtime with a trace before deploying in production.
  6. Power-cycle test. Reboot the partner CPU. The local PUT should re-establish the connection within one to three scan cycles and resume transfer without operator intervention.

Troubleshooting Matrix

Common PUT/GET variable-pointer pitfalls
Symptom Likely cause Fix
Compiler error: "REMOTE not permitted" Optimized block access still on Set Optimized block access = false on the FB that calls PUT/GET and on the FC that builds the pointer
STATUS = 0x8081 on every call Typ field in the ANY pointer invalid Use 0x02 for BYTE, 0x04 for WORD, 0x05 for INT, 0x08 for REAL — must match the source area type
STATUS = 0x8090 after slicing Ptr field offset misaligned (bit offset set non-zero) Mask the low byte of Ptr to zero before assigning the byte offset
Data written to wrong DB DB field left at 0 from a previous slice Always re-assign DB for every slice, even if it equals the previous value
First slice OK, second slice garbled SD_i VARIANT reused without re-binding Pass SD_i through InOut from the calling FC; never reuse a static VARIANT
DONE never sets TRUE REQ rising edge missing (held high) XOR REQ with a one-shot pulse, or use a flag toggled on entry to the slice
Only first 200 bytes transfer Hard-coded constant still in ADDR_1 Confirm the io_remote_1 InOut is wired to ADDR_1, not a literal
Compile passes but runtime crash AT overlay on symbolic name not aligned to REMOTE Place the AT overlay on the temp or InOut tag itself, not on a derived copy
STATUS = 0x8004 with valid connection Partner protection blocks PUT/GET Partner > Properties > Protection & Security > enable "Permit access with PUT/GET"

Performance Notes

An 800-byte transfer split into four 200-byte PUT calls typically completes within 60–120 ms on a 100 Mbit/s PROFINET segment, dominated by the TCP handshake overhead of each slice. If lower latency is required, configure two S7 connections and run pairs of slices in parallel — the S7-1200 supports up to 8 (V4.x) or 16 (V5.x and G2) simultaneous S7 connections, each with its own resource ID.

For transfers that exceed 848 bytes per cycle, consider migrating the data plane to Open User Communication (OUC) using TSEND/TRCV or USEND/URCV. OUC supports up to 64 KB per send call, removes the 212-byte slice limit, and exposes the connection state through STATUS outputs that are easier to interpret than PUT/GET status codes.

Key Field-Proven Caveats

  • String pointers (0x13, 0x16) cannot be passed through the AT overlay trick. The Count field for strings is encoded in characters, not bytes; PUT/GET expect a raw BYTE type. Always send strings as ARRAY OF BYTE and reconstruct on the partner.
  • The ID field must always be 0x10. Forgetting it produces a STATUS = 0x8081 with no diagnostic hint about the ID.
  • DB numbers above 65535 are not supported. S7-1200 limits DBs to 65535; a pointer with DB = 0 and a non-zero Ptr is interpreted as a bit-memory or I/O area and fails on the receiving side.
  • Optimized DBs cannot be referenced through ADDR_i on a remote CPU. If the partner's target DB is optimized, the absolute address visible at runtime does not match the symbolic one, and the partner rejects the write. Use a non-optimized DB on the partner for any PUT/GET target.
  • Watch out for watchdog on partner. A long-running PUT burst can extend the partner's OB1 beyond the configured watchdog time. Increase Cycle monitoring time in the partner's CPU properties or throttle slice issuance.

Reference — Official Documentation

Why does the S7-1200 PUT instruction refuse a variable pointer for ADDR_1?

Because ADDR_1 is declared as data type REMOTE, an alias for the legacy ANY pointer. TIA Portal only allows manipulation of ANY pointers inside non-optimized blocks; in optimized blocks the compiler treats REMOTE as opaque and rejects runtime writes. Use an FC and FB with optimized block access disabled and an AT overlay on a temporary REMOTE tag.

What is the maximum payload per PUT call on an S7-1200?

212 bytes per SD_i / ADDR_i, with up to four regions per call, giving a practical ceiling of 848 bytes per PUT request. Larger transfers must be sliced, either with multiple PUT calls in a CASE / FOR loop or by migrating to Open User Communication with TSEND / TRCV.

Do I have to disable optimized block access on every block in the chain?

Yes. The FC that builds the REMOTE pointer, the FB that owns the PUT/GET call, and the InOut variable that carries the REMOTE tag must all live in non-optimized blocks. Once the pointer crosses into an optimized block, the compiler copies the value and any later field updates are lost.

Why does STATUS = 0x8004 appear even though the S7 connection is configured?

The partner CPU blocks PUT/GET access by default. Open the partner's Properties > Protection & Security > Connection mechanisms and enable "Permit access with PUT/GET from remote partner." Without this, the partner rejects every write regardless of the connection state.

Can I replace the CASE ladder with a single FOR loop once the AT overlay is in place?

Yes. With the REMOTE pointer built dynamically, the slice offset, length, and source DB can all be computed per iteration. A FOR loop with a one-shot REQ pulse per iteration and a polling wait for DONE / ERROR replaces the eight-state CASE ladder with roughly a dozen lines of SCL.

Back to blog