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.
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
WORDtag works on S7-300/400, but on S7-1500 you must declare the tag asDWORDorLWORD. 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.7using 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 useL PIB [MD2]withPEB/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
- In the project tree, right-click Blocks > Insert New Object > Function. Name it
FC100_IndirectCopy, language STL. - Declare the temp/static area:
VAR iPointer : DWORD; // 32-bit pointer iIndex : INT; // 0..15 END_VAR - Network 1: initialise the pointer.
L P#0.0 T #iPointer - Network 2: initialise the loop index.
L 0 T #iIndex - 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 - 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.
- 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.
-
Online / Monitor. Open the block online and add
#iPointer,AR1, andAR2to the watch table. Confirm that the pointer advances by exactlyP#2.0per loop iteration. - 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.
-
Boundary test. Force
#iIndex := 15and step the program one cycle. The last access must beIW 30andQW 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. - 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.
- 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.
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.