S7-1200 SCL: Constructing ANY Pointers for Communication Blocks

David Krause14 min read
S7-1200SiemensTutorial / 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

Overview: Why Dynamic ANY Pointers Are Needed in S7-1200

Communication function blocks on the S7-1200 family (MB_CLIENT, MB_SERVER, PUT, GET, TSEND, TRCV, USEND, URCV, the legacy Modbus RTU library, and the PtP library) accept the source or destination address of the payload through an ANY pointer input. A static literal such as P#DB300.DBX0.0 WORD 2 is straightforward: the pointer is fixed at compile time and the FB always writes the two words to DB300 bytes 0-3.

The moment the destination DB number, the byte offset, or the repetition count must be computed at runtime (for example: a recipe selects DB 305, or a recipe index of 5 means "skip 5*4 bytes ahead"), the engineer has to build the ANY pointer in code. Doing this in SCL on an S7-1200 is not the same as doing it in STL on an S7-300. TIA Portal hides the byte layout of the ANY from the programmer, and the most common symptom is a correct-looking monitor value of P#0.0 VOID 0 even though ten bytes of data were moved into a holding DB. This article gives the exact byte layout, the AT-view technique that works, and the runtime verification steps.

Scope: The procedure below targets firmware V4.2 - V4.6 of the S7-1211/1212/1214/1215/1217 CPUs and S7-1500 CPUs using TIA Portal V15.1 - V18. The 16-byte ANY form described here is the format used since S7-1500/1200 firmware V2.0 / TIA V12. The legacy 10-byte form is shown for reference.

The 16-Byte ANY Pointer Layout (S7-1200 / S7-1500)

The S7-1200 inherits the S7-1500 ANY format. It is exactly 16 bytes long, although the first 10 bytes are the only ones the legacy communication FBs actually consume. Bytes 10-15 are reserved for VARIANT extensions and must be zero unless the field is forwarded to a block expecting a VARIANT.

Byte Field Size (bits) Description
0 bSyntaxID 8 Must be 16#10 (decimal 16) for any ANY pointer. The editor writes this automatically when an ANY is declared.
1 bInternalUse 8 Reserved. Must be 16#00.
2-3 wDataType 16 Data type code (see reference table below). Stored little-endian; WORD = 16#0004, INT = 16#0005.
4-5 wCount 16 Repetition factor. Number of elements of the data type. WORD 2 means 2 elements (4 bytes total).
6-7 wDB 16 DB number when the area code is 16#84. Zero for all other areas.
8 bArea 8 Memory area code. 16#84 = DB, 16#83 = M, 16#81 = I, 16#82 = Q, 16#86 = L.
9-11 dwByteOffset + bit 24 Bit offset occupies the low 3 bits of byte 11 (values 0-7). The remaining 21 bits form the byte offset. The full 24-bit value lives in bytes 9, 10 and 11, low byte first.
12-15 abReserved 32 Reserved (VARIANT extension). Keep at 16#00 when feeding legacy FBs.

The byte/bit offset encoding is the single most common source of bugs. The 24-bit field is laid out as: high-to-low: byte_offset[20:0] (21 bits) | bit_offset[2:0] (3 bits). A pointer at DBX14.3 encodes as 16#00000073 in little-endian: 14*8 + 3 = 115 = 0x73. For DBX0.0 the field is 16#00000000.

Data Type and Memory Area Code Reference

Mnemonic Code (hex) Use
VOID 16#00 Invalid / uninitialized; what the editor shows when an ANY has only its header set
BOOL 16#01 Bit (count is always 1 in this position for FB inputs)
BYTE 16#02 1 byte per element
CHAR 16#03 1 byte
WORD 16#04 2 bytes
INT 16#05 2 bytes, signed
DWORD 16#06 4 bytes
DINT 16#07 4 bytes, signed
REAL 16#08 4 bytes, IEEE-754
DATE 16#09 2 bytes
TOD 16#0A 4 bytes, time-of-day
TIME 16#0B 4 bytes, S5TIME-compatible
S5TIME 16#0C Legacy 4-byte S5 time
DT 16#0D 8 bytes, date-and-time (legacy)
STRING 16#0E 2 + n bytes (header + chars)
POINTER 16#0F 6 bytes, internal POINTER
DTL 16#13 12 bytes, IEC date/time long
LREAL 16#17 8 bytes, IEEE-754
USINT 16#1C 1 byte unsigned
SINT 16#1D 1 byte signed
UINT 16#18 2 bytes unsigned
UDINT 16#19 4 bytes unsigned
ULINT 16#1A 8 bytes unsigned
LWORD 16#1B 8 bytes
Area Code (hex) Notes
P (I/O inputs) 16#80 Peripheral inputs, rarely used as ANY targets in S7-1200
I (process image input) 16#81 Read-only payload source
Q (process image output) 16#82 Output image
M (bit memory) 16#83 Merker area
DB (data block) 16#84 Most common; wDB holds the DB number
DI (instance DB) 16#85 Multi-instance or background DB
L (local / temp) 16#86 Only valid inside the calling block

Prerequisites and Tool Versions

  1. CPU firmware V4.2 or higher (S7-1200). SCL AT view over a byte array is supported in TIA Portal V13+.
  2. DB DB500_AnyBuild with optimized block access OFF. Optimized DBs hide the absolute byte offset; the AT view technique requires the underlying byte ordering to be predictable. (If optimized access is required, the same AT technique still works because the 16 bytes are contiguous, but wDB must be set explicitly because TIA Portal no longer auto-fills it from the DB number.)
  3. Two data types in a global PLC data type (UDT) library: tAnyView for the 16-byte header and tAnyViewLegacy for the 10-byte form (optional, only for S7-300/400 compatibility).
  4. Inputs you want to vary: an INT index, a WORD for the element count, and a WORD for the DB number.

Method 1 — AT View Over a Static DB (DB500 Approach)

Step 1 — Declare the holder DB and the AT view

// Global PLC data type "tAnyView" (DUT)
TYPE tAnyView :
STRUCT
    bSyntaxID      : BYTE;    // 16#10
    bInternalUse   : BYTE;    // 16#00
    wDataType      : WORD;    // 16#0004 = WORD
    wCount         : WORD;    // 2
    wDB            : WORD;    // 300
    bArea          : BYTE;    // 16#84 = DB
    dwOffset       : DWORD;   // byte[9..11] + bit in low 3 bits of high byte
    abReserved     : ARRAY[0..5] OF BYTE;  // 16#00
END_STRUCT;
END_TYPE

Step 2 — Build the holder in DB500

DATA_BLOCK "DB500_AnyBuild"
{ S7_Optimized_Access := 'FALSE' }
STRUCT
    abAny : ARRAY[0..15] OF BYTE;   // raw 16 bytes
END_STRUCT;
BEGIN
END_DATA_BLOCK

Step 3 — Fill the bytes at runtime

// Inside an FC or FB written in SCL
"DB500_AnyBuild".abAny[0]  := 16#10;            // syntax ID
"DB500_AnyBuild".abAny[1]  := 16#00;            // internal use
"DB500_AnyBuild".abAny[2]  := 16#04;            // WORD low byte
"DB500_AnyBuild".abAny[3]  := 16#00;            // WORD high byte
"DB500_AnyBuild".abAny[4]  := 2;                // count low byte
"DB500_AnyBuild".abAny[5]  := 0;                // count high byte
"DB500_AnyBuild".abAny[6]  := 300 MOD 256;      // DB low byte
"DB500_AnyBuild".abAny[7]  := 300 / 256;        // DB high byte
"DB500_AnyBuild".abAny[8]  := 16#84;            // DB area
"DB500_AnyBuild".abAny[9]  := 0;                // offset low byte
"DB500_AnyBuild".abAny[10] := 0;                // offset mid byte
"DB500_AnyBuild".abAny[11] := 0;                // offset high byte (bit offset 0)
"DB500_AnyBuild".abAny[12] := 0;
"DB500_AnyBuild".abAny[13] := 0;
"DB500_AnyBuild".abAny[14] := 0;
"DB500_AnyBuild".abAny[15] := 0;

Step 4 — Pass it to the FB

Connect "DB500_AnyBuild".abAny to the ANY input of the FB. The compiler treats a 16-byte array as a valid ANY at the parameter boundary. Do not pass the DB number as an input parameter; the FB reads the destination from the 16 bytes themselves.

Method 2 — Temp ANY in FC, Static ANY in FB (Recommended)

The recommended approach is to keep the ANY inside the block instance, then write each byte of the static ANY before the FB is called. This is the approach Siemens uses in its scalable Modbus sample for S7-1500, which the discussion linked. For S7-1200 it works identically when the project is created with the appropriate CPU.

FB-based implementation

FUNCTION_BLOCK "fbModbusScaler"
VAR
    abAny : ARRAY[0..15] OF BYTE;          // holder inside instance DB
    sAny  AT abAny : tAnyView;             // structured overlay
    iIndex : INT;                          // runtime input
    wDbNum : WORD;                         // runtime DB selector
END_VAR
BEGIN
    // Build the ANY header once per cycle
    sAny.bSyntaxID    := 16#10;
    sAny.bInternalUse := 16#00;
    sAny.wDataType    := 16#0004;          // WORD
    sAny.wCount       := 2;                // 2 words = 4 bytes
    sAny.wDB          := wDbNum;
    sAny.bArea        := 16#84;

    // dwOffset encodes byte_offset * 8 + bit_offset (low 3 bits)
    // For byte-only offsets, bit_offset = 0
    sAny.dwOffset     := DWORD#0;
    sAny.abReserved[0] := 16#00;
    sAny.abReserved[1] := 16#00;
    sAny.abReserved[2] := 16#00;
    sAny.abReserved[3] := 16#00;
    sAny.abReserved[4] := 16#00;
    sAny.abReserved[5] := 16#00;
END_FUNCTION_BLOCK

Then wire abAny (the byte array) to the ANY input of the communication FB. TIA Portal will accept it because both are 16 bytes long and the editor will display it as the pointer value the engineer constructed.

Important: The AT view is a compile-time alias, not a copy. Writing to sAny.wDataType updates the same memory as abAny[2..3]. There is no need to copy between the two; just keep them synchronized in the order of operations if you write to both representations in the same cycle.

Why the Online View Shows P#0.0 VOID 0

When the user fills ten bytes in an instance DB and watches the value in TIA Portal's online monitor, TIA Portal reads the syntax ID, the type, the count, the DB number and the area, then displays the symbolic equivalent. If the syntax ID is not 16#10 or the data type is 16#00 (VOID), the editor falls back to P#0.0 VOID 0. The pointer itself is still in memory; the editor simply cannot interpret it.

Two common reasons this happens in practice:

  1. Byte order of WORD fields. The PLC stores WORD values little-endian. The value 16#0004 for the type field WORD must be laid down as abAny[2] = 16#04 and abAny[3] = 16#00. Writing 16#0004 into a WORD variable and then a byte-by-byte move in the wrong order produces abAny[2] = 16#00, abAny[3] = 16#04, which the editor then reads as 16#0400 = 1024, an invalid type code.
  2. The bSyntaxID is not exactly 16#10. Some sample code uses 10 (decimal), which is also 16#0A, the data type for SINT. The editor then assumes the rest of the field is a SINT array and renders it as such, hiding the real bytes.

Dynamic Offset Calculation from an INT Index

The original question asked how to convert an input of 5 (INT) into a pointer that points either at P#DB300.DBX0.0 WORD 7 or at P#DB305.DBX0.0 WORD 2. Two scaling strategies are common in real applications:

Strategy A — Index selects the DB number

The DB number is recomputed from a base plus the index, the offset and count stay constant. Useful when each device / slave / record lives in its own DB.

sAny.wDB := WORD#300 + WORD#(iIndex);
sAny.dwOffset := DWORD#0;          // start at byte 0, bit 0
sAny.wCount := 2;                  // unchanged

Strategy B — Index selects the byte offset (record inside a single DB)

All records live in one large DB; the index multiplies the record size. For a record of 2 words (4 bytes), index 5 means offset 20 bytes. For 7 words (14 bytes) per record, index 5 means offset 70 bytes.

VAR CONSTANT
    cRecordSizeBytes : INT := 14;  // 7 words = 14 bytes
END_VAR

sAny.wDB := 300;                   // base DB
sAny.dwOffset := DWORD#(INT#0 + iIndex * cRecordSizeBytes);
sAny.wCount := 2;                  // still 2 words of payload

Bit offset for BOOLEAN targets

If the payload is a BOOL rather than a WORD, the bit offset must be encoded into the low 3 bits of dwOffset:

// Index a single BOOL by iIndex 0..7, byte offset 0, DB 300
sAny.wDataType := 16#01;          // BOOL
sAny.wCount    := 1;
sAny.wDB       := 300;
sAny.bArea     := 16#84;
sAny.dwOffset  := SHL_DWORD(DWORD#0, 3)   // bit offset goes in low 3 bits
                OR SHL_DWORD(DWORD#(iIndex AND 16#07), 0);
// Note: iIndex up to 7 stays in the same byte; higher indices need
// a different layout: byte_offset = iIndex / 8, bit_offset = iIndex MOD 8

Wiring the Constructed ANY to MB_CLIENT / MB_SERVER

The Modbus RTU FBs in the S7-1200 Modbus RTU library take the source/destination as the input DATA_PTR of type VARIANT. A VARIANT in S7-1500/1200 is internally a 16-byte ANY with the extensions populated. As long as bytes 0-11 are filled correctly and the area code is 16#84, the FB will accept the pointer and write the payload to the chosen DB and offset.

Steps for an MB_CLIENT that reads 4 holding registers and stores them at a dynamic offset:

  1. Build the ANY as in Method 2 above with wDataType = 16#05 (INT) and wCount = 4.
  2. Connect abAny to MB_CLIENT.DATA_PTR.
  3. Trigger MB_CLIENT with a single-shot REQ edge. Polling rate should be ≥ 50 ms on a serial CM 1241 to avoid RS-485 bus contention.
  4. Read MB_CLIENT.DONE, MB_CLIENT.ERROR, and MB_CLIENT.STATUS (Word) to confirm.

The Siemens Modbus library also provides a scalable Modbus example for S7-1500 which uses the same construction. The example program Modbus_RTU_Scalable works on S7-1200 with the MB_CLIENT and MB_SERVER blocks after changing the CPU device in the project; no other modifications are required for the pointer construction itself.

Common Pitfalls, Error Codes, and Troubleshooting

Symptom Root cause Fix
Online monitor shows P#0.0 VOID 0 Either bSyntaxID != 16#10 or data type field = 0 Verify bytes 0 and 1 of the holder; reload and watch after a write
STATUS = 16#8188 from MB_CLIENT ANY references a DB that does not exist Confirm wDB was written with the correct number and that the DB is present in the project, downloaded to the CPU
STATUS = 16#80C8 or 16#80C9 Modbus slave timeout or CRC error on the wire Unrelated to ANY construction; check RS-485 termination, baud, parity
Data is written but to the wrong offset Byte offset is shifted by the data type size, or the count is wrong For WORD 2, the FB writes 2 * 2 = 4 bytes starting at dwOffset / 8
Data is written to the holder DB instead of the target DB The ANY was not actually the value; the DB number was passed as a separate input Pass the 16-byte array as the ANY; do not pass the DB number separately
Optimized DB blocks the construction TIA Portal places BOOL/INT fields in a non-byte order; AT view becomes ambiguous Build the ANY in a non-optimized instance, or compute it inside an FB's static section with explicit offsets
Compiler error "type ANY cannot be assigned" Trying to assign an ANY literal to a byte array field Write each byte explicitly, or use POKE_BLK to copy a 16-byte source into the holder
FB input behavior: The communication FB does not read the ANY until the call edge arrives. If the ANY is updated after REQ goes high, the FB will use the new value on the next call. Always update the ANY before setting REQ true.

Verification and Online Testing Procedure

  1. Open the holder DB or instance DB in TIA Portal and switch to Monitor/Modify > Monitor all.
  2. Force the index iIndex to a known value (e.g. 5) and the DB number to a DB that has been preloaded with a recognizable pattern (e.g. DB300 bytes 0-3 = 16#1111).
  3. Confirm the online view of the holder now shows the constructed pointer, e.g. P#DB300.DBX20.0 WORD 2 if the record size is 4 bytes and the index is 5.
  4. Trigger the communication FB. After DONE goes high, inspect the target DB at the calculated offset. The payload should have been written there, not into the holder.
  5. Repeat with index = 0, 1, 2, ... N-1 to confirm the offset formula.
  6. For an MB_CLIENT that reads, also confirm the slave's response was actually correct by reading the same register with a different tool (Modbus Poll, a HMI tag with polling).
  7. Reset the input iIndex to -1 (sentinel) when out of range and zero the bytes of the ANY; the FB will then return STATUS = 16#80B1 "Invalid pointer".

FAQ

Why does my ANY pointer show as P#0.0 VOID 0 in the online monitor even after I filled all 16 bytes?

The most common cause is byte 0 (the syntax ID) not equal to 16#10. The decimal value 10 is 16#0A (SINT), not 16. Verify that abAny[0] = 16#10 and that the data type field (bytes 2-3) is non-zero, in little-endian order.

Can I pass the constructed ANY as a single variable instead of 16 separate bytes?

Yes. Declare a DUT tAnyView with the 16 fields and an AT overlay over a ARRAY[0..15] OF BYTE. The compiler treats the byte array and the structured view as the same memory, so writing to one updates the other. Pass the byte array to the FB input.

How do I encode a bit offset for a BOOL ANY pointer?

The bit offset lives in the low 3 bits of the 24-bit dwOffset field. Multiply the byte offset by 8 and OR the bit offset (0-7): dwOffset = byte_offset * 8 + bit_offset. For BOOL targets, set wCount = 1 and wDataType = 16#01.

Does the same construction work on S7-300/400 with STEP 7 V5.5?

Yes, but the ANY is only 10 bytes long. Bytes 10-15 must be omitted. The legacy form is still supported in TIA Portal, but the modern S7-1200/1500 FBs expect the 16-byte form and will treat bytes 10-15 as "reserved".

What STATUS code from MB_CLIENT indicates a problem with the constructed ANY rather than the serial bus?

16#8188 means the DB referenced by the ANY does not exist or is wrong-sized; 16#80B1 means the ANY itself is invalid (header bytes wrong). Bus errors are 16#80C8 (timeout) and 16#80C9 (CRC). The Siemens Modbus RTU library documents all STATUS codes in the F1 help of the FB.

Back to blog