Decrementing AR1 in Siemens S7: Indirect Addressing Methods

David Krause15 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 of AR1/AR2 in Siemens S7-300/400

Every S7-300 and S7-400 CPU exposes two 32-bit address registers, AR1 and AR2, that act as the base pointer for memory-indirect and register-indirect STL operations. Instructions such as L MW [AR1, P#0.0], T DB[AR1].DBW0, A I [AR1, P#0.0], or = Q [AR1, P#0.0] dereference a memory location by combining the address register with an optional 16-bit constant offset (P#x.y).

AR1 is the general-purpose pointer register. AR2 is reserved by Siemens convention for the second pointer passed into FBs through DI/DO (instance data block), and by certain system functions. The CPU does not enforce this convention at runtime, but FB source code generated by STEP 7 follows it, and reassigning AR2 inside an FB can corrupt the implicit AR2-based parameter passing done by the caller's environment.

The STL instruction set gives engineers +AR1 and +AR2 (16-bit signed addition to the address register) and the LAR1/LAR2/TAR1/TAR2 load/transfer instructions. Notably absent is any -AR1 or -AR2 instruction. Decrementing the pointer requires either loading a negative constant into the accumulator and using +AR1, or pulling AR1 into the accumulator, doing integer math, and writing it back with LAR1. This technical reference documents each technique with STL examples, the bit-level pointer layout that determines when they work, and the edge cases that surface as field bugs.

Bit-Level Pointer Layout in AR1

Before manipulating AR1 you must understand the bit layout of the 32-bit pointer it stores. Bit 31 selects between two formats:

Format Bit 31 Bits 30-24 Bits 23-16 Bits 15-3 Bits 2-0
Area-internal 0 0000000 00000000 Byte offset (0-8191) Bit offset (0-7)
Area-crossing 1 00000 bbb (area ID) DB number or byte offset high Byte offset (0-8191) Bit offset (0-7)

Area ID (bbb in bits 26-24 of an area-crossing pointer):

bbb Area Typical Use
000 P Periphery (direct I/O)
001 I Process input image
010 Q Process output image
011 M Bit memory
100 DB Data block
101 DI Instance DB
110 L Local / temp
111 V Pre-V4 designation (legacy)

This layout drives a critical limitation of +AR1: the instruction only manipulates the low 16 bits of AR1 (the byte offset + bit offset). It never touches the high 16 bits, so it cannot move a pointer across area boundaries or change the DB number of a DB pointer. A subtraction implemented via +AR1 therefore behaves identically to an intra-area arithmetic operation on the offset portion of the pointer.

The +AR1 Instruction Variants

STL exposes three syntactic forms of +AR1. All three perform the same 16-bit signed add on the low word of AR1; only the source operand differs.

Form Effect Typical Use
+AR1 (no operand) AR1 = AR1 + ACCU1-L (16-bit signed) Add a precomputed dynamic offset
+AR1 P#x.y AR1 = AR1 + (x*8 + y) bits Add a constant compile-time offset
+AR1 <int> AR1 = AR1 + <int> (16-bit signed) Add a small integer literal

The constant forms are syntactic sugar; the S7 compiler/loader encodes P#x.y or the integer as a 16-bit value in ACCU1-L before issuing the no-operand +AR1 micro-operation. The hardware semantics are identical. The signed 16-bit range is -32768 to +32767. Because the pointer's low 16 bits encode byte+bit offset (byte in bits 15-3, bit in bits 2-0), the practical maximum offset per +AR1 is +8191 bytes + 7 bits forward or -8192 bytes backward, well within 16-bit signed range. The range limit does not bite in normal loop work because pointer walks of more than 16 KB are rare.

Why Siemens Does Not Provide a -AR1 Instruction

No -AR1 microcode exists on any S7-300 or S7-400 CPU. The +AR1 instruction predates the S7 line and was inherited from the S5-135U/155U family, which already had a similar limitation. Siemens has consistently declined to add a dedicated subtract instruction on the grounds that loading a negative constant and using +AR1 produces identical results with no cycle-time penalty.

Field-proven caveat: The cycle-time argument is only valid on CPUs that perform constant folding. On the older S7-300 CPUs (CPU 312 IFM through CPU 318-2 DP), every +AR1 with a constant operand consumes one extra word of MC7 code. Using LAR1 to load a precomputed pointer and then +AR1 with no operand sometimes compiles to a single MC7 instruction. Profile your hot loop in the PLC's Block Last Invocation Time diagnostic if cycle budget matters.

The trade-off is real but small. Most automation code calls +AR1 rarely (per loop iteration, not per scan), so the missing -AR1 is a documentation nuisance rather than a runtime problem.

Workaround 1: Negative Constant with +AR1

The shortest path to AR1 -= N is loading -N into ACCU1-L and executing +AR1. STL accepts a negative integer literal in the L instruction:

// Subtract 10 bytes (80 bits) from AR1
L     -80          // ACCU1-L = 16#FFF6 = -80 (signed)
+AR1               // AR1 = AR1 + (-80) bits = AR1 - 10 bytes

For symbolic offsets, precompute the bit count:

// Subtract pointer P#10.0 (= 10 bytes = 80 bits)
L     -80          // 16-bit constant load; lower word = -80
+AR1               // AR1 -= 10 bytes
Bit count discipline: AR1's low 16 bits store byte offset in bits 15-3 and bit offset in bits 2-0. To subtract N bytes, load -N*8. To subtract a P#x.y pointer, load -(x*8 + y). Misalignment off by one bit is the most common source of "I am reading the wrong word" bugs in legacy AR1 code.

Workaround 2: TAR1/LAR1 Round-Trip

When the value to subtract is computed at runtime (loop counter, parameter, or pointer arithmetic result) and cannot be expressed as a literal, route AR1 through the accumulator:

// AR1 := AR1 - MD100 (DINT, byte count)
TAR1              // ACCU1-L = current AR1 (low 16 bits)
L     MD100       // ACCU1-L = byte count to subtract (positive value)
NEGI              // ACCU1-L = -byte count (32-bit signed negate)
+AR1              // AR1 += ACCU1-L (low 16 bits) = AR1 - byte count

If you need to subtract a pointer value (P#) rather than a raw integer, the cleanest sequence is:

// AR1 := AR1 - P#4.0  (subtract 5 bytes worth of bits)
TAR1              // ACCU1 = AR1
L     P#4.0       // ACCU1 = P#4.0 (40 = 0x0028)
NEGI              // ACCU1 = -40 (0xFFFFFFD8 in 32-bit, 0xFFD8 in low word)
+AR1              // AR1 = AR1 + (-40) = AR1 - 5 bytes

This pattern keeps the symbolic P# in the source, which is more readable than a magic bit count, and works for any pointer-size operand from P#0.0 up to P#8191.7.

Note: the mnemonic CAR1 sometimes appears in legacy comments and informal notes; the correct instruction is TAR1 (Transfer AR1 to ACCU1). LAR1 is its inverse (Load ACCU1 to AR1). CAR is not a valid S7 instruction; treat it as TAR1.

Workaround 3: Full Pointer Reload with LAR1

If you want to set AR1 to an absolute pointer rather than walk it, just load the new pointer directly. No TAR1 step is required:

// AR1 := P#100.0  (offset 100 bytes into the current area)
LAR1   P#100.0     // 32-bit pointer load; AR1 fully overwritten

// AR1 := P#DB10.DBX0.0  (specific DB and offset)
LAR1   P#DB10.DBX0.0

This is not a decrement, but it is the answer in many real situations where the original question is "I want AR1 to point at byte N-K given it currently points at byte N." When N-K is known at compile time, reload the absolute pointer instead of subtracting. This avoids any accumulator overhead and produces a single MC7 instruction in most cases.

Application: AR1 Walking in FBs with ANY-Pointer Parameters

FBs declared with a VARIANT or ANY input parameter receive a 10-byte descriptor describing a source area. The classic pattern reads the source area byte-by-byte using AR1 as the walking pointer:

// Inside an FB (called via instance DB DI):
// Input:  srcAny : ANY
// Temp:   bytesLeft : INT,  srcDB : WORD
LAR1   P##srcAny   // AR1 -> srcAny parameter in DI
L     W [AR1,P#4.0] // load byte count from ANY body (bytes 4-5)
T     #bytesLeft
L     W [AR1,P#2.0] // load DB number from ANY body (bytes 2-3)
T     #srcDB
L     D [AR1,P#6.0] // load area-crossing pointer from ANY body (bytes 6-9)
LAR1                // AR1 now points to first source byte

To walk backwards through the source (e.g., for a reverse memcpy), apply Workaround 1 or 2 to AR1 inside the loop:

loop_back:
L     MW 100        // ACCU1-L = bytes processed so far
NEGI               // negate to make it negative
+AR1               // AR1 walks backward by that many bits

// ... read byte at new AR1 ...
L     DBB [AR1, P#0.0]

Because AR1 holds an area-crossing pointer at this point, the +AR1 only modifies the byte/bit offset portion. The DB number and area ID are preserved, so the dereference L DB[AR1].DBB0 still resolves to the correct data block.

ANY Pointer 10-Byte Layout

The 10-byte ANY structure used for parameter passing has a fixed layout. Knowing it lets you decompose the pointer loaded from srcAny:

Byte Offset Width Content
0-1 WORD ID: 16#10 = ANY (01=BYTE, 02=WORD, 03=DWORD, 04=REAL, 05=COUNTER, 06=TIMER)
2-3 WORD DB number (0 if not DB area)
4-5 WORD Byte count (length of pointed area)
6-9 DWORD Area-crossing pointer (bit 31=1) to first byte

The 4-byte pointer at offset 6-9 follows the area-crossing format from earlier. Loading it with L D [AR1, P#6.0] puts the full 32-bit value into ACCU1, ready for LAR1 or further decomposition with SLD/SRD rotates. VARIANT pointers (S7-1500) replace the 10-byte ANY with a structured handle plus an 8-byte pointer; the working principle is the same but the offsets differ. See the Siemens Industry Online Support entry on VARIANT pointer layout when migrating an FB from ANY to VARIANT.

Loop Pattern: Byte-by-Byte Copy

A common production code pattern copies N bytes from a source ANY to a destination ANY using AR1/AR2 as the walking pointers. The skeleton looks like:

// FC "ANY_COPY" : VOID
// IN:  src : ANY;  dst : ANY;  len : INT
LAR1   P##src
L     W [AR1,P#4.0]      // src byte count
T     #srcLen
LAR1   P##dst
L     W [AR1,P#4.0]      // dst byte count
T     #dstLen
L     #len
L     #srcLen
<I                      // IF len > srcLen THEN error
JC     err_short

LAR1   P##src
L     D [AR1,P#6.0]      // src area-crossing pointer
LAR1
LAR2   P##dst
L     D [AR2,P#6.0]      // dst area-crossing pointer
LAR2

L     #len
NEXT: T     #i
      L     DBB [AR1, P#0.0]   // read source
      T     DBB [AR2, P#0.0]   // write destination
      +AR1  P#1.0              // src += 1 byte
      +AR2  P#1.0              // dst += 1 byte
      L     #i
      LOOP  NEXT                // DEC #i; IF #i <> 0 THEN NEXT

To reverse the copy direction (fill dst from end to start), swap the loop direction and apply +AR1 with a negative offset:

// Inside the reversed loop body:
L     -8                    // -1 byte in bits
+AR1                        // src walks backward
L     -8
+AR2                        // dst walks backward

System Function Alternatives

For block moves larger than a few hundred bytes, replace the manual loop with SFC20 (BLKMOV) or SFC21 (FILL). SFC20 copies a source area to a destination area using any-pointer semantics internally, with cycle times on the order of 20 microseconds per kilobyte on a CPU 315-2 and 5 microseconds per kilobyte on a CPU 417-4. The AR1-based manual loop is appropriate when SFC20 cannot be used (overlapping source/destination, or mid-loop transformation of the data).

SFC Name Use Case AR1 Required?
SFC20 BLKMOV Non-overlapping block copy No (uses internal pointers)
SFC21 FILL Initialize block with pattern byte No
SFC81 UBLKMOV Uninterruptible block copy No
Manual AR1 loop -- Overlap or in-loop transformation Yes

STL +AR1 vs SCL PEEK/POKE

For new code on S7-1200/1500, Siemens recommends SCL with symbolic tags over STL with explicit AR1 walking. The PEEK and POKE intrinsics provide a near-equivalent capability without exposing the address register:

Capability STL +AR1 SCL PEEK/POKE
Read byte at runtime offset L DBB [AR1, P#0.0] PEEK_BOOL(area := 16#83, db := 10, byteOffset := i)
Write byte at runtime offset T DBB [AR1, P#0.0] POKE_BOOL(area := 16#83, db := 10, byteOffset := i, value := TRUE)
Read 32-bit value at runtime offset L D [AR1, P#0.0] PEEK_DWORD(area := 16#84, db := 10, byteOffset := i*4)
Cycle time per access 0.04-1.2 microseconds (CPU dependent) 3-6 microseconds (FCALL overhead)
Bounds checking None at runtime None at runtime
Area-crossing pointer support Native via LAR1 Implicit via area + db parameters

PEEK/POKE trades 2-4x slower execution for symbolic clarity and easier debugging. Inside a 10 ms OB1 cycle on a CPU 1516, a 1000-iteration PEEK loop completes in 6 ms; the equivalent STL loop completes in 1 ms. Choose based on whether the block is scan-rate critical or a maintenance utility.

CPU Cycle Time Notes

Cycle cost per +AR1 or LAR1 instruction varies by CPU generation:

CPU Family +AR1 (no operand) +AR1 P#x.y LAR1
CPU 312 IFM S7-300 (older) 1.2 microseconds 1.4 microseconds 1.5 microseconds
CPU 315-2 DP S7-300 0.5 microseconds 0.7 microseconds 0.6 microseconds
CPU 319-3 PN/DP S7-300 0.04 microseconds 0.05 microseconds 0.05 microseconds
CPU 412-2 S7-400 0.06 microseconds 0.08 microseconds 0.08 microseconds
CPU 416-3 S7-400 0.03 microseconds 0.04 microseconds 0.04 microseconds
CPU 1516-3 PN S7-1500 0.01 microseconds (non-optimized) 0.012 microseconds 0.01 microseconds

These figures come from the Instruction List reference manuals for the respective CPU families, available through the Siemens Industry Online Support portal under CPU-specific operating instructions. The CPU 1516 numbers assume non-optimized block compilation; optimized blocks reject the instructions outright. For cycle-critical code, prefer the no-operand +AR1 form (operands pre-loaded into ACCU1).

Edge Cases and Pitfalls

The workarounds above silently misbehave in five situations that surface as field bugs:

  1. Underflow on area-internal pointer: If AR1 points at byte 0 bit 0 and you +AR1 -1, the low 16 bits wrap to 0xFFFF (8191 bytes, 7 bits forward). For memory (M) or DB areas this points at a valid but unintended byte; for I/Q areas it points outside the process image and the read returns 0 silently. Always bounds-check before walking backward.
  2. Area-crossing pointer with non-zero DB number: +AR1 only touches bits 0-15. If you LAR1 P#DB10.DBX0.0 then +AR1 -80, AR1 still references DB 10, just 10 bytes earlier. This is usually what you want, but it does mean you cannot use +AR1 to step across to a different DB.
  3. Bit offset asymmetry: P#10.0 - P#0.7 is P#9.1, not P#10.0 (one byte, one bit forward). If you naively do L P#10.0; NEGI; +AR1 starting from a P#0.7 pointer, you end up at P#9.1. Verify with a watched pointer in VAT if bit-precise alignment matters.
  4. DB number greater than 255 on S7-300: S7-300 CPUs only accept DB numbers 1-255 in most models. An ANY body carrying DB number 300 loads successfully, but dereferencing it inside the CPU raises OB121 (programming error). Inspect the DB number word before walking.
  5. Optimized blocks on S7-1500: The +AR1 / LAR1 / TAR1 instructions exist on S7-1500 but only inside blocks compiled with Standard or non-optimized access. Optimized blocks forbid the symbolic pointer arithmetic in source code; you must use SCL slice notation or AT-view overlays. See the Siemens Industry Online Support portal for the official guidance on migrating from non-optimized to optimized block access.

S7-1200 and S7-1500 Considerations

S7-1200 (since firmware V2.0) and S7-1500 retain the AR1/AR2 registers and the +AR1 instruction, but Siemens has steadily migrated new code to symbolic addressing and tag-absolute access. In TIA Portal V15 and later, pointer arithmetic is supported only in STL source files inside FBs/FCs compiled with non-optimized access. Optimized blocks (the default for new S7-1200/1500 projects) reject +AR1, LAR1 P##Symbol, and TAR1 with a compile error.

For new S7-1500 code, prefer:

  • ARRAY indexing with a loop counter: "db".data[i] := "db".data[i-1]; in SCL compiles to bounds-checked, optimized machine code and avoids the AR1 register entirely.
  • AT view overlays: Declare a TEMP variable as AT %DB1.DBX0.0 BYTE and index into it with a static index. The compiler generates the pointer arithmetic for you.
  • PEEK/POKE on SCL: The PEEK (read) and POKE (write) intrinsics accept a pointer DWORD and a byte offset. They are the SCL equivalent of +AR1 without the STL.

Legacy S7-300/400 STL code that depends on AR1 walking still runs unmodified on an S7-1500 if the host FB is set to non-optimized access. Migration to optimized blocks requires rewriting the AR1 logic into the patterns above.

Verification and Diagnostics

After patching a block, verify the pointer math in three steps before deploying to a running process:

  1. Static STL watch table: Open the affected FB in STEP 7, right-click the AR1 dereference line, and Monitor/Modify. The status display shows the 32-bit AR1 in hex. Walk through one full loop iteration manually by toggling the loop index.
  2. Cross-reference the pointer byte offset: In the VAT, monitor both the modified AR1 and the operand it dereferences. Confirm the absolute byte addresses match what your algorithm expects. Common mismatch: P# (in bits) vs. DWORD (in bytes); one is 8x the other.
  3. OB121 / OB122 stress test: Wrap the AR1 walk in a loop that goes two iterations past the legal boundary. If OB121 fires (programming error, area violation) your bounds check is wrong; if OB122 fires (I/O access error) you walked into the process image gap.

For field commissioning, capture the AR1 value at the point of failure by writing it to a retentative MD before triggering the buggy dereference. The CPU fault buffer entry then shows both the failing instruction and the AR1 value, which usually pinpoints the underflow in seconds.

FAQ

Why does Siemens not provide a -AR1 instruction in S7-300/400?

The +AR1 micro-operation predates S7 and was carried over from the S5 family. A negative operand on +AR1 produces the identical result, so Siemens has not added a dedicated subtract instruction. The official line is documented in the STEP 7 STL reference manual under address-register instructions.

Can I do AR1 := AR1 - P#x.y in a single STL statement?

No single instruction exists. The shortest sequence is L P#x.y; NEGI; +AR1;. This preserves the symbolic offset in source and avoids manual bit-count arithmetic. The three-line form runs in approximately 3 microseconds on a CPU 315-2 DP and 1 microsecond on a CPU 416-3.

Does +AR1 with a negative value change the DB number of an area-crossing pointer?

No. +AR1 only manipulates bits 0-15 of AR1. For an area-crossing pointer, that means the byte offset and bit offset only; bits 16-31 (area ID and DB number) are untouched. To change the DB number, reload AR1 with LAR1 P#DBxx.DBXy.z.

What happens if AR1 underflows below byte 0 bit 0?

The low 16 bits wrap to 0xFFFF, pointing to byte 8191 bit 7 of the same area. For M and DB areas this reads or writes a valid but unintended location; for I/Q it points outside the process image and the read returns 0 silently or, on some CPUs, raises OB122. Always bounds-check before walking backward.

Can I use +AR1 inside an optimized S7-1500 block?

No. Optimized blocks in TIA Portal reject pointer-register instructions at compile time. Either set the block to non-optimized access for legacy compatibility, or rewrite the loop using ARRAY indexing or AT-view overlays. The Siemens Industry Online Support portal covers the migration path under the keyword "optimized block access."

Back to blog