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:
- Generate one explicit statement per element (wasteful and bounded by code size).
- Use a
FORloop with an index variable on a directly-indexed array element (clean, but the array must be declared in a known DB at compile time). - 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.
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
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:
- Direct symbolic indexing on a declared
ARRAY. - The
PEEKandPOKEinstructions that read or write a slice at aPOINTERorVARIANTaddress. - The
AToperator 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 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
- Create a non-optimized DB
DB_Recipewith anARRAY[0..1000] OF DIntstarting 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. - Import the FC
AccessArrayas 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. - In OB1 or a cyclic OB, call the FC and pass a witness value for
i_n(e.g. 17). Monitor the in/outio_valueand the source DB online. Confirm the value appears in the indexed element. - Set a breakpoint after the pointer build and read
Address_POIas a DWORD. Validate that the lower 24 bits represent the byte address (multiplied by 8) and the lowest 3 bits are zero. - Drive the loop version with
I_COUNT = 1001and check the first 10 elements after the call. Use Monitor with trigger on the value 0 to confirm write-side correctness. - 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 symbolicARRAYindexing. - 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
ANYand thePOINTERkeyword both still exist. TheVARIANTis 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_DBparameters when the caller may not know the DB number at compile time. Use it in conjunction withDB_OpenSimpleDBandDB_GetFieldfrom 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/POKEcall site handles theVARIANTvalidity return code before the data is used. - [ ] Bulk operation is implemented with
BLKMOV/MOVE_BLKfor any array larger than ~32 elements to keep loop time under one millisecond.
15. Related Documentation
- TIA Portal V20 Help - Indirect Addressing Using a Pointer (S7-1200, S7-1500)
- Siemens Support - Indirect addressing via pointer (STEP 7 Professional V13.1)
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.