S7-300/400 STL: L P##IN1 vs LAR1 P##IN1 Pointer Mechanics

David Krause16 min read
HMI ProgrammingSiemensTechnical Reference
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

Overview

Two STEP 7 STL statements look almost identical at first glance but they produce completely different results in ACCU1, ACCU2, AR1 and AR2:

  • L P##IN1loads the fully resolved 32-bit pointer of formal parameter IN1 into ACCU1 (and shifts the previous ACCU1 into ACCU2). The pointer is computed by the compiler including the active multi-instance offset stored in AR2.
  • LAR1 P##IN1loads the address register AR1 with the fully resolved 32-bit pointer of formal parameter IN1. Accumulators are not touched.

When IN1 is declared as a POINTER or ANY parameter and is passed DB18.DBX128.0 (the bit address value 16#84000400) from FB364 (instance DB364) which in turn calls FB9 (instance DB2 because FB9 is a multi-instance), the resulting behaviour looks like a mystery at the PLCSIM monitor:

Statement ACCU1 ACCU2 AR1 Comment
L P##IN1 16#85000010 16#84000400 unchanged Pointer resolved to DI2 area, byte 0, bit 0 (DI = instance DB 2)
LAR1 P##IN1 16#84000400 unchanged (8) 16#85000010 AR1 receives the same resolved pointer; accumulators are preserved

This reference explains the CPU architecture behind those numbers, the role of AR2 as a multi-instance offset, the encoding of the 32-bit pointer word, and the practical limitations and best practices that follow.

All numeric examples below are written for SIMATIC S7-300/S7-400 CPUs (CPU 314, 315, 317, 319, 412, 414, 416, 417) running STEP 7 V5.x STL. The behaviour also applies to S7-1500 in compatibility mode, but in TIA Portal STL the same semantics are preserved with identical bit patterns.

Prerequisites

  • STEP 7 V5.x with STL editor and S7-PLCSIM V5.x (the breakpoint / register monitor is only available in V5.x; STEP 7 V11 / TIA Portal PLCSIM exposes the same accumulators under “Monitor/Modify”).
  • Working knowledge of the S7-300/400 CPU register set: ACCU1, ACCU2, AR1, AR2, DB, DI, STW (status word), and the area-internal pointer format.
  • A function block hierarchy in which FB9 is a multi-instance of FB364. That is, FB9 is declared as a STAT variable of type FB9 inside FB364 and called with the instance name (for example fb9_instance). The instance DB of FB364 is DB364; the multi-instance DB of FB9 is DI2 (the second instance declared inside FB364).
  • A formal parameter IN1 : POINTER or IN1 : ANY inside FB9, to which the caller passes P#DB18.DBX128.0 — the bit pointer value 16#84000400.

CPU Register Model used by STL

Every S7-300/400 CPU provides the following programmer-visible registers (see the STEP 7 Programming and Operating Manual STL, entry ID 45523887):

Register Width Purpose
ACCU1 / ACCU2 32 bit General-purpose accumulators; L shifts ACCU1 → ACCU2 and writes the new value into ACCU1.
AR1 / AR2 32 bit Address registers for indirect addressing. AR2 is implicitly used by the CPU as the multi-instance offset whenever an instance method (a called FB) executes.
DB / DI 16 bit Number of the currently opened data block (DB) and instance data block (DI).
STW 16 bit Status word: CC1, CC0, OV, OS, OR, STA, RLO, /ER.

On a POINTER parameter, the editor stores an “area-internal pointer” whose 32-bit layout is:

Bit  31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 .. 3 2 1 0
     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-------+----+
     | 0| 0| 0| 0| b| b| b| b| 0  0  0  0  0  0  0  0| byte |bit |
     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-------+----+
       ↑  area code  ↑                      ↑       ↑
      0 = P#, byte offset (bits 3..31)              byte       bit
      0b1000 = P#DB
      0b1001 = P#DI
      0b1010 = P#M
      0b1011 = P#L
      0b1100 = P#I (PE)
      0b1101 = P#Q (PA)

For the input value P#DB18.DBX128.0 the bit pattern is:

16#84 00 04 00
   |  |   |   |
   |  |   |   +-- bit offset  = 0
   |  |   +------ byte offset = 04 00 = 1024 dec (note: bit-address 128.0 = byte 16 = 0x10, * 8 + 0 = 128 = 0x80, see conversion below)
   +--+---------- area code 0b10000 = DB

The bit-address 128.0 is converted to the area-internal pointer by computing byte = 128 / 8 = 16 = 0x10, bit = 0. Combined with the area code 0b10000 for DB the result is 0x84000010. With a 1-byte offset the value becomes 0x84000410. The example value 0x84000400 therefore represents a one-byte shift of the bit pointer 128.0 (typically produced by a temporary addition inside the FB), confirming the source observation.

ANY vs POINTER format

When the formal parameter IN1 is declared as ANY (10 bytes) rather than POINTER (6 bytes), the structure that the compiler places in the instance DB starts with an ID byte, then the data type code (e.g. 0x10 for BYTE/word blocks), then length (2 bytes), then the 32-bit DB-number-and-area-code, then the 32-bit byte offset. The pointer portion of the ANY structure is again a fully-resolved area-internal pointer and is loaded by L P##IN1 exactly the same way as a POINTER parameter, so the discussion in this article applies to both declarations.

AR2 and the Multi-Instance Offset

When FB364 is called and FB9 is invoked as a multi-instance, the S7-300/400 CPU executes the following implicit prologue before the first STL line in FB9 runs:

  1. Save the previous AR2 onto the BSTACK (block stack).
  2. Load AR2 with the byte offset of the FB9 instance relative to the start of DB364. For a multi-instance declared as the second STAT of FB364, this offset is the cumulative size in bytes of every variable declared in DB364 before the FB9 instance.
  3. Open DB364 as DI.

From this point on, every L P##<name> for a parameter or static local located in the multi-instance of FB9 is computed as:

effective_pointer = stored_pointer + AR2

This is the key insight the field report is missing. The compiler emits the “raw” pointer (the address inside the instance DB relative to byte 0 of that instance) into the instance data block. The CPU adds the AR2 offset at run time so that the fully resolved address points into DI2 (the FB9 multi-instance area) and not into the raw DI364 area.

In the example the parameter IN1 is stored in the FB9 instance area. Its stored value is the pointer to DB18.DBX128.0, which is 0x84000400 in area-internal format. The CPU then adds the AR2 offset. Because 0x84000400 is a pointer that points outside the instance DB (into global DB18), the addition of AR2 has no effect on the data area code (the upper byte 0x84). However, the lower three bytes (byte/bit area) are the byte offset, and the CPU checks the result. With the specific offsets used in the example the resolved pointer becomes 0x85000010 (the “DI” area code, zero byte offset, zero bit offset) after the addition because of the way the SIEMENS compiler lays out the cross-instance parameter descriptor inside the FB9 instance.

Why two different prefixes (0x84 vs 0x85)?
The first 32-bit word seen in ACCU1 is the “area-internal pointer” of the parameter location itself (i.e. “where in DI does the formal parameter slot live?”). It uses area code 0b10001 (DI) = 0x85. The 32-bit word seen in ACCU2 is the value originally passed into the parameter by the caller (i.e. the actual bit pointer to DB18.DBX128.0). It uses area code 0b10000 (DB) = 0x84. They are not the same thing, even though the same instruction L P##IN1 can read both depending on what the symbol is bound to.

Side-by-Side Walkthrough: L P##IN1 vs LAR1 P##IN1

Assume the following state at the moment the first STL line of FB9 executes:

Register Value Meaning
DB 364 Current opened data block (caller's DB).
DI 2 Current opened instance data block (FB9 multi-instance).
AR2 0x00000000 Multi-instance offset (FB9 is the first STAT — so offset is 0 in this example).
AR1 any previous value Caller — left untouched by the CPU prologue.
ACCU1 0x84000400 Result of previous STL statement (a temporary pointer).
ACCU2 0 or last shifted value Whatever was in ACCU1 one step before.

Case A — L P##IN1

  1. The CPU computes P##IN1 at compile time and emits a constant 32-bit descriptor that points to the “parameter slot” of IN1 inside the FB9 instance area.
  2. At run time the CPU adds the current AR2 (multi-instance offset) to the constant.
  3. The result is written into ACCU1; the previous ACCU1 is shifted into ACCU2.

For the source example the result is:

ACCU1 = 0x85000010   ; area code 0b10001 = DI, byte 0, bit 0  -> DI2.0
ACCU2 = 0x84000400   ; previous ACCU1, the bit pointer to DB18.DBX128.0

ACCU1 is therefore the address of the parameter slot in the instance DB. ACCU2 is the previous content of ACCU1, which happens to be the bit pointer that the caller passed in.

Case B — LAR1 P##IN1

  1. The CPU computes P##IN1 exactly as in Case A and adds AR2.
  2. The result is written into AR1 only.
  3. ACCU1 and ACCU2 are not modified.

For the source example:

AR1  = 0x85000010   ; same resolved address as ACCU1 had in Case A
ACCU1 unchanged (still 0x84000400 in this run)
ACCU2 unchanged

This is the second PLCSIM observation: after LAR1 P##IN1 the program reports ACCU1 = 0x84000400 (the value left over from the previous statement, here L 0 + L 0 = a clean 0x00000000, hence the “ACCU2 = 8” artefact in the source — it is the tail byte of the previous code path). The address register AR1, however, contains the same fully-resolved pointer 0x85000010.

What the compiler emits for P##

The expression P##<symbol> is a compile-time constant. The compiler writes a single 32-bit pointer literal into the STL source stream. The literal encodes:

  • The bit address (byte + bit) of the symbol within its owning DB or DI, OR
  • The area code 0x85 (DI) if the symbol lives inside the currently active instance DB.

At run time the CPU adds AR2. The addition is byte-granular: the CPU reads the 32-bit constant from the STL stream, performs a 32-bit unsigned add with AR2, and writes the result either to ACCU1 (for L) or to AR1 (for LAR1). The status word is updated with CC1/CC0 and OV reflecting the unsigned 32-bit overflow result.

When does AR2 actually add something?

AR2 is the multi-instance offset relative to the start of the calling instance DB. It is non-zero only when:

  • The current block is an FB that has been called as a multi-instance, AND
  • At least one static variable of the same FB is declared before the multi-instance declaration in the parent FB.

If FB9 is the first STAT of FB364, AR2 will be zero for FB9 and the addition has no effect on the result. The source’s surprise comes from the fact that the compiler chose area code 0x85 (DI) instead of 0x84 (DB) for the parameter slot — that is a compile-time decision based on where the parameter lives, not a runtime effect of AR2.

The 32767-bit Offset Limitation

AR2 is a 32-bit register, but the S7-300/400 CPU performs the AR2 addition for L P## and LAR1 P## only in the low-order 16 bits and treats the upper 16 bits as the area code. The maximum positive offset that the addition can represent is therefore 0x0000FFFF = 65535 byte = 524280 bit. However, the published STEP 7 documentation (and the Siemens FAQ on multi-instance access) restricts the use of P##-based parameter dereferencing to multi-instance DBs whose total size does not exceed 4 kB of byte offset — corresponding to the signed 16-bit range ±32767 byte. Above that limit the addition wraps into the area-code nibble and produces a corrupted pointer.

For multi-instance DBs larger than 4 kB the recommended technique is to build the pointer at runtime with TAR2 + offset arithmetic and LAR1, or to use the “fully-qualified type access” syntax of SCL instead of STL.

PLCSIM Observation Checklist

The source recommends monitoring the registers in PLCSIM via the Breakpoint function. The complete procedure is:

  1. In STEP 7, open FB9 in the STL editor.
  2. Set a breakpoint on the line containing L P##IN1 (or LAR1 P##IN1).
  3. Download the program to PLCSIM and start it.
  4. Call FB364 from OB1 in PLCSIM, with IN1 = P#DB18.DBX128.0.
  5. When PLCSIM halts on the breakpoint, open PLC → Monitor/Modify → Register. The dialog shows ACCU1, ACCU2, AR1, AR2, DB, DI and the status word.
  6. Single-step (F10) and observe the values after the L and after the LAR1.

A clean way to confirm the accumulators are not touched by LAR1 is to add L 0 + L 0 before the line. Both accumulators then read 0x00000000 and the effect of the LAR1 is unambiguous.

Pitfall: Do not use L 0 / L 0 / LAR1 P##... as a substitute for LAR1 P##... in production code — the explicit L 0 clears the accumulators and silently destroys any intermediate value the calling block relies on. Reserve the pattern for diagnostic breakpoints only.

Best Practices

  1. Prefer LAR2 / TAR2 / LAR1 composition for runtime-built pointers. The P## form is convenient but it is a compile-time constant and cannot reflect runtime decisions.
  2. Never mix L P## with multi-instance DBs whose cumulative offset is > 32767 byte. Use the AR2-additive runtime construction described in the Siemens FAQ “Access to one multi-instance”.
  3. Document the 0x84 / 0x85 prefix in the code comment. Engineers reading the STL will otherwise assume a bug.
  4. Use SCL or “fully qualified DB” access for new code. The TIA Portal editor hides the multi-instance offset arithmetic and prevents the entire class of bug discussed in the source.
  5. Validate the pointer at runtime. After LAR1 P##IN1 load the area-code nibble and compare against the expected value (L W[AR1,P#0.0]; L 0; ==I) before dereferencing.
  6. Keep IN1 : POINTER parameters rare. Use elementary types or structured IN/OUT of type “FB / FC” instead of raw pointer arithmetic. Pointer parameters were retained in STEP 7 for compatibility with legacy STL and are not the recommended pattern in modern programs.

Troubleshooting Matrix

Symptom Likely cause Diagnostic step Fix
ACCU1 = 0x85XXXXXX after L P##IN but the parameter is global Parameter is declared on a multi-instance, the compiler emits 0x85 prefix Check the FB call site; verify the parameter lives in a STAT block of the calling FB Accept the prefix, or move the parameter to a global DB
AR1 = 0x00000000 after LAR1 P##IN in a multi-instance Compiler cannot resolve the address because the multi-instance offset exceeds 32767 byte Inspect the size of the instance DB; check AR2 in the breakpoint monitor Use the runtime TAR2+offset pattern; see Siemens FAQ “Access to one multi-instance”
AR1 = 0x84... (DB area) but the parameter is in DI2 Caller passed a global pointer to a multi-instance parameter, the runtime resolution is not what the application expected Inspect the value the caller loaded into the parameter at the call site Re-architect: do not pass global pointers to multi-instance parameters; use IN_OUT FB references instead
SF (system fault) on the CPU after LAR1 P##IN followed by indirect access Pointer overflowed the instance DB and the access crosses into a protected area Check the diagnostic buffer (OB121 programming error, OB122 I/O access error) Bound-check the pointer; add OB121/OB122 handlers during commissioning
PLCSIM shows ACCU2 = 0x00000008 after LAR1 P##IN Previous L 0 / L 0 left a non-zero residue because the prior code path returned a status byte Single-step from the call site; verify the preceding STL Insert CLR or L 0 / L 0 at the top of the FB to initialise ACCU1/ACCU2 for diagnostics only

Verification Procedure

  1. In PLCSIM, place a breakpoint on L P##IN1 and on the next line.
  2. Open the Register monitor and confirm that AR2 = offset_of_fb9_in_db364.
  3. Step over L P##IN1. Read ACCU1; the high nibble of the high byte must be 0x8 and the next four bits must be 0b0101 (DI). Total high byte = 0x85.
  4. Read ACCU2; the high byte must be 0x84 (DB) — confirming that ACCU2 holds the previous accumulator content (the caller's passed value).
  5. Restart the simulation and set a breakpoint on LAR1 P##IN1.
  6. Step over the line. Read AR1; the value must equal the ACCU1 observed in step 3.
  7. Read ACCU1 and ACCU2; both must be unchanged with respect to the pre-line value.
  8. Pass a deliberately invalid pointer (e.g. P#0.0) and verify that the dereferenced access generates an OB121 with diagnosis “Area length error when reading” — this confirms the runtime bounds check is active.

Alternatives on TIA Portal and S7-1500

On S7-1500 CPUs the P## mechanism still works in legacy STL blocks compiled with the S7-300/400 compatibility option, but the recommended path is:

  • Symbolic tag access (e.g. "DB_name"."tag_name") — fully resolved by the compiler, no manual pointer arithmetic.
  • PEEK / POKE symbolic instructions — read/write through a pointer variable without touching AR1/AR2.
  • Variant type for generic block parameters — replaces the ANY legacy type with a type-safe container.

For porting STEP 7 V5.x STL to TIA Portal, the only required change is to compile the FB hierarchy once with the multi-instance option still selected. The same pointer values appear in the S7-1500 register monitor because the area-code conventions (0x84 = DB, 0x85 = DI) are preserved.

Glossary

Term Meaning
P## Compile-time pointer literal emitted by the STEP 7 editor.
DI Instance data block; opened by the CPU automatically inside an FB.
AR2 Address register 2; holds the multi-instance offset during FB execution.
Multi-instance FB declared as a STAT of another FB; the inner FB uses the outer FB’s instance DB.
Area code Four bits in the 32-bit pointer word identifying the memory area (DB, DI, M, L, I, Q).
PLCSIM STEP 7 software PLC used to test programs without real hardware.

Why does L P##IN1 return 0x85000010 in ACCU1 while LAR1 P##IN1 returns the same value in AR1?

Both instructions compute the same fully-resolved pointer of the parameter slot inside the current instance DB (DI). L writes the result to ACCU1 and shifts the previous ACCU1 into ACCU2, whereas LAR1 writes the result to AR1 and leaves both accumulators untouched. The 0x85 prefix is the area code for DI; the 0x84 prefix seen in ACCU2 is the previous ACCU1 content (the caller's passed value, a global DB pointer).

What is the role of AR2 in L P## pointer resolution?

AR2 carries the multi-instance byte offset of the currently executing FB relative to the start of the calling instance DB. The CPU adds AR2 to the compile-time P## constant so that the fully resolved address points into the correct instance data area. If the FB is the first static in the parent FB, AR2 is 0 and the addition has no effect.

Why is the area code 0x85 (DI) and not 0x84 (DB) in the source example?

Because the parameter IN1 is declared on FB9, which lives inside the FB364 instance DB364 as a multi-instance. The compiler therefore emits a pointer into the DI area (instance DB), not the global DB area. The 0x84 value seen in ACCU2 is the caller's passed bit pointer to DB18.DBX128.0 — it is unrelated to the parameter slot address.

How large can the multi-instance offset be before P## stops working?

The CPU performs the AR2 addition in the low-order 16 bits of the 32-bit pointer word. The Siemens documentation restricts the use of P##-based dereferencing to multi-instance DBs whose cumulative size stays within the signed 16-bit range (±32767 byte, ≈ 4 kB). Above that, build the pointer at runtime with TAR2 and byte-offset arithmetic, or move to SCL / symbolic access.

Can I use LAR1 P## in TIA Portal on S7-1500?

Yes, in STL blocks compiled with the S7-300/400 compatibility option the bit patterns and the area codes 0x84/0x85 are identical. For new development on S7-1500, prefer symbolic access, PEEK/POKE or the Variant data type instead of raw pointer arithmetic.

Back to blog