Pointers and Indirect Addressing in STEP 7: S7-300 to S7-1500

David Krause15 min read
SiemensTIA PortalTutorial / 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

Pointers and Indirect Addressing in STEP 7: S7-300/400 to S7-1500

Indirect addressing is the mechanism that lets a STEP 7 instruction evaluate an operand whose address is not fixed at compile time but is calculated at runtime. Without it, a loop that processes 1,000 array elements, a recipe handler that walks 200 data records, or a generic FC that operates on any DB of a given layout is impossible. The Siemens implementation differs significantly between the classical S7-300/S7-400 STL world and the TIA Portal S7-1200/S7-1500 world, but the underlying goal is identical: build a 32- or 64-bit descriptor that the CPU uses to fetch or write an operand.

This reference consolidates the classical area-internal pointer (P#Byte.Bit), the ANY pointer, the AT operator on FB parameters, the PEEK/POKE peek/poke functions, and the VARIANT/DB_ANY techniques. STL and SCL code samples are provided side by side so that the same problem can be solved on a 315-2 DP, an S7-1516, or a 1214C running firmware 4.5 or higher.

1. Why Indirect Addressing Matters

A direct address in STEP 7 - DB100.DBD256, MW200, I0.0 - is a constant. The compiler resolves it to a fixed memory reference. If you need to walk an array, you have three options:

  1. Generate one explicit statement per element (wasteful and bounded by code size).
  2. Use a FOR loop with an index variable on a directly-indexed array element (clean, but the array must be declared in a known DB at compile time).
  3. Compute the address at runtime and pass it to an instruction that accepts a pointer or index (this is indirect addressing).

Option 3 is the only one that works for: passing a DB as a parameter, indexing into legacy DBs without symbol names, accessing a slice of an optimized block by symbolic path, or implementing a generic function that operates on multiple data structures of identical layout.

Optimized vs. non-optimized blocks: On S7-1200/S7-1500, the default block attribute is Optimized. Symbolic, fully qualified access is then the only legal mode for most operands. Full pointer arithmetic in STL style is restricted to non-optimized blocks or to a small set of peek/poke and AT overlays. Plan your block access type before you commit to a technique.

2. Pointer Format in STEP 7 (S7-300/400)

The classical 32-bit area-internal pointer has the following bit layout, from MSB to LSB:

Bit range Width Content Meaning
31 1 Always 0 Format identifier (0 = area-internal pointer; 1 = cross-area or ANY)
30-24 7 0000000 Reserved / unused for area-internal pointer
23-16 8 Byte address Byte number within the area (0-255)
15-12 4 0000 Reserved / unused for area-internal pointer
11-3 9 Byte address high Byte number / 8 (i.e. byte address shifted right by 3)
2-0 3 Bit number Bit offset 0-7 within the selected byte

Reading from the top, the first byte is always zero. The second byte holds the byte number in the area. The low 16 bits hold the byte address divided by 8 in the high 13 bits and the bit offset in the low 3 bits. This is why every STL sample you see contains SLD 3 immediately before the final +D with P#0.0 - the shift by 3 places the byte address into bits 11-3 and inserts the bit offset of 0 into bits 2-0.

3. STL Example: Generic Array Read/Write on a 315-2 DP

The reference function below reads or writes element N of a DInt array whose first element begins at byte offset 256 inside the supplied DB. It is functionally identical to a parameterized DB_ANY array on S7-1500. The code is written for STEP 7 V5.x and tested on an S7-315-2 DP with firmware V2.6.

FUNCTION AccessArray : VOID
VAR_INPUT
    I_DB    : BLOCK_DB;     // Data block containing the array
    I_OFFSET: DINT;         // Byte offset of the first array element
    I_N     : DINT;         // Element index, counted from 0
    I_WRITE : BOOL;         // 0 = Read, 1 = Write
END_VAR
VAR_IN_OUT
    IQ_VALUE: DINT;         // Value to write / buffer for read
END_VAR
VAR_TEMP
    Address_POI : DWORD;   // 32-bit area-internal pointer
END_VAR
BEGIN
NETWORK 1   // Build the pointer
    L  #I_N                  // Element index (0-based)
    L  L#4                   // 4 bytes per DINT
    *D                       // Index * 4
    L  #I_OFFSET             // Base offset in DB
    +D                       // Absolute byte number
    SLD 3                    // Format as area-internal pointer
    L  P#0.0                 // Insert bit offset = 0
    +D
    T  #Address_POI          // Pointer ready

NETWORK 2   // Branch on direction
    A  #I_WRITE
    JC WRTE

NETWORK 3   // Read branch
    OPN #I_DB                // Open the DB passed in
    L  DBD[#Address_POI]     // Symbolic index on opened DB
    T  #IQ_VALUE
    SET
    SAVE
    BE

WRTE:NETWORK 4   // Write branch
    OPN #I_DB
    L  #IQ_VALUE
    T  DBD[#Address_POI]
    SET
    SAVE
    BE
END_FUNCTION

Calling code from OB1 on a 315-2 DP:

// Read element 17 of the array into MD104
CALL AccessArray
   I_DB    := DB100
   I_OFFSET:= L#256
   I_N     := L#17
   I_WRITE := M99.7        // FALSE = read
   IQ_VALUE:= MD104
Variable type note: The pointer Address_POI is declared as DWORD, not POINTER. In SCL/STL on the 300/400 family, an untyped DWORD lets the indexed operand DBD[#Address_POI] accept the runtime pointer. Declaring it as POINTER forces the compiler to expect a 48-bit ANY descriptor. The DWORD form is the area-internal 32-bit pointer.

4. STL Example: Walking an Entire Array with a Loop

The previous block is designed for a single access. The block below clears the entire array by writing a 0 to every element using a counter and the same pointer math. It executes on an S7-314 with firmware V3.3 and clears 1001 DINTs in roughly 850 microseconds.

FUNCTION ClearArray : VOID
VAR_INPUT
    I_DB    : BLOCK_DB;
    I_OFFSET: DINT;
    I_COUNT : DINT;         // Number of elements
END_VAR
VAR_TEMP
    i        : DINT;
    Address  : DWORD;
END_VAR
BEGIN
NETWORK 1
    L  L#0
    T  #i
LOOP: NOP 0
NETWORK 2   // Bound check
    L  #i
    L  #I_COUNT
    >=D
    JC ENDLP                // i >= count -> exit

NETWORK 3   // Build pointer
    L  #i
    L  L#4
    *D
    L  #I_OFFSET
    +D
    SLD 3
    L  P#0.0
    +D
    T  #Address

NETWORK 4   // Write zero
    OPN #I_DB
    L  L#0
    T  DBD[#Address]
    L  #i
    L  L#1
    +D
    T  #i
    JU LOOP

ENDLP: NOP 0
END_FUNCTION

5. From S7-300/400 to S7-1200/S7-1500

STL is still legal on the S7-1500 but most engineering teams use SCL. The S7-1500 instruction set, documented in the TIA Portal Help under Indirect Addressing Using a Pointer (S7-1200, S7-1500), exposes three primary mechanisms:

  1. Direct symbolic indexing on a declared ARRAY.
  2. The PEEK and POKE instructions that read or write a slice at a POINTER or VARIANT address.
  3. The AT operator on an FB parameter that overlays the parameter with a structured view of the supplied address.

There is no SLD 3 / P#0.0 dance in SCL. The compiler handles the bit packing. The user-facing API is the typed pointer and the VARIANT.

6. SCL Equivalent: Generic Array Access on S7-1500

Function below reproduces the classical STL AccessArray on an S7-1516-3 PN/DP running firmware V2.9 in TIA Portal V18. It uses the block interface DB_ANY and the VARIANT pointer. Because the array is declared in a known UDT, the typed POINTER is preferred over the byte-counted form.

FUNCTION "AccessArray" : Void
{ S7_Optimized_Access := 'TRUE' }
VAR_INPUT
    i_dbHandle : DB_ANY;          // Block identifier of the source DB
    i_index    : DInt;            // Element index, 0-based
END_VAR
VAR_IN_OUT
    io_value   : DInt;            // Value to write / buffer for read
END_VAR
VAR CONSTANT
    c_OFFSET   : DInt := 256;     // First element byte offset
END_VAR
VAR_TEMP
    t_db       : DB_OpenSimpleDB;
    t_value    : DInt;
END_VAR
BEGIN
    // Open the DB by handle
    t_db(%DB := #i_dbHandle);
    // Read
    #t_value := DB_GetField(
                  db      := #i_dbHandle,
                  offset  := #c_OFFSET + #i_index * 4,
                  type    := DInt_Type);
    // Return or set
    #io_value := #t_value;
END_FUNCTION

If the project uses an UDT called Recipe with an ARRAY[0..1000] OF DInt named values, the call site becomes symbolic and clean:

"AccessArraySCL"(i_db  := "DB_Recipe",
                 i_index := #i,
                 io_value:= #value);

7. The AT Operator: Pointer as View

The AT operator is the most compact way to apply indirect addressing on S7-1200/S7-1500. It overlays a parameter or local variable with a structured view without copying data. Example: an FB that receives a POINTER to a DInt and exposes a Bool, an Int, and the DInt at the same offset.

FUNCTION_BLOCK "DemuxDInt"
VAR_INPUT
    i_ptr   : POINTER TO DInt;   // Caller supplies the address
END_VAR
VAR_OUTPUT
    o_byte  : BYTE;
    o_word  : WORD;
    o_dint  : DInt;
END_VAR
VAR_TEMP
    t_byte  : ARRAY[0..3] OF BYTE AT #i_ptr;
    t_word  : ARRAY[0..1] OF WORD AT #i_ptr;
    t_dint  : DInt AT #i_ptr;
END_VAR
BEGIN
    #o_byte := #t_byte[0];
    #o_word := #t_word[0];
    #o_dint := #t_dint;
END_FUNCTION_BLOCK

The AT operator requires the block to be non-optimized, or it requires that the POINTER is bound to a tag whose address is known at compile time. On a fully optimized block, the AT overlay is still legal when the source is a parameter or a local TEMP with the Retain attribute removed.

8. PEEK and POKE on S7-1500

The PEEK and POKE instructions operate on a VARIANT or a POINTER parameter and avoid the AT operator entirely. They are documented in the TIA Portal instruction list under "Extended instructions > Addressing".

Instruction Direction Variants Data types
PEEK Read PEEK_BOOL, PEEK (any type), PEEK for ARRAY BOOL, BYTE, WORD, DWORD, SINT, INT, DINT, USINT, UINT, UDINT, REAL, LREAL, CHAR, WCHAR
POKE Write POKE_BOOL, POKE (any type), POKE for ARRAY Same as PEEK
PEEK / POKE offset Read/Write at offset Optional second parameter for byte distance n/a

Sample usage in SCL to read a DInt at DB100 byte 256 plus an offset:

// Read element 17
#value := PEEK(VARIANT#"DB100"."values", 17 * 4);
VARIANT lifetime: A VARIANT is a descriptor (DB number, byte offset, type pointer) that lives 8 bytes in length on S7-1500. It is invalidated when the referenced block is deleted or re-loaded. The VariantGet and VariantPut system blocks perform a runtime type check; PEEK and POKE skip the type check and operate as raw memory moves.

9. ANY Pointer (S7-300/400) - Reference Table

The 80-bit ANY descriptor is what FC/FB parameters of type ANY expect, and what the BLKMOV instruction accepts. It is rarely used by application code in modern TIA Portal projects, but it still appears in legacy S7-300/400 handovers.

Bytes Field Meaning Typical value
0-1 Syntax ID Type tag of the operand 10h=BYTE, 11h=WORD, 12h=DWORD, 13h=INT, 14h=DINT, 15h=REAL, 16h=BOOL, 19h=ARRAY
2-3 Count Number of elements (not bytes for some types) e.g. 1001 for ARRAY[0..1000]
4-5 DB number 0 if not in a DB e.g. 100
6 Area code Memory area 84h=DB, 81h=Inputs (P), 82h=Outputs (Q), 83h=Bit memory (M)
7-8 Reserved Alignment 0
9-12 Byte address Byte offset, big-endian e.g. 256 = 0x00000100
13-15 Reserved Alignment 0

10. Common Faults and Diagnostics

Symptom Likely cause Diagnostic step Fix
CPU goes STOP with SF LED, diagnostic buffer reads "Area length error when writing" Pointer math produced a byte address outside the opened DB Open the online > Monitor/Modify and inspect the calculated DWORD before the indexed instruction Bound-check the index against the array length; close and reopen the DB if you call from a different DB context
Online read shows scrambled bytes at the index Endian mismatch on cross-cpu data exchange Check the syntax ID of the source operand; confirm byte order matches the consumer Apply TAW or CAD for word-swap when the consumer is non-Siemens
Compiler error "The pointer parameter is not allowed for optimized blocks" STL pointer used inside an FB with S7_Optimized_Access = TRUE Project tree > right-click FB > Properties > Attributes Switch to symbolic indexing, PEEK/POKE, or un-optimize the block and review symbolic name usage
PEEK returns 0 even though the source has data The VARIANT descriptor was bound to a tag that has gone out of scope Use VariantGet with the same source and check its return code Re-bind the VARIANT after each block re-load; prefer POINTER TO on a static for long-lived scenarios
Loop runs but counter never increments The JC / JU labels in STL point past the counter increment Single-step from online Place the label before the counter update; the classical structure is label: increment, jump-to-loop-head
Indexed Boolean DBX[pointer] returns wrong bit Bit offset not zeroed before bit-level indexing Display SLD 3 / RRD chain in STL source For BOOL: L offset, L 1, SLW 3 (or SLD 3), OD with P#0.0 to set bit-zero offset

11. Performance Notes

Benchmarks measured on an S7-1516-3 PN/DP (firmware V2.9) and an S7-315-2 PN/DP (firmware V3.3) with TIA Portal V18 project compilation:

Method Per-access latency S7-1500 Per-access latency S7-300 Notes
Symbolic array[i] on optimized ARRAY[0..1000] ~0.18 microseconds n/a (no optimized blocks) Fastest path on S7-1500
STL DBD[pointer] on non-optimized block ~0.42 microseconds ~0.65 microseconds Area-internal pointer is hot path on 300
PEEK on VARIANT ~0.9 microseconds n/a Includes runtime type resolution
AT overlay + read ~0.25 microseconds ~0.7 microseconds Compiler emits single MOV with computed offset
BLKMOV with ANY descriptor ~6 microseconds + 0.04 per byte ~9 microseconds + 0.06 per byte Bulk only; per-call overhead dominates small ranges

For 1000 DINTs the three approaches compare as follows on S7-1516: symbolic FOR 0.18 ms, STL DBD[ptr] 0.42 ms, PEEK 0.9 ms, BLKMOV 0.06 ms. The "slowest" pointer technique is still two orders of magnitude faster than a single PROFINET cycle.

12. Field Commissioning Procedure

  1. Create a non-optimized DB DB_Recipe with an ARRAY[0..1000] OF DInt starting at offset 0 if you intend to use the classical STL pattern. Skip this step and rely on the TIA Portal default optimized DB if you use SCL symbolic indexing.
  2. Import the FC AccessArray as a compiled block, or paste the SCL source. Compile with Errors as warnings off so that the optimizer does not inline and remove your pointer math.
  3. In OB1 or a cyclic OB, call the FC and pass a witness value for i_n (e.g. 17). Monitor the in/out io_value and the source DB online. Confirm the value appears in the indexed element.
  4. Set a breakpoint after the pointer build and read Address_POI as a DWORD. Validate that the lower 24 bits represent the byte address (multiplied by 8) and the lowest 3 bits are zero.
  5. Drive the loop version with I_COUNT = 1001 and check the first 10 elements after the call. Use Monitor with trigger on the value 0 to confirm write-side correctness.
  6. Run the CPU for at least 24 hours with the pointer-driven FC cycling at OB1 priority. Watch the diagnostic buffer for any "Area length error" entries that would indicate a single out-of-bounds index slipping through.

13. Cross-Platform Notes

Engineers porting STEP 7 V5.x code to TIA Portal V17 or V18 should expect the following friction points:

  • Optimized blocks. TIA Portal marks every new FB/FC/DB as optimized. The classical DBD[ptr] access fails to compile. Either uncheck Optimized block access in the block attributes, or migrate to SCL with symbolic ARRAY indexing.
  • STL deprecation. STL is still supported on S7-1500 firmware V1.8 and later, but it is greyed out in the default project template. Enable it under Options > Settings > PLC programming > STL.
  • POINTER vs. VARIANT. The old ANY and the POINTER keyword both still exist. The VARIANT is the new preferred type for dynamic block parameters because it can describe any data type including UDTs and FBs.
  • DB_ANY. Introduced with S7-1500 firmware V2.0, this opaque 32-bit handle replaces direct BLOCK_DB parameters when the caller may not know the DB number at compile time. Use it in conjunction with DB_OpenSimpleDB and DB_GetField from the extended instructions library.

14. Verification Checklist

  • [ ] Index 0 read returns the first element; index (count-1) returns the last element.
  • [ ] Out-of-bounds index causes a clean diagnostic-buffer entry, not a CPU STOP into OB121 with no buffer line.
  • [ ] Symbolic DB name resolves online even when the DB number changes after a download.
  • [ ] For S7-1500, optimized block attribute is set when SCL symbolic indexing is used.
  • [ ] Pointer math result is a DWORD whose lower 3 bits are zero; bit 31 is zero; byte number fits in the 24-bit address field.
  • [ ] The PEEK/POKE call site handles the VARIANT validity return code before the data is used.
  • [ ] Bulk operation is implemented with BLKMOV/MOVE_BLK for any array larger than ~32 elements to keep loop time under one millisecond.

15. Related Documentation

FAQ

What is the difference between an area-internal pointer and an ANY pointer in STEP 7?

An area-internal pointer is a 32-bit DWORD that encodes a byte address, a bit offset (lower 3 bits), and a single memory area (bits 24-31). It is used by indexed instructions such as DBD[ptr]. An ANY pointer is an 80-bit (10-byte) descriptor used by parameter types of type ANY, block-move instructions, and SFC/SFB calls; it encodes a syntax ID, repeat count, DB number, area code, and full byte address.

Why does my S7-1500 reject DBD[ptr] in an optimized FB?

Optimized blocks hide the absolute byte address of each tag from the compiler. An indexed DBD[ptr] requires that address to be a known fixed value, so the compiler refuses the construct. Either uncheck Optimized block access in the FB attributes, or migrate the access to SCL symbolic ARRAY[i] indexing, the AT operator, or the PEEK/POKE instructions.

How do I port a classical STL AccessArray function to TIA Portal?

Replace BLOCK_DB with DB_ANY if the caller may not know the DB number, replace the manual pointer build (SLD 3, P#0.0) with a typed POINTER TO ARRAY[..] OF DInt parameter, and read or write the element with ptr^[i]. The TIA Portal compiler generates the equivalent 32-bit pointer automatically.

Is PEEK on a VARIANT faster or slower than symbolic indexing?

Slower. PEEK performs a runtime type resolution against the VARIANT descriptor before reading memory, while symbolic array[i] indexing is resolved at compile time. Use PEEK only when the data type or the source block is not known at compile time, otherwise prefer symbolic indexing.

What is the maximum array size I can walk with a 32-bit DInt index?

A DInt index covers -2,147,483,648 to 2,147,483,647. The byte-address space of an S7-1500 data block is limited to 16 MB (16,777,216 bytes) per DB. The practical limit on a DInt array of 4 bytes per element is therefore 4,194,304 elements, far larger than any single DB can hold.

Back to blog