Indirect Addressing in Siemens STL Step-by-Step Programming Guide

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

Indirect Addressing in Siemens STL: Step-by-Step Programming Guide

Indirect addressing in Siemens STL (Statement List) lets a program compute an operand address at runtime instead of hard-coding it into the instruction. This is the foundation of compact, scalable PLC code: array scans, recipe handling, search routines, block moves, and any loop that must walk through inputs, outputs, flags, or data blocks without writing a unique instruction for every element.

This guide walks through the three canonical STL addressing forms used across the SIMATIC S7-300/400, S7-1200, and S7-1500 families: memory-indirect, register-indirect area-internal, and register-indirect area-crossing. Each form is shown with the exact STL instructions, the pointer format P#byte.bit, and a complete I/O copy example. The article closes with DB-indirect addressing, the 6-byte ANY pointer introduced in S7-1500, and a verification checklist you can run on a real CPU.

Toolchain baseline: Examples below are valid for STEP 7 V5.x on S7-300/400, STEP 7 Professional V13.1+, and TIA Portal V16–V20 on S7-1200/1500. The S7-1500 flavor is documented separately because the ANY-pointer format differs from the older S7-300/400 byte/bit pointer format. See the official Siemens documentation linked throughout.

1. Overview of Indirect Addressing Forms in STL

Siemens STL exposes three direct and three indirect addressing mechanisms. Choosing among them is driven by the data type of the address register, the memory area being referenced, and the bit offset granularity required.

Form Storage of Pointer Bit-Granular? Typical CPU
Memory-indirect MW / MD tag (WORD or DWORD) No (byte boundary only on S7-300/400) S7-300/400, S7-1500
Register-indirect, area-internal AR1 or AR2 with P#x.y Yes S7-300/400
Register-indirect, area-crossing AR1 or AR2 with area ID + P#x.y Yes S7-300/400
DB-indirect, area-internal AR with P#x.y, DB in DBO register Yes S7-300/400
DB-indirect, area-crossing AR with area ID + P#x.y Yes S7-300/400
ANY pointer (6-byte, bit-granular) VARIANT / ANY tag Yes S7-1500 / S7-1200

Siemens formally categorizes these as memory-indirect and register-indirect; the register-indirect branch then splits into area-internal (fixed memory area) and area-crossing (area in the pointer itself). The S7-1200/1500 documentation in the TIA Portal help distinguishes the same forms and adds the slice-based syntax for symbolic tags. See TIA Portal V20 — Addressing Operands Indirectly for the canonical reference.

2. The P#Byte.Bit Pointer Format

Every STL indirect operation uses a 32-bit pointer constant of the form P#byte.bit. The constant is constructed in STL with the P# prefix and is loaded into ACCU 1 with the L instruction. Internally the value is laid out as follows for S7-300/400:

Bit Position Field Meaning
31 … 24 Memory area ID (byte) 0x80=Inputs (I), 0x81=Outputs (Q), 0x82=Flags (M), 0x83=DB, 0x84=DI, 0x85=Local L, 0x86=VAT
23 … 16 Unused / area qualifier (S7-400) Generally 0 for area-internal, 0x84 (DB) for area-crossing DB
15 … 3 Byte address Byte offset within the area (0…65535)
2 … 0 Bit address Bit offset 0…7 (×8 encoded)

For S7-1500, the ANY pointer widens to 80 bits: a 16-byte structure carrying area ID, byte offset, bit offset, repetition factor, and source/destination DB. The TIA Portal help page Indirect Addressing (S7-300, S7-400) documents the legacy format; the equivalent S7-1500 page lives at Indirect Addressing in STL (S7-1500).

2.1 Loading a Pointer

// S7-300/400 syntax
L   P#8.7        // Load pointer to byte 8, bit 7 (i.e. I 8.7)
T   MD2          // Store the 32-bit value into flag double-word MD2

After this pair, the 32-bit value of MD2 is a valid pointer that can be consumed by any STL instruction accepting a [MD…] operand. The same pointer can also be loaded into address register AR1 with LAR1.

3. Memory-Indirect Addressing: Store the Pointer in a Tag

Memory-indirect addressing stores the pointer inside a regular operand tag (a WORD or DWORD in the M, L, D, or static area) and dereferences it inside square brackets. The CPU reads the 32-bit value of the tag, interprets it as a pointer, and resolves the operand at runtime.

3.1 First Example — Copying I 8.7 to Q 8.7

// Network 1: memory-indirect, input and output share one pointer in MD2
      L   P#8.7        // 1. Load pointer to I 8.7 into ACCU 1
      T   MD2           // 2. Transfer the pointer to flag double-word MD2
      A   I [MD2]       // 3. Query signal state at the addressed input bit
      =   Q [MD2]       // 4. Assign the same signal state to the output bit

On an S7-300 CPU 314, the value loaded by L P#8.7 is 16#0000_0087 (byte 8 → bits 3..15, bit 7 → bits 0..2 with the 3-bit shift). When I [MD2] executes, the CPU uses MD2 as a pointer into the process image of the inputs (PII) and reads bit I 8.7. The same pointer is reused for Q [MD2], writing to bit Q 8.7 of the process image of the outputs (PIQ).

On an S7-1500, memory-indirect addressing is available in STL only when the pointer tag is a DWORD. Word-sized tags are rejected by the editor with the diagnostic "Invalid data type for indirect addressing". See the TIA Portal help topic Indirect Addressing in STL (S7-1500) for the exact rules.

3.2 Common Errors with Memory-Indirect

  • Tag type mismatch. A WORD tag works on S7-300/400, but on S7-1500 you must declare the tag as DWORD or LWORD. The compiler flags this with error 16#80C3 — Operand not allowed for this instruction.
  • Byte boundary only on S7-300/400. Because the lower three bits of the pointer are masked off for memory-indirect access, you cannot point to I 8.7 using memory-indirect addressing on an S7-300. Register-indirect is required for bit-granular pointers on the older family.
  • Process-image timing. Reading I [MD2] returns the value from the PII, not from the physical terminal. To force a fresh read use L PIB [MD2] with PEB/PAB, or call "DPRD_DAT" on a distributed I/O submodule.

4. Register-Indirect Area-Internal Addressing

Register-indirect addressing uses the two 32-bit Address Registers AR1 and AR2 built into every S7-300/400 CPU. The area-internal form fixes the memory area in the instruction itself; AR1/AR2 carry only the byte and bit offset. This is the form requested in the second snippet of the source thread.

4.1 Loading a Pointer into AR1 with LAR1

      L   P#8.7        // Load pointer to byte 8, bit 7 into ACCU 1
      LAR1             // Copy the 32-bit value from ACCU 1 into AR1
      A   I [AR1,P#0.0]    // Query the signal state at I 8.7 + 0.0
      =   Q [AR1,P#1.1]    // Assign the same state to Q (8.7 + 1.1) = Q 10.0

The notation [AR1, P#0.0] dereferences AR1 and adds the constant offset P#0.0 at the time the instruction executes. The compiler does not modify AR1 itself, so the same register can be reused across multiple instructions inside a network.

Bit-granular addressing is what distinguishes the register forms from memory-indirect. In the example above, the offset P#1.1 advances the address from 8.7 to 10.0: 8 + 1 = 9 bytes plus the carry of 1 bit, which adds 1 to the byte address (10), so the resolved bit is 10.0. The carry from the bit field into the byte field is a property of the STL pointer arithmetic, not of the application code.

4.2 Loop-Counter Variant

The real value of register-indirect addressing shows up when AR1 is updated each scan. A common pattern is to read an input word array and copy it to an output word array:

// Loop body — assume loop index in MW100 (0..n-1)
      L   MW100
      SLD 3             // Multiply by 8 to convert bit-0 pointer to byte pointer
      LAR1              // AR1 = P#(index*8).0
      L   IW [AR1,P#0.0]
      T   QW [AR1,P#0.0]
      L   MW100
      +   1
      T   MW100
      L   MW100
      L   16            // loop length
      <I               // loop if index < 16
      JC  LOOP

The SLD 3 (shift left double-word by 3) is a Siemens idiom: it converts an integer index into the equivalent byte-pointer shift because the bit field occupies the lower three bits of the pointer word.

5. Register-Indirect Area-Crossing Addressing

Area-crossing addressing encodes the memory area inside AR1 or AR2, which lets a single instruction access different areas (I, Q, M, DB) without modifying the instruction itself. The third snippet from the source thread demonstrates this with two registers:

// Network 1: area-crossing using P# I 8.7 and P# Q 8.7
      L   P#I8.7        // Load pointer with area ID 'I' and address 8.7
      LAR1              // AR1 = 0x80 00 00 87
      L   P#Q8.7        // Load pointer with area ID 'Q' and address 8.7
      LAR2              // AR2 = 0x81 00 00 87
      A   [AR1,P#0.0]   // Query signal state at I 8.7
      =   [AR2,P#1.1]   // Assign to Q (8.7 + 1.1) = Q 10.0

Notice that the instruction A [AR1, P#0.0] no longer has the I prefix; the area is carried in the pointer. The two-register pattern is the cleanest way to perform a function on a source area and a destination area of different memory classes inside a single network — for example scanning inputs and writing flags, or reading a DB and writing the process outputs.

5.1 Area-Crossing Pointer Encodings

Source Area ID byte Example Resulting 32-bit value
P#I8.7 0x80 Input bit 8.7 16#8000_0087
P#Q10.0 0x81 Output bit 10.0 16#8100_0000
P#M20.3 0x82 Flag bit 20.3 16#8200_00A3
P#DB5.DBX10.0 0x83 (with 0x84 byte for DB5) DB5 data bit 10.0 16#8405_0050
P#DI6.DIX10.0 0x83 (with 0x85 byte for DI6) Instance-DB bit 10.0 16#8506_0050
P#L12.4 0x86 Temporary local bit 12.4 16#8600_0092

These encodings are documented in the STEP 7 programming manual "Programming with STEP 7" — see Indirect addressing in STL — Support entry 109011420. The same article clarifies the two-byte interpretation used on S7-400 (bits 16–23 carry the DB number) versus the simpler one-byte form on S7-300.

6. DB-Indirect Addressing

For data-block oriented loops (most recipe, logging, and array code), DB-indirect addressing combines a pointer in AR with a DB register (DBO for the open DB, DB1 for the open instance DB). The instruction form looks like A DBX [AR1, P#0.0] or L DBD [AR2, P#4.0].

// Walk through DB10 words DBW0..DBW98 in 2-word steps
      OPN  DB10             // Open DB10 in DB register 0
      L     P#0.0
      LAR1
LOOP: L     DBW [AR1,P#0.0]
      T     MW 200          // Move word to flag area for processing
      +AR1  P#2.0           // Advance pointer by 2.0 (2 bytes, 0 bits)
      L     AR1
      L     P#100.0         // End address 100.0
      <D
      JC    LOOP

The +AR1 instruction (and its +AR2 counterpart) is the only STL instruction that modifies AR1/AR2 as a side effect. Use it carefully: +AR1 P#2.0 advances by 2 bytes and 0 bits, +AR1 P#0.1 advances by 0 bytes and 1 bit, and the carry from the bit field into the byte field is automatic.

7. Indirect Addressing on S7-1200 and S7-1500

The S7-1200/1500 family does not have a programmer-visible AR1/AR2 — those registers are hidden inside the compiler. STL on those CPUs uses tag-based indirect addressing, with the pointer carried in a DWORD or LWORD tag. The TIA Portal help page Indirect Addressing in STL (S7-1500) shows the syntax:

// S7-1500 STL — pointer in a DWORD tag "ptrTemp"
      L   P#10.0          // Pointer to byte 10, bit 0
      T   "ptrTemp"       // Store in a DWORD tag
      A   I ["ptrTemp"]   // Bit-granular access is allowed
      =   Q ["ptrTemp"]

For symbolic tags, the S7-1500 supports slice access ("MyDB".MyArray[%i]) which is closer to high-level languages. Slice access is faster to type-check, easier to read in cross-reference, and the preferred form for new code; STL memory/register indirect addressing remains useful for legacy ports and for hand-optimized scan loops.

7.1 ANY Pointer (6-byte / 80-bit)

On S7-1500, indirect block moves and parameter passing use the ANY pointer, a 16-byte structure (or 80-bit packed form) defined as:

Bytes Field Meaning
0..1 Syntax ID / flags 0x10 = data, 0x12 = block, etc.
2..3 Data type 0x0001 BOOL, 0x0002 BYTE, 0x0004 WORD, 0x0006 DWORD, …
4..5 Count Number of elements
6..7 DB number 0 for non-DB areas
8 Memory area ID Same codes as the legacy 32-bit pointer
9..12 Byte offset (32-bit) Byte address in the area
13 Unused / alignment Reserved
14..15 Bit offset (16-bit) Bit address 0..7

This wider pointer lets the S7-1500 carry both an area and a typed array length, which is why POKE_BLK, FILL_BLK, and the symbolic "DWORD"[index] access all work without explicit AR registers.

8. Step-by-Step Procedure: Implement an Indirect Copy Block

The following procedure ports the source-thread examples into a reusable FC that copies 16 input words from IW 0..IW 30 to QW 0..QW 30, on an S7-300 CPU running STEP 7 V5.5.

8.1 Prerequisites

  • STEP 7 V5.5 SP2 (or TIA Portal V13.1+ for S7-1500).
  • SIMATIC S7-300 with CPU 314 or higher, or S7-1500 with CPU 1511 or higher.
  • An FB (e.g. FB1) or FC (e.g. FC1) in the S7 program, called once per scan in OB1.
  • Symbols: iPointer (DWORD, in M area), iIndex (INT, in M area), iLength (INT, in M area).

8.2 Build the Function

  1. In the project tree, right-click Blocks > Insert New Object > Function. Name it FC100_IndirectCopy, language STL.
  2. Declare the temp/static area:
    VAR
      iPointer : DWORD;  // 32-bit pointer
      iIndex   : INT;    // 0..15
    END_VAR
  3. Network 1: initialise the pointer.
          L   P#0.0
          T   #iPointer
  4. Network 2: initialise the loop index.
          L   0
          T   #iIndex
  5. Network 3: loop body — load word, transfer, advance, test.
    LOOP: L   IW [AR1,P#0.0]
          T   QW [AR1,P#0.0]
          +AR1 P#2.0
          L   #iIndex
          +   1
          T   #iIndex
          L   16
          <I
          JC  LOOP
  6. Save and download to the CPU.

9. Verification Checklist

Run the following checks before declaring the indirect block production-ready. The same checks apply to S7-1500 with slice syntax substituted for the STL square-bracket form.

  1. Static analysis. In LAD/FBD/STL editor, choose Edit > Check Block Consistency. Any unresolved pointer or tag-type mismatch appears in the message window with the line number.
  2. Online / Monitor. Open the block online and add #iPointer, AR1, and AR2 to the watch table. Confirm that the pointer advances by exactly P#2.0 per loop iteration.
  3. Cross-reference. Use Options > Reference Data > Display to confirm that the pointer is loaded exactly once per scan and is not aliased by a parallel network.
  4. Boundary test. Force #iIndex := 15 and step the program one cycle. The last access must be IW 30 and QW 30; any out-of-range access raises a S7-300 diagnostic SF (system fault) with error code 16#4003 — area length error in the diagnostic buffer.
  5. Process image. Toggle a physical input wired to I 0.0, confirm that the corresponding Q 0.0 follows within one OB1 cycle. For a word loop, write a known value to IW 0 from a HMI tag and verify QW 0 changes.
  6. Cycle time. Record OB1 execution time before and after the indirect block. A 16-iteration loop should add < 0.05 ms on a CPU 314C-2. A regression here usually means the loop counter has been promoted to a slow data type (REAL) by mistake.
Error-code reference: Pointer faults on S7-300/400 raise diagnostic-buffer event ID 16#4003 ("area length error during write/read") and 16#2522 ("area pointer error"). On S7-1500 the equivalent events are 16#007F (peripheral access error) and 16#0503 (range length exceeded). Always check the diagnostic buffer first when an indirect block behaves unexpectedly.

10. Troubleshooting Matrix

Symptom Likely Cause Diagnostic Fix
Compiler error "Invalid pointer format" Pointer tag is declared as WORD, not DWORD Symbol table Change tag type to DWORD; reload block
SF LED lit, diagnostic buffer 16#4003 Loop overruns the area end Diagnostic buffer Clamp #iIndex to area length; verify on online monitor
Output bit never updates despite input toggling Pointer loaded once in OB1 startup only Cross-reference Reload pointer at the top of the loop; check OB1 cycle
Outputs are always zero Process image write is masked by a higher-priority OB (e.g. OB35) Watch table PIQ Move the indirect block into OB35 or use L PQB direct write
Watch table shows AR1 = 0 Pointer loaded into AR1 inside a different block without LAR1 Source code search Re-execute LAR1 at block entry
S7-1500 STL rejects [AR1,P#1.1] AR1/AR2 are not visible on S7-1500 Compiler error Use tag-based indirect addressing I["ptr"] or slice access "Tag".Bit[%i]
Bit-granular pointer silently aligned to byte on S7-300 Memory-indirect used where register-indirect is required Online monitor of the pointer word Switch to LAR1 form or move to S7-1500

11. Field-Proven Tips and Edge Cases

  • Prefer symbolic tags on S7-1500. Slice access "Recipe".Value[%i] is type-checked at compile time, which removes the entire class of pointer-arithmetic mistakes described in the troubleshooting matrix above.
  • Reset AR1/AR2 at block exit. In multi-instance FBs the address registers may be reused by the calling block. The STEP 7 programming guideline recommends saving and restoring AR1/AR2 on FC entry/exit when the FC uses indirect addressing.
  • Use +AR1 P#<constant> for clarity. Direct addition of two AR registers is rarely needed and is a frequent source of off-by-one bit errors.
  • Avoid STRING/VARIANT loops on S7-300/400. The byte/bit pointer format is intended for elementary data types. For variable-length data move to S7-1500 with the ANY pointer or to standard MOVE_BLK.
  • Watch the area qualifier byte on S7-400. When you copy a pointer from an S7-300 program and run it on an S7-400, the upper-byte interpretation changes. Re-validate every P#DB… constant in the S7-400 project.
  • Pointer wraparound. If +AR1 P#<delta> would carry past byte 65535, the result wraps to 0 on S7-300. Add a defensive clamp before the dereference if the upper bound is close.

What is the difference between memory-indirect and register-indirect addressing in Siemens STL?

Memory-indirect addressing stores the 32-bit pointer in a WORD or DWORD tag (e.g. MD2) and dereferences it as [MD2]. Register-indirect addressing stores the pointer in address register AR1 or AR2 and dereferences it as [AR1, P#offset], which is the only form that allows bit-granular offsets on S7-300/400. The TIA Portal help pages Indirect Addressing (S7-300, S7-400) and Indirect Addressing in STL (S7-1500) cover both forms.

Why does I [MD2] not point to bit 7 on an S7-300 CPU?

On S7-300/400, memory-indirect addressing ignores the lower three bits of the pointer (the bit field) and aligns the access to the next byte boundary. To reach I 8.7 bit-accurately you must use register-indirect addressing with LAR1 and I [AR1, P#0.0], or move the program to an S7-1500 where bit-granular memory-indirect is supported.

How do I load a pointer with an explicit memory area into AR1 or AR2?

Use the P# constant with the area prefix, e.g. L P#I8.7 for an input, L P#Q10.0 for an output, L P#M20.3 for a flag, or L P#DB5.DBX10.0 for a data-block bit. Then execute LAR1 (or LAR2) to copy the 32-bit value from ACCU 1 into the address register. The dereference form is then A [AR1, P#0.0] with no I/Q/M prefix.

What is the S7-1500 equivalent of AR1/AR2 indirect addressing?

The S7-1500 hides the AR registers from the programmer. Use a DWORD tag (e.g. "ptrTemp") as the pointer, load it with L P#byte.bit and T "ptrTemp", and dereference with I["ptrTemp"] or Q["ptrTemp"]. For symbolic data blocks, prefer the slice form "MyDB".MyArray[%i] which is type-checked by the compiler.

Which diagnostic-buffer events indicate an indirect-addressing fault?

S7-300/400 raise event 16#4003 ("area length error during write/read") and 16#2522 ("area pointer error") when a pointer walks past the area boundary. S7-1500 raises 16#007F for peripheral access errors and 16#0503 for range-length exceeded. Read the diagnostic buffer first whenever an indirect block lights the SF LED or stops updating outputs.

Back to blog