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.
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
- CPU firmware V4.2 or higher (S7-1200). SCL
ATview over a byte array is supported in TIA Portal V13+. - DB
DB500_AnyBuildwith 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, butwDBmust be set explicitly because TIA Portal no longer auto-fills it from the DB number.) - Two data types in a global PLC data type (UDT) library:
tAnyViewfor the 16-byte header andtAnyViewLegacyfor the 10-byte form (optional, only for S7-300/400 compatibility). - Inputs you want to vary: an
INTindex, aWORDfor the element count, and aWORDfor 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.
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:
-
Byte order of WORD fields. The PLC stores
WORDvalues little-endian. The value16#0004for the type fieldWORDmust be laid down asabAny[2] = 16#04andabAny[3] = 16#00. Writing16#0004into a WORD variable and then a byte-by-byte move in the wrong order producesabAny[2] = 16#00,abAny[3] = 16#04, which the editor then reads as16#0400= 1024, an invalid type code. -
The bSyntaxID is not exactly 16#10. Some sample code uses
10(decimal), which is also16#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:
- Build the ANY as in Method 2 above with
wDataType = 16#05(INT) andwCount = 4. - Connect
abAnytoMB_CLIENT.DATA_PTR. - Trigger
MB_CLIENTwith a single-shotREQedge. Polling rate should be ≥ 50 ms on a serial CM 1241 to avoid RS-485 bus contention. - Read
MB_CLIENT.DONE,MB_CLIENT.ERROR, andMB_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 |
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
- Open the holder DB or instance DB in TIA Portal and switch to Monitor/Modify > Monitor all.
- Force the index
iIndexto a known value (e.g. 5) and the DB number to a DB that has been preloaded with a recognizable pattern (e.g.DB300bytes 0-3 =16#1111). - Confirm the online view of the holder now shows the constructed pointer, e.g.
P#DB300.DBX20.0 WORD 2if the record size is 4 bytes and the index is 5. - Trigger the communication FB. After
DONEgoes high, inspect the target DB at the calculated offset. The payload should have been written there, not into the holder. - Repeat with index = 0, 1, 2, ... N-1 to confirm the offset formula.
- 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).
- Reset the input
iIndexto -1 (sentinel) when out of range and zero the bytes of the ANY; the FB will then returnSTATUS = 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.